1 /* ResidualVM - A 3D game interpreter
2 *
3 * ResidualVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the AUTHORS
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "common/unicode-bidi.h"
24 #include "engines/grim/debug.h"
25 #include "engines/grim/grim.h"
26 #include "engines/grim/textobject.h"
27 #include "engines/grim/savegame.h"
28 #include "engines/grim/lua.h"
29 #include "engines/grim/font.h"
30 #include "engines/grim/gfx_base.h"
31 #include "engines/grim/color.h"
32
33 namespace Grim {
34
TextObjectCommon()35 TextObjectCommon::TextObjectCommon() :
36 _x(0), _y(0), _fgColor(0), _justify(0), _width(0), _height(0),
37 _font(nullptr), _duration(0), _layer(0), _coords(0) {
38 if (g_grim)
39 g_grim->invalidateTextObjectsSortOrder();
40 }
41
setLayer(int layer)42 void TextObjectCommon::setLayer(int layer) {
43 _layer = layer;
44 if (g_grim)
45 g_grim->invalidateTextObjectsSortOrder();
46 }
47
TextObject()48 TextObject::TextObject() :
49 TextObjectCommon(), _numberLines(1), _textID(""), _elapsedTime(0),
50 _maxLineWidth(0), _lines(nullptr), _userData(nullptr), _created(false),
51 _blastDraw(false), _isSpeech(false), _stackLevel(0) {
52 }
53
~TextObject()54 TextObject::~TextObject() {
55 delete[] _lines;
56 if (_created) {
57 g_driver->destroyTextObject(this);
58 }
59 if (g_grim)
60 g_grim->invalidateTextObjectsSortOrder();
61 }
62
setText(const Common::String & text,bool delaySetup)63 void TextObject::setText(const Common::String &text, bool delaySetup) {
64 destroy();
65 _textID = text;
66 if (!delaySetup)
67 setupText();
68 }
69
reset()70 void TextObject::reset() {
71 destroy();
72 setupText();
73 }
74
saveState(SaveGame * state) const75 void TextObject::saveState(SaveGame *state) const {
76 state->writeColor(_fgColor);
77
78 state->writeLESint32(_x);
79 state->writeLESint32(_y);
80 state->writeLESint32(_width);
81 state->writeLESint32(_height);
82 state->writeLESint32(_justify);
83 state->writeLESint32(_numberLines);
84 state->writeLESint32(_duration);
85
86 state->writeBool(_blastDraw);
87 state->writeBool(_isSpeech);
88 state->writeLESint32(_elapsedTime);
89
90 if (_font) {
91 state->writeLESint32(_font->getId());
92 } else {
93 state->writeLESint32(-1);
94 }
95
96 state->writeString(_textID);
97
98 if (g_grim->getGameType() == GType_MONKEY4) {
99 state->writeLESint32(_layer);
100 state->writeLESint32(_stackLevel);
101 }
102 }
103
restoreState(SaveGame * state)104 bool TextObject::restoreState(SaveGame *state) {
105 _fgColor = state->readColor();
106
107 _x = state->readLESint32();
108 _y = state->readLESint32();
109 _width = state->readLESint32();
110 _height = state->readLESint32();
111 _justify = state->readLESint32();
112 _numberLines = state->readLESint32();
113 _duration = state->readLESint32();
114
115 _blastDraw = state->readBool();
116 _isSpeech = state->readBool();
117 _elapsedTime = state->readLESint32();
118
119 int32 fontId = state->readLESint32();
120 if (fontId == -1) {
121 _font = nullptr;
122 } else {
123 _font = Font::getPool().getObject(fontId);
124 }
125
126 _textID = state->readString();
127
128 if (g_grim->getGameType() == GType_MONKEY4) {
129 _layer = state->readLESint32();
130 _stackLevel = state->readLESint32();
131 g_grim->invalidateTextObjectsSortOrder();
132 }
133
134 setupText();
135 _created = false;
136 _userData = nullptr;
137
138 return true;
139 }
140
setDefaults(const TextObjectDefaults * defaults)141 void TextObject::setDefaults(const TextObjectDefaults *defaults) {
142 _x = defaults->getX();
143 _y = defaults->getY();
144 _font = defaults->getFont();
145 _fgColor = defaults->getFGColor();
146 _justify = defaults->getJustify();
147 }
148
getBitmapWidth() const149 int TextObject::getBitmapWidth() const {
150 return _maxLineWidth;
151 }
152
getBitmapHeight() const153 int TextObject::getBitmapHeight() const {
154 return _numberLines * _font->getKernedHeight();
155 }
156
getTextCharPosition(int pos)157 int TextObject::getTextCharPosition(int pos) {
158 int width = 0;
159 Common::String msg = LuaBase::instance()->parseMsgText(_textID.c_str(), nullptr);
160 for (int i = 0; (msg[i] != '\0') && (i < pos); ++i) {
161 width += _font->getCharKernedWidth(msg[i]);
162 }
163 return width;
164 }
165
destroy()166 void TextObject::destroy() {
167 if (_created) {
168 g_driver->destroyTextObject(this);
169 _created = false;
170 }
171 }
172
setupText()173 void TextObject::setupText() {
174 Common::String msg = LuaBase::instance()->parseMsgText(_textID.c_str(), nullptr);
175 Common::String message;
176
177 // remove spaces (NULL_TEXT) from the end of the string,
178 // while this helps make the string unique it screws up
179 // text justification
180 // remove char of id 13 from the end of the string,
181 int pos = msg.size() - 1;
182 while (pos >= 0 && (msg[pos] == ' ' || msg[pos] == 13)) {
183 msg.deleteLastChar();
184 pos = msg.size() - 1;
185 }
186 delete[] _lines;
187 if (msg.size() == 0) {
188 _lines = nullptr;
189 return;
190 }
191
192 // format the output message to incorporate line wrapping
193 // (if necessary) for the text object
194 const int SCREEN_WIDTH = _width ? _width : 640;
195 const int SCREEN_MARGIN = SCREEN_WIDTH / 10;
196
197 // If the speaker is too close to the edge of the screen we have to make
198 // some room for the subtitles.
199 if (_isSpeech) {
200 if (_x < SCREEN_MARGIN) {
201 _x = SCREEN_MARGIN;
202 } else if (SCREEN_WIDTH - _x < SCREEN_MARGIN) {
203 _x = SCREEN_WIDTH - SCREEN_MARGIN;
204 }
205 }
206
207 // The maximum width for any line of text is determined by the justification
208 // mode. Note that there are no left/right margins -- this is consistent
209 // with GrimE.
210 int maxWidth = 0;
211 if (_justify == CENTER) {
212 maxWidth = 2 * MIN(_x, SCREEN_WIDTH - _x);
213 } else if (_justify == LJUSTIFY) {
214 maxWidth = SCREEN_WIDTH - _x;
215 } else if (_justify == RJUSTIFY) {
216 maxWidth = _x;
217 }
218
219 // We break the message to lines not longer than maxWidth
220 Common::String currLine;
221 _numberLines = 1;
222 int lineWidth = 0;
223 for (uint i = 0; i < msg.size(); i++) {
224 message += msg[i];
225 currLine += msg[i];
226 lineWidth += _font->getCharKernedWidth(msg[i]);
227
228 if (currLine.size() > 1 && lineWidth > maxWidth) {
229 if (currLine.contains(' ')) {
230 while (currLine.lastChar() != ' ' && currLine.size() > 1) {
231 lineWidth -= _font->getCharKernedWidth(currLine.lastChar());
232 message.deleteLastChar();
233 currLine.deleteLastChar();
234 --i;
235 }
236 } else { // if it is a unique word
237 int dashWidth = _font->getCharKernedWidth('-');
238 while (lineWidth + dashWidth > maxWidth && currLine.size() > 1) {
239 lineWidth -= _font->getCharKernedWidth(currLine.lastChar());
240 message.deleteLastChar();
241 currLine.deleteLastChar();
242 --i;
243 }
244 message += '-';
245 }
246 message += '\n';
247 currLine.clear();
248 _numberLines++;
249
250 lineWidth = 0;
251 }
252 }
253
254 // If the text object is a speech subtitle, the y parameter is the
255 // coordinate of the bottom of the text block (instead of the top). It means
256 // that every extra line pushes the previous lines up, instead of being
257 // printed further down the screen.
258 const int SCREEN_TOP_MARGIN = _font->getKernedHeight();
259 if (_isSpeech) {
260 _y -= _numberLines * _font->getKernedHeight();
261 if (_y < SCREEN_TOP_MARGIN) {
262 _y = SCREEN_TOP_MARGIN;
263 }
264 }
265
266 _lines = new Common::String[_numberLines];
267
268
269 // Reset the max width so it can be recalculated
270 _maxLineWidth = 0;
271
272 for (int j = 0; j < _numberLines; j++) {
273 int nextLinePos, cutLen;
274 const char *breakPos = strchr(message.c_str(), '\n');
275 if (breakPos) {
276 nextLinePos = breakPos - message.c_str();
277 cutLen = nextLinePos + 1;
278 } else {
279 nextLinePos = message.size();
280 cutLen = nextLinePos;
281 }
282 Common::String currentLine(message.c_str(), message.c_str() + nextLinePos);
283
284 // Reverse the line for the Hebrew translation
285 if (g_grim->getGameLanguage() == Common::HE_ISR)
286 currentLine = Common::convertBiDiString(currentLine, Common::kWindows1255);
287
288 _lines[j] = currentLine;
289 int width = _font->getKernedStringLength(currentLine);
290 if (width > _maxLineWidth)
291 _maxLineWidth = width;
292 for (int count = 0; count < cutLen; count++)
293 message.deleteChar(0);
294 }
295 _elapsedTime = 0;
296 }
297
getLineX(int line) const298 int TextObject::getLineX(int line) const {
299 int x = _x;
300 if (_justify == CENTER)
301 x = _x - (_font->getKernedStringLength(_lines[line]) / 2);
302 else if (_justify == RJUSTIFY)
303 x = _x - getBitmapWidth();
304
305 if (x < 0)
306 x = 0;
307 return x;
308 }
309
getLineY(int line) const310 int TextObject::getLineY(int line) const {
311 int y = _y;
312
313 if (g_grim->getGameType() == GType_GRIM) {
314 if (_blastDraw) { // special case for Grim for menu text draw, issue #1083
315 y = _y + 5;
316 } else {
317 /* if (_font->getKernedHeight() == 21) // talk_font,verb_font
318 y = _y - 6;
319 else if (_font->getKernedHeight() == 26) // special_font
320 y = _y - 12;
321 else */if (_font->getKernedHeight() == 13) // computer_font
322 y = _y - 6;/*
323 else if (_font->getKernedHeight() == 19) // pt_font
324 y = _y - 9;*/
325 else
326 y = _y;
327 }
328 }
329
330 if (y < 0)
331 y = 0;
332 y += _font->getKernedHeight() * line;
333
334 return y;
335 }
336
draw()337 void TextObject::draw() {
338 if (!_lines)
339 return;
340
341 if (!_created) {
342 g_driver->createTextObject(this);
343 _created = true;
344 }
345
346 if (_justify > 3 || _justify < 0)
347 warning("TextObject::draw: Unknown justification code (%d)", _justify);
348
349 g_driver->drawTextObject(this);
350
351 }
352
update()353 void TextObject::update() {
354 if (!_duration || !_created) {
355 return;
356 }
357
358 _elapsedTime += g_grim->getFrameTime();
359 if (_elapsedTime > _duration) {
360 delete this;
361 }
362 }
363
364 } // end of namespace Grim
365