1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
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 "bladerunner/subtitles.h"
24 
25 #include "bladerunner/font.h"
26 #include "bladerunner/text_resource.h"
27 #include "bladerunner/audio_speech.h"
28 
29 #include "common/debug.h"
30 
31 #include "graphics/font.h"
32 #include "graphics/fonts/ttf.h"
33 
34 namespace BladeRunner {
35 
36 /*
37  * Optional support for subtitles
38  *
39  * CHECK what happens in VQA where the audio plays separately (are the finales such VQAs ?)
40  * TODO? Use another escape sequence to progressively display text in a line (like in SCUMM games) <-- this could be very useful with very long lines
41  *			- might also need an extra manual time or ticks parameter to determine when during the display of the first segment we should switch to the second.
42  * TODO? A more advanced subtitles system:
43  *          TODO: subtitles could be independent from sound playing (but probably should disappear when switching between UI screens)
44  *          TODO?: Support for queuing subtitles when more than one subtitle should play for a spoken dialogue (due to a very long quote)
45  *          TODO?: Predefine a minimum time for a subtitle to appear, before it is interrupted by the next one. (might need queuing)
46  *          TODO?: If the subtitle is the last one then extend its duration to another predefined delay.
47  *
48  * DONE Removed support for internal font TAHOMA18 - this particular font is corrupted!
49  * DONE Create and Support proper external FON for subtitles.
50  * DONE split at new line character (priority over auto-split)
51  * DONE auto-split a long line into two
52  * DONE support the basic 2 line subtitles
53  * DONE support a third line for subtitles (some quotes are too long for 2 lines). Are there quotes that are too long for 3 lines?
54  * DONE handle missing subtitle files! Gracefully don't show subtitles for VQAs or in-game dialogue if the required respective files are missing!
55  * DONE add subtitle files for the rest of VQAs that have spoken dialogue
56  * DONE A system to auto-split a dialogue after some max total width of character glyphs per line.
57  * DONE - OK - CHECK What happens with skipped dialogue (enter / skip dialogue key pressed)
58  * DONE - OK - CHECK what happens in VQA when no corresponding TRE subs file?
59  */
60 
61 const char *Subtitles::SUBTITLES_FONT_FILENAME_EXTERNAL = "SUBTLS_E.FON";
62 
63 const char *Subtitles::SUBTITLES_VERSION_TRENAME        = "SBTLVERS"; // addon resource file for Subtitles version info - can only be SBTLVERS.TRE
64 /*
65  * All entries need to have the language code appended (after a '_').
66  * And all entries should get the suffix extension ".TRx"; the last letter in extension "TR*" should also be the language code
67  * If/When adding new Text Resources here --> Update kMaxTextResourceEntries and also update method getIdxForSubsTreName()
68  */
69 const char *Subtitles::SUBTITLES_FILENAME_PREFIXES[kMaxTextResourceEntries] = {
70 	"INGQUO",           // 0 // (in-game subtitles, not VQA subtitles)
71 	"WSTLGO",           // 1 // all game (language) versions have the English ('E') version of WSTLGO
72 	"BRLOGO",           // 2 // all game (language) versions have the English ('E') version of BRLOGO
73 	"INTRO",            // 3
74 	"MW_A",             // 4
75 	"MW_B01",           // 5
76 	"MW_B02",           // 6
77 	"MW_B03",           // 7
78 	"MW_B04",           // 8
79 	"MW_B05",           // 9
80 	"INTRGT",           // 10
81 	"MW_C01",           // 11
82 	"MW_C02",           // 12
83 	"MW_C03",           // 13
84 	"MW_D",             // 14
85 	"END04A",           // 15
86 	"END04B",           // 16
87 	"END04C",           // 17
88 	"END06",            // 18
89 	"END01A",           // 19
90 	"END01B",           // 20
91 	"END01C",           // 21
92 	"END01D",           // 22
93 	"END01E",           // 23
94 	"END01F",           // 24
95 	"END03",            // 25
96 	"TB_FLY"            // 26
97 };
98 
99 /**
100  * Subtitles Constructor
101  */
Subtitles(BladeRunnerEngine * vm)102 Subtitles::Subtitles(BladeRunnerEngine *vm) {
103 	_vm = vm;
104 	_isSystemActive = false;
105 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
106 		_vqaSubsTextResourceEntries[i] = nullptr;
107 		_gameSubsResourceEntriesFound[i] = false;
108 	}
109 	_font = nullptr;
110 	_useUTF8 = false;
111 	reset();
112 }
113 
114 /**
115  * Subtitles Destructor
116  */
~Subtitles()117 Subtitles::~Subtitles() {
118 	reset();
119 }
120 
121 //
122 // Init is kept separated from constructor to allow not loading up resources if subtitles system is disabled
123 //
init(void)124 void Subtitles::init(void) {
125 	// Loading subtitles versioning info if available
126 	TextResource versionTxtResource(_vm);
127 	if ( versionTxtResource.open(SUBTITLES_VERSION_TRENAME, false)) {
128 		_subtitlesInfo.credits       = versionTxtResource.getText((uint32)0);
129 		_subtitlesInfo.versionStr    = versionTxtResource.getText((uint32)1);
130 		_subtitlesInfo.dateOfCompile = versionTxtResource.getText((uint32)2);
131 		_subtitlesInfo.languageMode  = versionTxtResource.getText((uint32)3);
132 		Common::String fontType      = versionTxtResource.getText((uint32)4);
133 		_subtitlesInfo.fontName      = versionTxtResource.getText((uint32)5);
134 		Common::String license       = versionTxtResource.getText((uint32)6);
135 		Common::String licenseLink   = versionTxtResource.getText((uint32)7);
136 
137 		if (fontType.equalsIgnoreCase("ttf")) {
138 			_subtitlesInfo.fontType = Subtitles::kSubtitlesFontTypeTTF;
139 		} else {
140 			_subtitlesInfo.fontType = Subtitles::kSubtitlesFontTypeInternal;
141 		}
142 
143 		if (_subtitlesInfo.fontName.empty()) {
144 			_subtitlesInfo.fontName = SUBTITLES_FONT_FILENAME_EXTERNAL;
145 		}
146 
147 		debug("Subtitles version info: v%s (%s) %s",
148 		       _subtitlesInfo.versionStr.c_str(),
149 		       _subtitlesInfo.dateOfCompile.c_str(),
150 		       _subtitlesInfo.languageMode.c_str());
151 
152 	} else {
153 		debug("Subtitles version info: N/A");
154 	}
155 
156 	//
157 	// Initializing/Loading Subtitles Fonts
158 	if (_subtitlesInfo.fontType == Subtitles::kSubtitlesFontTypeInternal) {
159 		// Use TAHOMA18.FON (is corrupted in places)
160 		// 10PT or TAHOMA24 or KIA6PT  have all caps glyphs (and also are too big or too small) so they are not appropriate.
161 		_font = Font::load(_vm, _subtitlesInfo.fontName, -1, true);
162 		_useUTF8 = false;
163 	} else if (_subtitlesInfo.fontType == Subtitles::kSubtitlesFontTypeTTF) {
164 #if defined(USE_FREETYPE2)
165 		Common::ScopedPtr<Common::SeekableReadStream> stream(_vm->getResourceStream(_subtitlesInfo.fontName));
166 		_font = Graphics::loadTTFFont(*stream, 18);
167 		_useUTF8 = true;
168 #else
169 		warning("Subtitles require a TTF font but this ScummVM build doesn't support it.");
170 		return;
171 #endif
172 	}
173 
174 	if (_font) {
175 		debug("Subtitles font '%s' was loaded successfully.", _subtitlesInfo.fontName.c_str());
176 	} else {
177 		warning("Subtitles font '%s' could not be loaded.", _subtitlesInfo.fontName.c_str());
178 		return;
179 	}
180 	//Done - Initializing/Loading Subtitles Fonts
181 	//
182 
183 	//
184 	// Loading text resources
185 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
186 		_vqaSubsTextResourceEntries[i] = new TextResource(_vm);
187 		Common::String tmpConstructedFileName = "";
188 		bool localizedResource = true;
189 		if (!strcmp(SUBTITLES_FILENAME_PREFIXES[i], "WSTLGO") || !strcmp(SUBTITLES_FILENAME_PREFIXES[i], "BRLOGO")) {
190 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_E"; // Only English versions of these exist
191 			localizedResource = false;
192 		}
193 		else {
194 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_" + _vm->_languageCode;
195 		}
196 
197 		if (_vqaSubsTextResourceEntries[i]->open(tmpConstructedFileName, localizedResource)) {
198 			_gameSubsResourceEntriesFound[i] = true;
199 		}
200 	}
201 	// Done - Loading text resources
202 	//
203 
204 	_isSystemActive = true;
205 }
206 
getSubtitlesInfo() const207 Subtitles::SubtitlesInfo Subtitles::getSubtitlesInfo() const {
208 	return _subtitlesInfo;
209 }
210 
211 /**
212  * Returns the index of the specified Text Resource filename in the SUBTITLES_FILENAME_PREFIXES table
213  */
getIdxForSubsTreName(const Common::String & treName) const214 int Subtitles::getIdxForSubsTreName(const Common::String &treName) const {
215 	Common::String tmpConstructedFileName = "";
216 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
217 		if (!strcmp(SUBTITLES_FILENAME_PREFIXES[i], "WSTLGO") || !strcmp(SUBTITLES_FILENAME_PREFIXES[i], "BRLOGO")) {
218 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_E"; // Only English versions of these exist
219 		} else {
220 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_" + _vm->_languageCode;
221 		}
222 		if (tmpConstructedFileName == treName) {
223 			return i;
224 		}
225 	}
226 	// error case
227 	return -1;
228 }
229 
230 /**
231  * Get the active subtitle text by searching with actor ID and speech ID
232  * Use this method for in-game dialogue - Not dialogue during a VQA cutscene
233  */
loadInGameSubsText(int actorId,int speech_id)234 void Subtitles::loadInGameSubsText(int actorId, int speech_id)  {
235 	if (!_isSystemActive) {
236 		return;
237 	}
238 
239 	int32 id = 10000 * actorId + speech_id;
240 	if (!_gameSubsResourceEntriesFound[0]) {
241 		_currentText.clear();
242 		return;
243 	}
244 
245 	// Search in the first TextResource of the _vqaSubsTextResourceEntries table, which is the TextResource for in-game dialogue (i.e. not VQA dialogue)
246 	const char *text = _vqaSubsTextResourceEntries[0]->getText((uint32)id);
247 	// Use of Common::kWindows1252 codepage to fix bug whereby accented characters
248 	// would not show for subtitles.
249 	// TODO maybe the codepage here should be determined based on some subtitles property per language
250 	//      especially for non-latin languages that still use a FON font rather than a TTF font (eg. Greek would need Common::kWindows1253)
251 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(text) : Common::U32String(text, Common::kWindows1252);
252 }
253 
254 /**
255  * Use this method for dialogue during VQA cutscenes
256  */
loadOuttakeSubsText(const Common::String & outtakesName,int frame)257 void Subtitles::loadOuttakeSubsText(const Common::String &outtakesName, int frame) {
258 	if (!_isSystemActive) {
259 		return;
260 	}
261 
262 	int fileIdx = getIdxForSubsTreName(outtakesName);
263 	if (fileIdx == -1 || !_gameSubsResourceEntriesFound[fileIdx]) {
264 		_currentText.clear();
265 		return;
266 	}
267 
268 	// Search in the requested TextResource at the fileIdx index of the _vqaSubsTextResourceEntries table for a quote that corresponds to the specified video frame
269 	// debug("Number of resource quotes to search: %d, requested frame: %u", _vqaSubsTextResourceEntries[fileIdx]->getCount(), (uint32)frame );
270 	const char *text = _vqaSubsTextResourceEntries[fileIdx]->getOuttakeTextByFrame((uint32)frame);
271 	// Use of Common::kWindows1252 codepage to fix bug whereby accented characters
272 	// would not show for subtitles.
273 	// TODO maybe the codepage here should be determined based on some subtitles property per language
274 	//      especially for non-latin languages that still use a FON font rather than a TTF font (eg. Greek would need Common::kWindows1253)
275 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(text) : Common::U32String(text, Common::kWindows1252);
276 }
277 
278 /**
279  * Explicitly set the active subtitle text to be displayed
280  * Used for debug purposes mainly.
281  */
setGameSubsText(Common::String dbgQuote,bool forceShowWhenNoSpeech)282 void Subtitles::setGameSubsText(Common::String dbgQuote, bool forceShowWhenNoSpeech) {
283 	// TODO is Common::kWindows1252 correct here?
284 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(dbgQuote) : Common::U32String(dbgQuote, Common::kWindows1252);
285 	_forceShowWhenNoSpeech = forceShowWhenNoSpeech; // overrides not showing subtitles when no one is speaking
286 }
287 
288 /**
289  * Sets the _isVisible member var to true if it's not already set
290  * @return true if the member was set now, false if the member was already set
291  */
show()292 bool Subtitles::show() {
293 	if (!_isSystemActive) {
294 		return false;
295 	}
296 
297 	if (_isVisible) {
298 		return false;
299 	}
300 
301 	_isVisible = true;
302 	return true;
303 }
304 
305 /**
306  * Clears the _isVisible member var if not already clear.
307  * @return true if the member was cleared, false if it was already clear.
308  */
hide()309 bool Subtitles::hide() {
310 	if (!_isSystemActive) {
311 		return false;
312 	}
313 
314 	if (!_isVisible) {
315 		return false;
316 	}
317 
318 	_isVisible = false;
319 	return true;
320 }
321 
322 /**
323  * Checks whether the subtitles should be visible or not
324  * @return the value of the _isVisible member boolean var
325  */
isVisible() const326 bool Subtitles::isVisible() const {
327 	return !_isSystemActive || _isVisible;
328 }
329 
330 /**
331  * Tick method specific for outtakes (VQA videos)
332  */
tickOuttakes(Graphics::Surface & s)333 void Subtitles::tickOuttakes(Graphics::Surface &s) {
334 	if (!_isSystemActive || !_vm->isSubtitlesEnabled()) {
335 		return;
336 	}
337 
338 	if (_currentText.empty()) {
339 		_vm->_subtitles->hide();
340 	} else {
341 		_vm->_subtitles->show();
342 	}
343 
344 	if (!_isVisible) { // keep it as a separate if
345 		return;
346 	}
347 
348 	draw(s);
349 }
350 
351 /**
352  * Tick method for in-game subtitles -- Not for outtake cutscenes (VQA videos)
353  */
tick(Graphics::Surface & s)354 void Subtitles::tick(Graphics::Surface &s) {
355 	if (!_isSystemActive || !_vm->isSubtitlesEnabled()) {
356 		return;
357 	}
358 
359 	if (_isVisible && !_forceShowWhenNoSpeech && !_vm->_audioSpeech->isPlaying()) {
360 		_vm->_subtitles->hide(); // TODO might need a better system. Don't call it always.
361 	}
362 
363 	if (!_isVisible) { // keep it as a separate if
364 		return;
365 	}
366 
367 	draw(s);
368 }
369 
370 /**
371  * Draw method for drawing the subtitles on the display surface
372  */
draw(Graphics::Surface & s)373 void Subtitles::draw(Graphics::Surface &s) {
374 	if (!_isSystemActive || !_isVisible || _currentText.empty()) {
375 		return;
376 	}
377 
378 	// This check is done so that lines won't be re-calculated multiple times for the same text
379 	if (_currentText != _prevText) {
380 		lines.clear();
381 		_prevText = _currentText;
382 		_font->wordWrapText(_currentText, kTextMaxWidth, lines, 0, Graphics::kWordWrapEvenWidthLines | Graphics::kWordWrapOnExplicitNewLines);
383 	}
384 
385 	int y = s.h - (kMarginBottom + MAX(kPreferedLine, lines.size()) * _font->getFontHeight());
386 
387 	for (uint i = 0; i < lines.size(); ++i, y += _font->getFontHeight()) {
388 		switch (_subtitlesInfo.fontType) {
389 			case Subtitles::kSubtitlesFontTypeInternal:
390 				// shadow/outline is part of the font color data
391 				_font->drawString(&s, lines[i], 0, y, s.w, 0, Graphics::kTextAlignCenter);
392 				break;
393 			case Subtitles::kSubtitlesFontTypeTTF:
394 				_font->drawString(&s, lines[i], -1, y    , s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
395 				_font->drawString(&s, lines[i],  0, y - 1, s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
396 				_font->drawString(&s, lines[i],  1, y    , s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
397 				_font->drawString(&s, lines[i],  0, y + 1, s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
398 
399 				_font->drawString(&s, lines[i],  0, y    , s.w, s.format.RGBToColor(255, 255, 255), Graphics::kTextAlignCenter);
400 				break;
401 		}
402 	}
403 }
404 
405 /**
406  * Initialize a few basic member vars
407  */
clear()408 void Subtitles::clear() {
409 	_isVisible = false;
410 	_forceShowWhenNoSpeech = false;
411 	_currentText.clear();
412 }
413 
414 /**
415  * Initialize/reset member vars, close open file descriptors and garbage collect subtitle fonts and text resource
416  */
reset()417 void Subtitles::reset() {
418 	clear();
419 
420 	_subtitlesInfo.credits = "N/A";
421 	_subtitlesInfo.versionStr = "N/A";
422 	_subtitlesInfo.dateOfCompile = "N/A";
423 	_subtitlesInfo.languageMode = "N/A";
424 	_subtitlesInfo.fontType = kSubtitlesFontTypeInternal;
425 	_subtitlesInfo.fontName = "N/A";
426 
427 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
428 		if (_vqaSubsTextResourceEntries[i] != nullptr) {
429 			delete _vqaSubsTextResourceEntries[i];
430 			_vqaSubsTextResourceEntries[i] = nullptr;
431 		}
432 		_gameSubsResourceEntriesFound[i] = false;
433 	}
434 
435 	if (_font != nullptr) {
436 		delete _font;
437 		_font = nullptr;
438 	}
439 
440 	_useUTF8 = false;
441 }
442 
443 } // End of namespace BladeRunner
444