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