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