Files
scummvm/engines/nancy/misc/hypertext.cpp
T
Kaloyan Chehlarski f3fee34f42 NANCY: Indent Autotext when wrapping
Replicating a bug in the original engine, where a value
from TBOX is used in all rendering, resulting in all wrapped
textlines appearing slightly indented inwards.
2024-01-30 23:13:12 +01:00

375 lines
12 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/tokenizer.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/misc/hypertext.h"
namespace Nancy {
namespace Misc {
struct ColorFontChange {
bool isFont;
uint numChars;
byte colorOrFontID;
};
void HypertextParser::initSurfaces(uint width, uint height, const Graphics::PixelFormat &format, uint32 backgroundColor, uint32 highlightBackgroundColor) {
_backgroundColor = backgroundColor;
_highlightBackgroundColor = highlightBackgroundColor;
_fullSurface.create(width, height, format);
_fullSurface.clear(backgroundColor);
_textHighlightSurface.create(width, height, format);
_textHighlightSurface.clear(highlightBackgroundColor);
}
void HypertextParser::addTextLine(const Common::String &text) {
_textLines.push_back(text);
_needsTextRedraw = true;
}
void HypertextParser::addImage(uint16 lineID, const Common::Rect &src) {
_imageLineIDs.push_back(lineID);
_imageSrcs.push_back(src);
}
void HypertextParser::setImageName(const Common::Path &name) {
_imageName = name;
}
void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffsetNonNewline, uint fontID, uint highlightFontID) {
using namespace Common;
const Font *font = nullptr;
const Font *highlightFont = nullptr;
Graphics::ManagedSurface image;
_numDrawnLines = 0;
if (!_imageName.empty()) {
g_nancy->_resource->loadImage(_imageName, image);
}
for (uint lineID = 0; lineID < _textLines.size(); ++lineID) {
Common::String currentLine;
bool hasHotspot = false;
Rect hotspot;
Common::Queue<ColorFontChange> colorTextChanges;
Common::Queue<uint16> newlineTokens;
newlineTokens.push(0);
int curFontID = fontID;
uint numNonSpaceChars = 0;
// Token braces plus invalid characters that are known to appear in strings
Common::StringTokenizer tokenizer(_textLines[lineID], "<>\"");
Common::String curToken;
while(!tokenizer.empty()) {
curToken = tokenizer.nextToken();
if (tokenizer.delimitersAtTokenBegin().lastChar() == '<' && tokenizer.delimitersAtTokenEnd().firstChar() == '>') {
switch (curToken.firstChar()) {
case 'i' :
// CC begin
// fall through
case 'o' :
// CC end
// fall through
case 'e' :
// End conversation. Originally used for quickly ending dialogue when debugging
// We do nothing and just skip
if (curToken.size() != 1) {
break;
}
continue;
case 'h' :
// Hotspot
if (curToken.size() != 1) {
break;
}
if (hasHotspot) {
// Replace duplicate hotspot token with a newline to copy the original behavior
currentLine += '\n';
}
hasHotspot = true;
continue;
case 'n' :
// Newline
if (curToken.size() != 1) {
break;
}
currentLine += '\n';
newlineTokens.push(numNonSpaceChars);
continue;
case 't' :
// Tab
if (curToken.size() != 1) {
break;
}
currentLine += '\t';
continue;
case 'c' :
// Color tokens
// We keep the positions (excluding spaces) and colors of the color tokens in a queue
if (curToken.size() != 2) {
break;
}
colorTextChanges.push({false, numNonSpaceChars, (byte)(curToken[1] - 48)});
continue;
case 'f' :
// Font token
// This selects a specific font ID for the following text
if (curToken.size() != 2) {
break;
}
colorTextChanges.push({true, numNonSpaceChars, (byte)(curToken[1] - 48)});
continue;
default:
break;
}
// Ignore non-tokens when they're between braces. This fixes nancy6 scenes 1953 & 1954,
// where some sound names slipped through into the text data.
debugC(Nancy::kDebugHypertext, "Unrecognized hypertext tag <%s>", curToken.c_str());
continue;
}
// Count the number of non-space characters. We use this to keep track
// of where color changes should happen, since including whitespaces
// presents a lot of edge cases when combined with word wrapping
for (uint i = 0; i < curToken.size(); ++i) {
if (curToken[i] != ' ') {
++numNonSpaceChars;
}
}
currentLine += curToken;
}
font = g_nancy->_graphicsManager->getFont(curFontID);
highlightFont = g_nancy->_graphicsManager->getFont(highlightFontID);
assert(font && highlightFont);
// Do word wrapping on the text, sans tokens. This assumes
// all text uses fonts of the same width
Array<Common::String> wrappedLines;
font->wordWrap(currentLine, textBounds.width(), wrappedLines, 0);
// Setup most of the hotspot
if (hasHotspot) {
hotspot.left = textBounds.left;
hotspot.top = textBounds.top + (_numDrawnLines * font->getFontHeight()) - 1;
hotspot.setHeight(wrappedLines.size() * font->getFontHeight());
hotspot.setWidth(0);
}
// Go through the wrapped lines and draw them, making sure to
// respect color tokens
uint totalCharsDrawn = 0;
byte colorID = _defaultTextColor;
uint numNewlineTokens = 0;
uint horizontalOffset = 0;
bool newLineStart = false;
for (uint lineNumber = 0; lineNumber < wrappedLines.size(); ++lineNumber) {
Common::String &line = wrappedLines[lineNumber];
horizontalOffset = 0;
newLineStart = false;
// Draw images
if (newlineTokens.front() <= totalCharsDrawn) {
newlineTokens.pop();
newLineStart = true;
for (uint i = 0; i < _imageLineIDs.size(); ++i) {
if (numNewlineTokens == _imageLineIDs[i]) {
// A lot of magic numbers that make sure we draw pixel-perfect. This is a mess for three reasons:
// - The original engine draws strings with a bottom-left anchor, while ScummVM uses top-left
// - The original engine uses inclusive rects, while ScummVM uses non-includive
// - The original engine does some stupid stuff with spacing
// This works correctly in nancy7, but might fail with different games/fonts
if (lineNumber != 0) {
_imageVerticalOffset += (font->getFontHeight() + 1) / 2 + 1;
}
_fullSurface.blitFrom(image, _imageSrcs[i],
Common::Point( textBounds.left + horizontalOffset + 1,
textBounds.top + _numDrawnLines * highlightFont->getFontHeight() + _imageVerticalOffset));
_imageVerticalOffset += _imageSrcs[i].height() - 1;
if (lineNumber == 0) {
_imageVerticalOffset += font->getFontHeight() / 2 - 1;
} else {
_imageVerticalOffset += (font->getFontHeight() + 1) / 2 + 3;
}
}
}
++numNewlineTokens;
}
// Trim whitespaces (only) at beginning and end of wrapped lines
while (line.lastChar() == ' ') {
line.deleteLastChar();
}
while (line.firstChar() == ' ') {
line.deleteChar(0);
}
// Set the width of the hotspot
if (hasHotspot) {
hotspot.setWidth(MAX<int16>(hotspot.width(), font->getStringWidth(line)));
}
while (!line.empty()) {
Common::String subLine;
while (colorTextChanges.size() && totalCharsDrawn >= colorTextChanges.front().numChars) {
// We have a color/font change token at begginning of (what's left of) the current line
ColorFontChange change = colorTextChanges.pop();
if (change.isFont) {
curFontID = change.colorOrFontID;
font = g_nancy->_graphicsManager->getFont(curFontID);
} else {
colorID = change.colorOrFontID;
}
}
if (colorTextChanges.size() && totalCharsDrawn < colorTextChanges.front().numChars && colorTextChanges.front().numChars < (totalCharsDrawn + line.size())) {
// There's a token inside the current line, so split off the part before it
subLine = line.substr(0, colorTextChanges.front().numChars - totalCharsDrawn);
line = line.substr(subLine.size());
}
// Choose whether to draw the subLine, or the full line
Common::String &stringToDraw = subLine.size() ? subLine : line;
// Draw the normal text
font->drawString( &_fullSurface,
stringToDraw,
textBounds.left + horizontalOffset + (newLineStart ? 0 : leftOffsetNonNewline),
textBounds.top + _numDrawnLines * font->getFontHeight() + _imageVerticalOffset,
textBounds.width(),
colorID);
// Then, draw the highlight
if (hasHotspot) {
highlightFont->drawString( &_textHighlightSurface,
stringToDraw,
textBounds.left + horizontalOffset + (newLineStart ? leftOffsetNonNewline : 0),
textBounds.top + _numDrawnLines * highlightFont->getFontHeight() + _imageVerticalOffset,
textBounds.width(),
colorID);
}
// Count number of non-space characters drawn. Used for color.
// Note that we use isSpace() specifically to exclude the tab character
for (uint i = 0; i < stringToDraw.size(); ++i) {
if (!isSpace(stringToDraw[i])) {
++totalCharsDrawn;
}
}
if (subLine.size()) {
horizontalOffset += font->getStringWidth(subLine);
} else {
break;
}
}
++_numDrawnLines;
// Record the height of the text currently drawn. Used for textbox scrolling
_drawnTextHeight = (_numDrawnLines - 1) * font->getFontHeight() + _imageVerticalOffset;
}
// Draw the footer image(s)
for (uint i = 0; i < _imageLineIDs.size(); ++i) {
if (numNewlineTokens <= _imageLineIDs[i]) {
_imageVerticalOffset += (font->getFontHeight() + 1) / 2 + 1;
_fullSurface.blitFrom(image, _imageSrcs[i],
Common::Point( textBounds.left + horizontalOffset + 1,
textBounds.top + _numDrawnLines * highlightFont->getFontHeight() + _imageVerticalOffset));
_imageVerticalOffset += _imageSrcs[i].height() - 1;
if (i < _imageLineIDs.size() - 1) {
_imageVerticalOffset += (font->getFontHeight() + 1) / 2 + 3;
}
_drawnTextHeight = (_numDrawnLines - 1) * font->getFontHeight() + _imageVerticalOffset;
}
}
// Add the hotspot to the list
if (hasHotspot) {
_hotspots.push_back(hotspot);
}
// Note: disabled since it was most likely a bug, and is behavior exclusive to the textbox
/*
// Simulate a bug in the original engine where player text longer than
// a single line gets a double newline afterwards
if (wrappedLines.size() > 1 && hasHotspot) {
++_numLines;
if (lineID == _textLines.size() - 1) {
_lastResponseisMultiline = true;
}
}
*/
// Add a newline after every full piece of text
++_numDrawnLines;
_drawnTextHeight += font->getFontHeight();
}
// Add a line's height at end of text to replicate original behavior
if (font) {
_drawnTextHeight += font->getFontHeight();
}
_needsTextRedraw = false;
}
void HypertextParser::clear() {
if (_textLines.size()) {
_fullSurface.clear(_backgroundColor);
_textHighlightSurface.clear(_highlightBackgroundColor);
_textLines.clear();
_hotspots.clear();
_numDrawnLines = 0;
_drawnTextHeight = 0;
}
}
} // End of namespace Misc
} // End of namespace Nancy