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 };
97 
98 /**
99  * Subtitles Constructor
100  */
Subtitles(BladeRunnerEngine * vm)101 Subtitles::Subtitles(BladeRunnerEngine *vm) {
102 	_vm = vm;
103 	_isSystemActive = false;
104 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
105 		_vqaSubsTextResourceEntries[i] = nullptr;
106 		_gameSubsResourceEntriesFound[i] = false;
107 	}
108 	_font = nullptr;
109 	_useUTF8 = false;
110 	reset();
111 }
112 
113 /**
114  * Subtitles Destructor
115  */
~Subtitles()116 Subtitles::~Subtitles() {
117 	reset();
118 }
119 
120 //
121 // Init is kept separated from constructor to allow not loading up resources if subtitles system is disabled
122 //
init(void)123 void Subtitles::init(void) {
124 	// Loading subtitles versioning info if available
125 	TextResource versionTxtResource(_vm);
126 	if ( versionTxtResource.open(SUBTITLES_VERSION_TRENAME, false)) {
127 		_subtitlesInfo.credits = versionTxtResource.getText((uint32)0);
128 		_subtitlesInfo.versionStr = versionTxtResource.getText((uint32)1);
129 		_subtitlesInfo.dateOfCompile = versionTxtResource.getText((uint32)2);
130 		_subtitlesInfo.languageMode = versionTxtResource.getText((uint32)3);
131 		Common::String fontType = versionTxtResource.getText((uint32)4);
132 		_subtitlesInfo.fontName = versionTxtResource.getText((uint32)5);
133 
134 		if (fontType.equalsIgnoreCase("ttf")) {
135 			_subtitlesInfo.fontType = Subtitles::kSubtitlesFontTypeTTF;
136 		} else {
137 			_subtitlesInfo.fontType = Subtitles::kSubtitlesFontTypeInternal;
138 		}
139 
140 		if (_subtitlesInfo.fontName.empty()) {
141 			_subtitlesInfo.fontName = SUBTITLES_FONT_FILENAME_EXTERNAL;
142 		}
143 
144 		debug("Subtitles version info: v%s (%s) %s by: %s",
145 		       _subtitlesInfo.versionStr.c_str(),
146 		       _subtitlesInfo.dateOfCompile.c_str(),
147 		       _subtitlesInfo.languageMode.c_str(),
148 		       _subtitlesInfo.credits.c_str());
149 
150 	} else {
151 		debug("Subtitles version info: N/A");
152 	}
153 
154 	//
155 	// Initializing/Loading Subtitles Fonts
156 	if (_subtitlesInfo.fontType == Subtitles::kSubtitlesFontTypeInternal) {
157 		// Use TAHOMA18.FON (is corrupted in places)
158 		// 10PT or TAHOMA24 or KIA6PT  have all caps glyphs (and also are too big or too small) so they are not appropriate.
159 		_font = Font::load(_vm, _subtitlesInfo.fontName, -1, true);
160 		_useUTF8 = false;
161 	} else if (_subtitlesInfo.fontType == Subtitles::kSubtitlesFontTypeTTF) {
162 #if defined(USE_FREETYPE2)
163 		Common::ScopedPtr<Common::SeekableReadStream> stream(_vm->getResourceStream(_subtitlesInfo.fontName));
164 		_font = Graphics::loadTTFFont(*stream, 18);
165 		_useUTF8 = true;
166 #else
167 		warning("Subtitles require a TTF font but this ScummVM build doesn't support it.");
168 		return;
169 #endif
170 	}
171 
172 	if (_font) {
173 		debug("Subtitles font '%s' was loaded successfully.", _subtitlesInfo.fontName.c_str());
174 	} else {
175 		warning("Subtitles font '%s' could not be loaded.", _subtitlesInfo.fontName.c_str());
176 		return;
177 	}
178 	//Done - Initializing/Loading Subtitles Fonts
179 	//
180 
181 	//
182 	// Loading text resources
183 	for (int i = 0; i < kMaxTextResourceEntries; i++) {
184 		_vqaSubsTextResourceEntries[i] = new TextResource(_vm);
185 		Common::String tmpConstructedFileName = "";
186 		bool localizedResource = true;
187 		if (!strcmp(SUBTITLES_FILENAME_PREFIXES[i], "WSTLGO") || !strcmp(SUBTITLES_FILENAME_PREFIXES[i], "BRLOGO")) {
188 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_E"; // Only English versions of these exist
189 			localizedResource = false;
190 		}
191 		else {
192 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_" + _vm->_languageCode;
193 		}
194 
195 		if (_vqaSubsTextResourceEntries[i]->open(tmpConstructedFileName, localizedResource)) {
196 			_gameSubsResourceEntriesFound[i] = true;
197 		}
198 	}
199 	// Done - Loading text resources
200 	//
201 
202 	_isSystemActive = true;
203 }
204 
getSubtitlesInfo() const205 Subtitles::SubtitlesInfo Subtitles::getSubtitlesInfo() const {
206 	return _subtitlesInfo;
207 }
208 
209 /**
210  * Returns the index of the specified Text Resource filename in the SUBTITLES_FILENAME_PREFIXES table
211  */
getIdxForSubsTreName(const Common::String & treName) const212 int Subtitles::getIdxForSubsTreName(const Common::String &treName) const {
213 	Common::String tmpConstructedFileName = "";
214 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
215 		if (!strcmp(SUBTITLES_FILENAME_PREFIXES[i], "WSTLGO") || !strcmp(SUBTITLES_FILENAME_PREFIXES[i], "BRLOGO")) {
216 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_E"; // Only English versions of these exist
217 		} else {
218 			tmpConstructedFileName = Common::String(SUBTITLES_FILENAME_PREFIXES[i]) + "_" + _vm->_languageCode;
219 		}
220 		if (tmpConstructedFileName == treName) {
221 			return i;
222 		}
223 	}
224 	// error case
225 	return -1;
226 }
227 
228 /**
229  * Get the active subtitle text by searching with actor ID and speech ID
230  * Use this method for in-game dialogue - Not dialogue during a VQA cutscene
231  */
loadInGameSubsText(int actorId,int speech_id)232 void Subtitles::loadInGameSubsText(int actorId, int speech_id)  {
233 	if (!_isSystemActive) {
234 		return;
235 	}
236 
237 	int32 id = 10000 * actorId + speech_id;
238 	if (!_gameSubsResourceEntriesFound[0]) {
239 		_currentText.clear();
240 		return;
241 	}
242 
243 	// Search in the first TextResource of the _vqaSubsTextResourceEntries table, which is the TextResource for in-game dialogue (i.e. not VQA dialogue)
244 	const char *text = _vqaSubsTextResourceEntries[0]->getText((uint32)id);
245 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(text) : Common::U32String(text);
246 }
247 
248 /**
249  * Use this method for dialogue during VQA cutscenes
250  */
loadOuttakeSubsText(const Common::String & outtakesName,int frame)251 void Subtitles::loadOuttakeSubsText(const Common::String &outtakesName, int frame) {
252 	if (!_isSystemActive) {
253 		return;
254 	}
255 
256 	int fileIdx = getIdxForSubsTreName(outtakesName);
257 	if (fileIdx == -1 || !_gameSubsResourceEntriesFound[fileIdx]) {
258 		_currentText.clear();
259 		return;
260 	}
261 
262 	// Search in the requested TextResource at the fileIdx index of the _vqaSubsTextResourceEntries table for a quote that corresponds to the specified video frame
263 	// debug("Number of resource quotes to search: %d, requested frame: %u", _vqaSubsTextResourceEntries[fileIdx]->getCount(), (uint32)frame );
264 	const char *text = _vqaSubsTextResourceEntries[fileIdx]->getOuttakeTextByFrame((uint32)frame);
265 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(text) : Common::U32String(text);
266 }
267 
268 /**
269  * Explicitly set the active subtitle text to be displayed
270  * Used for debug purposes mainly.
271  */
setGameSubsText(Common::String dbgQuote,bool forceShowWhenNoSpeech)272 void Subtitles::setGameSubsText(Common::String dbgQuote, bool forceShowWhenNoSpeech) {
273 	_currentText = _useUTF8 ? Common::convertUtf8ToUtf32(dbgQuote) : dbgQuote;
274 	_forceShowWhenNoSpeech = forceShowWhenNoSpeech; // overrides not showing subtitles when no one is speaking
275 }
276 
277 /**
278  * Sets the _isVisible member var to true if it's not already set
279  * @return true if the member was set now, false if the member was already set
280  */
show()281 bool Subtitles::show() {
282 	if (!_isSystemActive) {
283 		return false;
284 	}
285 
286 	if (_isVisible) {
287 		return false;
288 	}
289 
290 	_isVisible = true;
291 	return true;
292 }
293 
294 /**
295  * Clears the _isVisible member var if not already clear.
296  * @return true if the member was cleared, false if it was already clear.
297  */
hide()298 bool Subtitles::hide() {
299 	if (!_isSystemActive) {
300 		return false;
301 	}
302 
303 	if (!_isVisible) {
304 		return false;
305 	}
306 
307 	_isVisible = false;
308 	return true;
309 }
310 
311 /**
312  * Checks whether the subtitles should be visible or not
313  * @return the value of the _isVisible member boolean var
314  */
isVisible() const315 bool Subtitles::isVisible() const {
316 	return !_isSystemActive || _isVisible;
317 }
318 
319 /**
320  * Tick method specific for outtakes (VQA videos)
321  */
tickOuttakes(Graphics::Surface & s)322 void Subtitles::tickOuttakes(Graphics::Surface &s) {
323 	if (!_isSystemActive || !_vm->isSubtitlesEnabled()) {
324 		return;
325 	}
326 
327 	if (_currentText.empty()) {
328 		_vm->_subtitles->hide();
329 	} else {
330 		_vm->_subtitles->show();
331 	}
332 
333 	if (!_isVisible) { // keep it as a separate if
334 		return;
335 	}
336 
337 	draw(s);
338 }
339 
340 /**
341  * Tick method for in-game subtitles -- Not for outtake cutscenes (VQA videos)
342  */
tick(Graphics::Surface & s)343 void Subtitles::tick(Graphics::Surface &s) {
344 	if (!_isSystemActive || !_vm->isSubtitlesEnabled()) {
345 		return;
346 	}
347 
348 	if (_isVisible && !_forceShowWhenNoSpeech && !_vm->_audioSpeech->isPlaying()) {
349 		_vm->_subtitles->hide(); // TODO might need a better system. Don't call it always.
350 	}
351 
352 	if (!_isVisible) { // keep it as a separate if
353 		return;
354 	}
355 
356 	draw(s);
357 }
358 
359 /**
360  * Draw method for drawing the subtitles on the display surface
361  */
draw(Graphics::Surface & s)362 void Subtitles::draw(Graphics::Surface &s) {
363 	if (!_isSystemActive || !_isVisible || _currentText.empty()) {
364 		return;
365 	}
366 
367 	// This check is done so that lines won't be re-calculated multiple times for the same text
368 	if (_currentText != _prevText) {
369 		lines.clear();
370 		_prevText = _currentText;
371 		_font->wordWrapText(_currentText, kTextMaxWidth, lines, 0, true, true);
372 	}
373 
374 	int y = s.h - (kMarginBottom + MAX(kPreferedLine, lines.size()) * _font->getFontHeight());
375 
376 	for (uint i = 0; i < lines.size(); i++, y += _font->getFontHeight()) {
377 		switch (_subtitlesInfo.fontType) {
378 			case Subtitles::kSubtitlesFontTypeInternal:
379 				// shadow/outline is part of the font color data
380 				_font->drawString(&s, lines[i], 0, y, s.w, 0, Graphics::kTextAlignCenter);
381 				break;
382 			case Subtitles::kSubtitlesFontTypeTTF:
383 				_font->drawString(&s, lines[i], -1, y    , s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
384 				_font->drawString(&s, lines[i],  0, y - 1, s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
385 				_font->drawString(&s, lines[i],  1, y    , s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
386 				_font->drawString(&s, lines[i],  0, y + 1, s.w, s.format.RGBToColor(  0,   0,   0), Graphics::kTextAlignCenter);
387 
388 				_font->drawString(&s, lines[i],  0, y    , s.w, s.format.RGBToColor(255, 255, 255), Graphics::kTextAlignCenter);
389 				break;
390 		}
391 	}
392 }
393 
394 /**
395  * Initialize a few basic member vars
396  */
clear()397 void Subtitles::clear() {
398 	_isVisible = false;
399 	_forceShowWhenNoSpeech = false;
400 	_currentText.clear();
401 }
402 
403 /**
404  * Initialize/reset member vars, close open file descriptors and garbage collect subtitle fonts and text resource
405  */
reset()406 void Subtitles::reset() {
407 	clear();
408 
409 	_subtitlesInfo.credits = "N/A";
410 	_subtitlesInfo.versionStr = "N/A";
411 	_subtitlesInfo.dateOfCompile = "N/A";
412 	_subtitlesInfo.languageMode = "N/A";
413 	_subtitlesInfo.fontType = kSubtitlesFontTypeInternal;
414 	_subtitlesInfo.fontName = "N/A";
415 
416 	for (int i = 0; i < kMaxTextResourceEntries; ++i) {
417 		if (_vqaSubsTextResourceEntries[i] != nullptr) {
418 			delete _vqaSubsTextResourceEntries[i];
419 			_vqaSubsTextResourceEntries[i] = nullptr;
420 		}
421 		_gameSubsResourceEntriesFound[i] = false;
422 	}
423 
424 	if (_font != nullptr) {
425 		delete _font;
426 		_font = nullptr;
427 	}
428 
429 	_useUTF8 = false;
430 }
431 
432 } // End of namespace BladeRunner
433