mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
The text stack in EMI must be saved and restored in save games because the game will call PopText after returning from the menu. Instead of managing separate lists for each text stack level this patch adds a _stackLevel field that is incremented for PushText and decremented for PopText. Only text objects from the stack top (level 0) are rendered.
382 lines
9.7 KiB
C++
382 lines
9.7 KiB
C++
/* ResidualVM - A 3D game interpreter
|
|
*
|
|
* ResidualVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the AUTHORS
|
|
* 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
#include "engines/grim/debug.h"
|
|
#include "engines/grim/grim.h"
|
|
#include "engines/grim/textobject.h"
|
|
#include "engines/grim/savegame.h"
|
|
#include "engines/grim/lua.h"
|
|
#include "engines/grim/font.h"
|
|
#include "engines/grim/gfx_base.h"
|
|
#include "engines/grim/color.h"
|
|
|
|
namespace Grim {
|
|
|
|
TextObjectCommon::TextObjectCommon() :
|
|
_x(0), _y(0), _fgColor(0), _justify(0), _width(0), _height(0),
|
|
_font(NULL), _duration(0), _positioned(false),
|
|
_posX(0), _posY(0), _layer(0) {
|
|
if (g_grim)
|
|
g_grim->invalidateTextObjectsSortOrder();
|
|
}
|
|
|
|
void TextObjectCommon::setLayer(int layer) {
|
|
_layer = layer;
|
|
if (g_grim)
|
|
g_grim->invalidateTextObjectsSortOrder();
|
|
}
|
|
|
|
TextObject::TextObject() :
|
|
TextObjectCommon(), _numberLines(1), _textID(""), _elapsedTime(0),
|
|
_maxLineWidth(0), _lines(NULL), _userData(NULL), _created(false),
|
|
_blastDraw(false), _isSpeech(false), _stackLevel(0) {
|
|
}
|
|
|
|
TextObject::~TextObject() {
|
|
delete[] _lines;
|
|
if (_created) {
|
|
g_driver->destroyTextObject(this);
|
|
}
|
|
if (g_grim)
|
|
g_grim->invalidateTextObjectsSortOrder();
|
|
}
|
|
|
|
void TextObject::setText(const Common::String &text) {
|
|
destroy();
|
|
_textID = text;
|
|
setupText();
|
|
}
|
|
|
|
void TextObject::reset() {
|
|
destroy();
|
|
setupText();
|
|
}
|
|
|
|
void TextObject::saveState(SaveGame *state) const {
|
|
state->writeColor(_fgColor);
|
|
|
|
state->writeLESint32(_x);
|
|
state->writeLESint32(_y);
|
|
state->writeLESint32(_width);
|
|
state->writeLESint32(_height);
|
|
state->writeLESint32(_justify);
|
|
state->writeLESint32(_numberLines);
|
|
state->writeLESint32(_duration);
|
|
|
|
state->writeBool(_blastDraw);
|
|
state->writeBool(_isSpeech);
|
|
state->writeLESint32(_elapsedTime);
|
|
|
|
state->writeLESint32(_font->getId());
|
|
|
|
state->writeString(_textID);
|
|
|
|
if (g_grim->getGameType() == GType_MONKEY4) {
|
|
state->writeLESint32(_layer);
|
|
state->writeLESint32(_stackLevel);
|
|
}
|
|
}
|
|
|
|
bool TextObject::restoreState(SaveGame *state) {
|
|
_fgColor = state->readColor();
|
|
|
|
_x = state->readLESint32();
|
|
_y = state->readLESint32();
|
|
_width = state->readLESint32();
|
|
_height = state->readLESint32();
|
|
_justify = state->readLESint32();
|
|
_numberLines = state->readLESint32();
|
|
_duration = state->readLESint32();
|
|
|
|
_blastDraw = state->readBool();
|
|
_isSpeech = state->readBool();
|
|
_elapsedTime = state->readLESint32();
|
|
|
|
_font = Font::getPool().getObject(state->readLESint32());
|
|
|
|
_textID = state->readString();
|
|
|
|
if (g_grim->getGameType() == GType_MONKEY4) {
|
|
_layer = state->readLESint32();
|
|
_stackLevel = state->readLESint32();
|
|
g_grim->invalidateTextObjectsSortOrder();
|
|
}
|
|
|
|
setupText();
|
|
_created = false;
|
|
_userData = NULL;
|
|
|
|
return true;
|
|
}
|
|
|
|
void TextObject::setDefaults(const TextObjectDefaults *defaults) {
|
|
_x = defaults->getX();
|
|
_y = defaults->getY();
|
|
_font = defaults->getFont();
|
|
_fgColor = defaults->getFGColor();
|
|
_justify = defaults->getJustify();
|
|
}
|
|
|
|
int TextObject::getBitmapWidth() const {
|
|
return _maxLineWidth;
|
|
}
|
|
|
|
int TextObject::getBitmapHeight() const {
|
|
return _numberLines * _font->getHeight();
|
|
}
|
|
|
|
int TextObject::getTextCharPosition(int pos) {
|
|
int width = 0;
|
|
Common::String msg = LuaBase::instance()->parseMsgText(_textID.c_str(), NULL);
|
|
for (int i = 0; (msg[i] != '\0') && (i < pos); ++i) {
|
|
width += _font->getCharWidth(msg[i]);
|
|
}
|
|
return width;
|
|
}
|
|
|
|
void TextObject::destroy() {
|
|
if (_created) {
|
|
g_driver->destroyTextObject(this);
|
|
_created = false;
|
|
}
|
|
}
|
|
|
|
void TextObject::reposition() {
|
|
if (_positioned)
|
|
return;
|
|
|
|
_positioned = true;
|
|
_posX = _x;
|
|
_posY = _y;
|
|
if (g_grim->getGameType() == GType_MONKEY4) {
|
|
if (_isSpeech || abs(_posX) >= 320 || abs(_posY) >= 240) {
|
|
_posY = _posY > 0 ? 480 - _posY : abs(_posY);
|
|
if (_posX < 0)
|
|
_posX = _posX + 640;
|
|
if (_justify == CENTER && _posX == 0)
|
|
_posX = 320;
|
|
} else {
|
|
_posX = 320 + _posX;
|
|
_posY = 240 - _posY;
|
|
}
|
|
|
|
Debug::debug(Debug::TextObjects, "Repositioning (%d, %d) -> (%d, %d)", _x, _y, _posX, _posY);
|
|
assert(0 <= _posX && _posX <= 640);
|
|
assert(0 <= _posY && _posY <= 480);
|
|
}
|
|
}
|
|
|
|
void TextObject::setupText() {
|
|
Common::String msg = LuaBase::instance()->parseMsgText(_textID.c_str(), NULL);
|
|
Common::String message;
|
|
|
|
// remove spaces (NULL_TEXT) from the end of the string,
|
|
// while this helps make the string unique it screws up
|
|
// text justification
|
|
// remove char of id 13 from the end of the string,
|
|
int pos = msg.size() - 1;
|
|
while (pos >= 0 && (msg[pos] == ' ' || msg[pos] == 13)) {
|
|
msg.deleteLastChar();
|
|
pos = msg.size() - 1;
|
|
}
|
|
delete[] _lines;
|
|
if (msg.size() == 0) {
|
|
_lines = NULL;
|
|
return;
|
|
}
|
|
|
|
reposition();
|
|
|
|
// format the output message to incorporate line wrapping
|
|
// (if necessary) for the text object
|
|
const int SCREEN_WIDTH = _width ? _width : 640;
|
|
const int SCREEN_MARGIN = 75;
|
|
|
|
// If the speaker is too close to the edge of the screen we have to make
|
|
// some room for the subtitles.
|
|
if (_isSpeech) {
|
|
if (_posX < SCREEN_MARGIN) {
|
|
_posX = SCREEN_MARGIN;
|
|
} else if (SCREEN_WIDTH - _posX < SCREEN_MARGIN) {
|
|
_posX = SCREEN_WIDTH - SCREEN_MARGIN;
|
|
}
|
|
}
|
|
|
|
// The maximum width for any line of text is determined by the justification
|
|
// mode. Note that there are no left/right margins -- this is consistent
|
|
// with GrimE.
|
|
int maxWidth = 0;
|
|
if (_justify == CENTER) {
|
|
maxWidth = 2 * MIN(_posX, SCREEN_WIDTH - _posX);
|
|
} else if (_justify == LJUSTIFY) {
|
|
maxWidth = SCREEN_WIDTH - _posX;
|
|
} else if (_justify == RJUSTIFY) {
|
|
maxWidth = _posX;
|
|
}
|
|
|
|
// We break the message to lines not longer than maxWidth
|
|
Common::String currLine;
|
|
_numberLines = 1;
|
|
int lineWidth = 0;
|
|
int maxLineWidth = 0;
|
|
for (uint i = 0; i < msg.size(); i++) {
|
|
lineWidth += MAX(_font->getCharWidth(msg[i]), _font->getCharDataWidth(msg[i]));
|
|
if (lineWidth > maxWidth) {
|
|
bool wordSplit = false;
|
|
if (currLine.contains(' ')) {
|
|
while (msg[i] != ' ' && i > 0) {
|
|
lineWidth -= MAX(_font->getCharWidth(msg[i]), _font->getCharDataWidth(msg[i]));
|
|
message.deleteLastChar();
|
|
--i;
|
|
}
|
|
} else if (msg[i] != ' ') { // if it is a unique word
|
|
int dashWidth = MAX(_font->getCharWidth('-'), _font->getCharDataWidth('-'));
|
|
while (lineWidth + dashWidth > maxWidth) {
|
|
lineWidth -= MAX(_font->getCharWidth(msg[i]), _font->getCharDataWidth(msg[i]));
|
|
message.deleteLastChar();
|
|
--i;
|
|
}
|
|
message += '-';
|
|
wordSplit = true;
|
|
}
|
|
message += '\n';
|
|
currLine.clear();
|
|
_numberLines++;
|
|
|
|
if (lineWidth > maxLineWidth) {
|
|
maxLineWidth = lineWidth;
|
|
}
|
|
lineWidth = 0;
|
|
|
|
if (wordSplit) {
|
|
lineWidth += MAX(_font->getCharWidth(msg[i]), _font->getCharDataWidth(msg[i]));
|
|
} else {
|
|
continue; // don't add the space back
|
|
}
|
|
}
|
|
|
|
if (lineWidth > maxLineWidth)
|
|
maxLineWidth = lineWidth;
|
|
|
|
message += msg[i];
|
|
currLine += msg[i];
|
|
}
|
|
|
|
// If the text object is a speech subtitle, the y parameter is the
|
|
// coordinate of the bottom of the text block (instead of the top). It means
|
|
// that every extra line pushes the previous lines up, instead of being
|
|
// printed further down the screen.
|
|
const int SCREEN_TOP_MARGIN = 16;
|
|
if (_isSpeech) {
|
|
_posY -= _numberLines * _font->getHeight();
|
|
if (_posY < SCREEN_TOP_MARGIN) {
|
|
_posY = SCREEN_TOP_MARGIN;
|
|
}
|
|
}
|
|
|
|
_lines = new Common::String[_numberLines];
|
|
|
|
for (int j = 0; j < _numberLines; j++) {
|
|
int nextLinePos, cutLen;
|
|
const char *breakPos = strchr(message.c_str(), '\n');
|
|
if (breakPos) {
|
|
nextLinePos = breakPos - message.c_str();
|
|
cutLen = nextLinePos + 1;
|
|
} else {
|
|
nextLinePos = message.size();
|
|
cutLen = nextLinePos;
|
|
}
|
|
Common::String currentLine(message.c_str(), message.c_str() + nextLinePos);
|
|
_lines[j] = currentLine;
|
|
int width = _font->getStringLength(currentLine);
|
|
if (width > _maxLineWidth)
|
|
_maxLineWidth = width;
|
|
for (int count = 0; count < cutLen; count++)
|
|
message.deleteChar(0);
|
|
}
|
|
_elapsedTime = 0;
|
|
}
|
|
|
|
int TextObject::getLineX(int line) const {
|
|
int x = _posX;
|
|
if (_justify == CENTER)
|
|
x = _posX - (_font->getStringLength(_lines[line]) / 2);
|
|
else if (_justify == RJUSTIFY)
|
|
x = _posX - getBitmapWidth();
|
|
|
|
if (x < 0)
|
|
x = 0;
|
|
return x;
|
|
}
|
|
|
|
int TextObject::getLineY(int line) const {
|
|
int y = _posY;
|
|
if (_blastDraw)
|
|
y = _posY + 5;
|
|
else {
|
|
if (_font->getHeight() == 21) // talk_font,verb_font
|
|
y = _posY - 6;
|
|
else if (_font->getHeight() == 26) // special_font
|
|
y = _posY - 12;
|
|
else if (_font->getHeight() == 13) // computer_font
|
|
y = _posY - 6;
|
|
else if (_font->getHeight() == 19) // pt_font
|
|
y = _posY - 9;
|
|
else
|
|
y = _posY;
|
|
}
|
|
if (y < 0)
|
|
y = 0;
|
|
y += _font->getHeight() * line;
|
|
|
|
return y;
|
|
}
|
|
|
|
void TextObject::draw() {
|
|
if (!_lines)
|
|
return;
|
|
|
|
if (!_created) {
|
|
g_driver->createTextObject(this);
|
|
_created = true;
|
|
}
|
|
|
|
if (_justify > 3 || _justify < 0)
|
|
warning("TextObject::draw: Unknown justification code (%d)", _justify);
|
|
|
|
g_driver->drawTextObject(this);
|
|
|
|
}
|
|
|
|
void TextObject::update() {
|
|
if (!_duration || !_created) {
|
|
return;
|
|
}
|
|
|
|
_elapsedTime += g_grim->getFrameTime();
|
|
if (_elapsedTime > _duration) {
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
} // end of namespace Grim
|