/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "twine/text.h" #include "common/algorithm.h" #include "common/config-manager.h" #include "common/memstream.h" #include "common/scummsys.h" #include "common/str.h" #include "common/system.h" #include "common/text-to-speech.h" #include "common/util.h" #include "twine/audio/sound.h" #include "twine/input.h" #include "twine/menu/interface.h" #include "twine/menu/menu.h" #include "twine/parser/text.h" #include "twine/renderer/renderer.h" #include "twine/renderer/screens.h" #include "twine/resources/hqr.h" #include "twine/resources/resources.h" #include "twine/scene/gamestate.h" #include "twine/scene/scene.h" #include "twine/shared.h" #include "twine/twine.h" namespace TwinE { /** FLA movie extension */ #define VOX_EXT ".vox" static const int32 PADDING = 8; static const int kLBASJISCharWidth = 24; static const int kLBASJISCharHeight = 24; Text::Text(TwinEEngine *engine) : _engine(engine) { Common::fill(&_currMenuTextBuffer[0], &_currMenuTextBuffer[256], 0); _isShiftJIS = _engine->getGameLang() == Common::Language::JA_JPN; _isVisualRTL = _engine->getGameLang() == Common::Language::HE_ISR; } Text::~Text() { } void Text::initVoxBank(TextBankId bankIdx) { static const char *ListFileText[] = { "sys", "cre", "gam", // global game voices (e.g. inventory descriptions) "000", // Citadel Island voices "001", // Principal Island voices "002", // White Leaf Desert voices "003", // Proxima Island voices "004", // Rebellion Island voices "005", // Hamalayi Mountains - southern range voices "006", // Hamalayi Mountains - northern range voices "007", // Tippett Island voices "008", // Brundle Island voices "009", // Fortress Island voices "010", // Polar Island voices "011" // }; if ((int)bankIdx < 0 || (int)bankIdx >= ARRAYSIZE(ListFileText)) { error("bankIdx is out of bounds: %i", (int)bankIdx); } // get the correct vox hqr file - english is the default _currentVoxBankFile = Common::String::format("%s%s" VOX_EXT, ListLanguage[0].id, ListFileText[(int)bankIdx]); _currentOggBaseFile = Common::String::format("%s%s_", ListLanguage[0].id, ListFileText[(int)bankIdx]); const int voice = ConfMan.getInt("audio_language"); const int32 length = ARRAYSIZE(ListLanguage); for (int32 i = 0; i < length; i++) { if (ListLanguage[i].voice == voice) { _currentVoxBankFile = Common::String::format("%s%s" VOX_EXT, ListLanguage[i].id, ListFileText[(int)bankIdx]); _currentOggBaseFile = Common::String::format("%s%s_", ListLanguage[i].id, ListFileText[(int)bankIdx]); return; } } warning("Could not find voice mapping for %i", voice); // TODO check the rest to reverse } bool Text::initVoxToPlayTextId(TextId textId) { const TextEntry *text = _engine->_resources->getText(_currentBankIdx, textId); return initVoxToPlay(text); } bool Text::initVoxToPlay(const TextEntry *text) { _currDialTextEntry = text; _voxHiddenIndex = 0; _hasHiddenVox = false; if (text == nullptr) { return false; } const int voice = ConfMan.getInt("audio_language"); if (voice <= 0) { debug(3, "Voices are disabled"); return false; } return _engine->_sound->playVoxSample(_currDialTextEntry); } bool Text::playVox(const TextEntry *text) { if (ConfMan.getInt("audio_language") <= 0) { return false; } if (text == nullptr) { return false; } if (_hasHiddenVox && !_engine->_sound->isSamplePlaying(text->index)) { _engine->_sound->playVoxSample(text); return true; } return false; } bool Text::playVoxSimple(const TextEntry *text) { if (text == nullptr) { return false; } if (_engine->_sound->isSamplePlaying(text->index)) { return true; } return playVox(text); } bool Text::stopVox(const TextEntry *text) { Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager(); if (ttsMan != nullptr) { ttsMan->stop(); } if (text == nullptr) { return false; } if (!_engine->_sound->isSamplePlaying(text->index)) { return false; } _hasHiddenVox = false; _engine->_sound->stopSample(text->index); return true; } void Text::initSceneTextBank() { const int textBankId = (int)_engine->_scene->_sceneTextBank; initDial((TextBankId)(textBankId + (int)TextBankId::Citadel_Island)); } void Text::drawCharacter(int32 x, int32 y, uint16 character) { // CarFont const uint8 usedColor = _dialTextColor; if (_isShiftJIS && character > 0x100 && _engine->_resources->_sjisFontPtr) { int index = 0; if (character >= 0x8140 && character <= 0x9fff) index = character - 0x8140; else if (character >= 0xe040 && character <= 0xeaff) index = character - 0xe040 + 8320; else { drawCharacter(x, y, '?'); return; } byte *glyphPtr = _engine->_resources->_sjisFontPtr + index * (kLBASJISCharHeight * kLBASJISCharWidth / 8); for (uint8 fontY = 0; fontY < kLBASJISCharHeight; ++fontY) { byte bits = 0; int remBits = 0; for (uint8 fontX = 0; fontX < kLBASJISCharWidth; ++fontX) { if (remBits == 0) { remBits = 8; bits = *glyphPtr++; } int32 tempX = x + fontX; int32 tempY = y + fontY; if ((bits & 0x80) && tempX >= 0 && tempX < (_engine->width() - 1) && tempY >= 0 && tempY < (_engine->height() - 1)) { _engine->_frontVideoBuffer.setPixel(tempX, tempY, usedColor); } remBits--; bits <<= 1; } } return; } if (character > 0x100) character = '?'; Common::MemoryReadStream stream(_engine->_resources->_fontPtr, _engine->_resources->_fontBufSize); stream.seek(character * 4); stream.seek(stream.readSint16LE()); /*uint8 charWidth =*/stream.readByte(); const uint8 sizeY = stream.readByte(); x += stream.readByte(); y += stream.readByte(); int32 tempX = x; int32 tempY = y; for (uint8 fontY = 0; fontY < sizeY; ++fontY) { uint8 index = stream.readByte(); do { const uint8 jump = stream.readByte(); if (--index == 0) { tempY++; tempX = x; break; } const uint8 number = stream.readByte(); tempX += jump; uint8 *basePtr = (uint8 *)_engine->_frontVideoBuffer.getBasePtr(tempX, tempY); for (uint8 i = 0; i < number; i++) { if (tempX >= 0 && tempX < (_engine->width() - 1) && tempY >= 0 && tempY < (_engine->height() - 1)) { *basePtr = usedColor; } ++basePtr; tempX++; } if (--index == 0) { tempY++; tempX = x; break; } } while (1); } } uint16 Text::getNextChar(const char *&dialogue) { uint16 currChar = *dialogue++ & 0xff; if (_isShiftJIS && ((currChar >= 0x81 && currChar <= 0x9f) || (currChar >= 0xe0 && currChar <= 0xea)) && ((*dialogue & 0xff) >= 0x40)) currChar = (currChar << 8) | (*dialogue++ & 0xff); return currChar; } void Text::drawText(int32 x, int32 y, const char *dialogue, bool shadow) { // if the font is not defined if (_engine->_resources->_fontPtr == nullptr) { return; } do { const uint16 currChar = getNextChar(dialogue); // read the next char from the string if (currChar == '\0') { break; } if (currChar == ' ') { x += _interSpace; } else { const int32 dialTextSize = getCharWidth(currChar); if (shadow) { Common::Rect dirtyRect; drawCharacterShadow(x, y, currChar, COLOR_WHITE, dirtyRect); } else { drawCharacter(x, y, currChar); // draw the character on screen } // add the length of the space between 2 characters x += _interLeave; // add the length of the current character x += dialTextSize; } } while (1); } int32 Text::sizeFont(const char *dialogue) { int32 dialTextSize = 0; do { const uint16 currChar = getNextChar(dialogue); if (currChar == '\0') { break; } if (currChar == ' ') { dialTextSize += _interSpace; } else { dialTextSize += _interLeave; dialTextSize += getCharWidth(currChar); } } while (1); return dialTextSize; } void Text::initInventoryText(InventoryItems index) { // OpenDialNoWindow // 100 if the offset for the inventory item descriptions commonOpenDial((TextId)(100 + (int)index)); } void Text::initItemFoundText(InventoryItems index) { // OpenDialNoWindow commonOpenDial((TextId)index); } void Text::initDial(TextBankId bankIdx) { // don't load if we already have the dialogue text bank loaded if (bankIdx == _currentBankIdx) { return; } _currentBankIdx = bankIdx; initVoxBank(bankIdx); } Text::WordSize Text::getNextWord(const char *completeText, char *dst, int32 wordBufSize) { int32 lenword = 0; const char *word = dst; while (*completeText != '\0' && *completeText != '\1' && *completeText != ' ') { lenword++; *dst++ = *completeText++; if (lenword >= wordBufSize - 1) { break; } } *dst = '\0'; WordSize size; size.lenWord = lenword; size.sizeWord = sizeFont(word); return size; } void Text::pushChar(int16 x, int16 y, const LineCharacter &chr) { if (_nbChar < TEXT_MAX_FADE_IN_CHR) { _stackChar[_nbChar].chr = chr.chr; _stackChar[_nbChar].x = x; _stackChar[_nbChar].y = y; _nbChar++; return; } for (int32 i = 0; i < TEXT_MAX_FADE_IN_CHR - 1; ++i) { _stackChar[i] = _stackChar[i + 1]; } _stackChar[TEXT_MAX_FADE_IN_CHR - 1].chr = chr.chr; _stackChar[TEXT_MAX_FADE_IN_CHR - 1].x = x; _stackChar[TEXT_MAX_FADE_IN_CHR - 1].y = y; } void Text::drawCharacterShadow(int32 x, int32 y, uint16 character, int32 color, Common::Rect &dirtyRect) { // AffOneCar if (character == ' ') { return; } // shadow color setFontColor(COLOR_BLACK); drawCharacter(x + 2, y + 4, character); // text color setFontColor(color); drawCharacter(x, y, character); // TODO: get font size const Common::Rect rect(x, y, x + 32, y + 38); if (dirtyRect.isEmpty()) { dirtyRect = rect; } else { dirtyRect.extend(rect); } } void Text::fadeInCharacters(int32 counter, int32 fontColor) { // AffAllCar Common::Rect dirtyRect; while (--counter >= 0) { const BlendInCharacter *ptr = &_stackChar[counter]; setFontColor(fontColor); drawCharacterShadow(ptr->x, ptr->y, ptr->chr, fontColor, dirtyRect); fontColor -= _stepDegrade; if (fontColor > _minDegrade) { fontColor = _minDegrade; } } if (dirtyRect.isEmpty()) { return; } _engine->copyBlockPhys(dirtyRect); } void Text::setTextCrossColor(int32 stopColor, int32 startColor, int32 stepSize) { // CoulDial _maxDegrade = startColor; _minDegrade = stopColor; _stepDegrade = stepSize; _nbDegrade = ((startColor - stopColor) + 1) / stepSize; } void Text::setFontCrossColor(int32 color) { // TestCoulDial _stepDegrade = -1; _nbDegrade = 14; _maxDegrade = color * 16; _minDegrade = _maxDegrade + 12; } bool Text::getText(TextId index) { const TextEntry *textEntry = _engine->_resources->getText(_currentBankIdx, index); if (textEntry == nullptr) { return false; } _ptText = textEntry->string.c_str(); _currDialTextSize = textEntry->string.size(); // RECHECK: this was added for vox playback _currDialTextEntry = textEntry; debug(3, "text for bank %i with index %i (currIndex: %i): %s", (int)_currentBankIdx, textEntry->index, (int)textEntry->textIndex, _ptText); return true; } void Text::renderContinueReadingTriangle() { // AffFleche const int32 border = 3; const int32 size = 21; const int16 color = 136; const int32 right = _dialTextBox.right - border; const int32 left = _dialTextBox.right - (size + border); const int32 top = _dialTextBox.bottom - (size + border); const int32 bottom = _dialTextBox.bottom - border; ComputedVertex vertices[3]; vertices[0].intensity = color; vertices[0].x = right; vertices[0].y = top; vertices[1].intensity = color; vertices[1].x = left; vertices[1].y = bottom; vertices[2].intensity = color; vertices[2].x = right; vertices[2].y = bottom; CmdRenderPolygon polygon; polygon.numVertices = ARRAYSIZE(vertices); polygon.colorIndex = _minDegrade; polygon.renderType = POLYGONTYPE_FLAT; _engine->_renderer->renderPolygons(polygon, vertices); _engine->copyBlockPhys(left, top, right, bottom); } void Text::initDialWindow() { _engine->blitWorkToFront(_dialTextBox); if (_flagMessageShade) { _engine->_menu->drawRectBorders(_dialTextBox); Common::Rect rect(_dialTextBox); rect.grow(-1); _engine->_interface->shadeBox(rect, 3); } _nbChar = 0; _engine->copyBlock(_dialTextBox); } void Text::secondInitDialWindow() { _engine->blitWorkToFront(_dialTextBox); _nbChar = 0; } void Text::normalWinDial() { const int32 margin = 16; _maxLineDial = 3; //(int32)(_dialTextBox.height() / lineHeight) - 1; const int32 textHeight = _maxLineDial * lineHeight; _dialTextBox.left = margin; _dialTextBox.top = _engine->height() - textHeight - margin - PADDING; _dialTextBox.right = _engine->width() - 1 - margin; _dialTextBox.bottom = _engine->height() - 1 - margin; _dialMaxSize = _dialTextBox.width() - (2 * PADDING); } void Text::bigWinDial() { const int32 margin = 8; _dialTextBox.left = margin; _dialTextBox.top = margin; _dialTextBox.right = _engine->width() - 1 - margin; _dialTextBox.bottom = _engine->height() - 1 - margin; _maxLineDial = (int32)(_dialTextBox.height() / lineHeight) - 1; _dialMaxSize = _dialTextBox.width() - (2 * PADDING); } void Text::commonOpenDial(TextId index) { closeDial(); if (!getText(index)) { return; } _xDial = _dialTextBox.left + PADDING; _yDial = _dialTextBox.top + PADDING; _nbLineDial = 0; _ptDial = _ptText; // _word = '\0'; // _ptWord = _word; _bufLine[0].chr = '\0'; _bufLine[0].width = 0; _ptLine = _bufLine; _nbChar = 0; _flagEndDial = false; _flagEnd3Line = false; _flagRunningDial = true; // lba font is loaded during engine start setFont(INTER_LEAVE, INTER_SPACE); } void Text::getNextLine() { _sizeLine = 0; _nbSpace = 0; _sizeSpace = INTER_SPACE; _bufLine[0].chr = '\0'; _bufLine[0].width = 0; bool flag = true; uint i = 0; while (true) { if (*_ptDial == ' ') { _ptDial++; continue; } if (*_ptDial == '\0') { break; } char word[256] = ""; WordSize sizeWord = getNextWord(_ptDial, word, sizeof(word)); if (_sizeLine + _sizeSpace + sizeWord.sizeWord > _dialMaxSize) { break; } if (*word == '\1') { _ptDial++; flag = false; break; } // @ is a line break if (*word == '@') { flag = false; _ptDial++; if (_sizeLine == 0) { _bufLine[0].chr = '\0'; _bufLine[0].width = 0; _sizeLine = INTER_SPACE; } // new page? if (word[1] == 'P') { _nbLineDial = _maxLineDial; _ptDial++; } break; } _ptDial += sizeWord.lenWord; appendText(word, i); appendText(" ", i); _sizeLine += sizeWord.sizeWord + _sizeSpace; _nbSpace++; if (*_ptDial == '\0') { break; } _ptDial++; } _bufLine[i].chr = 0; _bufLine[i].width = 0; if (_isVisualRTL) { for (uint j = 0; j < i / 2; j++) { LineCharacter t = _bufLine[j]; _bufLine[j] = _bufLine[i - j - 1]; _bufLine[i - j - 1] = t; } } if (_nbSpace) { _nbSpace--; } if (*_ptDial != '\0' && flag) { if (_nbSpace == 0) { _nbSpace = 1; } int32 dx = _dialMaxSize - _sizeLine; int32 dd = dx / _nbSpace; _sizeSpace += dd; dd = _nbSpace * dd; _nbBigSpace = dx - dd; } _ptLine = _bufLine; } void Text::initEndPage() { for (uint i = 0; i < ARRAYSIZE(_bufLine); i++) { _bufLine[i].chr = ' '; _bufLine[i].width = _sizeSpace; } // the end of the buffer defines how fast the next page is shown - as the // whitespaces are handled in the fade in process, too. But we need at least 32 chars, // to completly fade in the last characters of a full page (see TEXT_MAX_FADE_IN_CHR) _bufLine[ARRAYSIZE(_bufLine) - 1].chr = '\0'; _bufLine[ARRAYSIZE(_bufLine) - 1].width = 0; _ptLine = _bufLine; _nbLineDial = 0; _sizeLine = 16; // hack } ProgressiveTextState Text::nextDialChar() { // NextDialCar if (!_flagRunningDial) { return ProgressiveTextState::End; } if (_ptLine->chr == '\0') { if (_flagEndDial) { _flagRunningDial = false; return ProgressiveTextState::End; } if (_flagEnd3Line) { secondInitDialWindow(); _flagEnd3Line = false; _xDial = _dialTextBox.left + PADDING; _yDial = _dialTextBox.top + PADDING; } if (*_ptDial == '\0') { initEndPage(); _flagEndDial = true; return ProgressiveTextState::ContinueRunning; } getNextLine(); } if (_ptLine->chr != '\0') { pushChar(_xDial, _yDial, *_ptLine); fadeInCharacters(_nbChar, _maxDegrade); if (_ptLine->chr != ' ') _xDial += _ptLine->width + INTER_LEAVE; else { if (_nbBigSpace) { _nbBigSpace--; _xDial++; } _xDial += _sizeSpace; } ++_ptLine; if (_ptLine->chr == '\0') { _xDial = _dialTextBox.left + PADDING; _yDial += lineHeight; if (_flagEnd3Line && !_flagEndDial) { renderContinueReadingTriangle(); return ProgressiveTextState::NextPage; } _nbLineDial++; if (_nbLineDial >= _maxLineDial) { initEndPage(); _flagEnd3Line = true; if (*_ptDial == '\0') { _flagEndDial = true; } } } } return ProgressiveTextState::ContinueRunning; } void Text::closeDial() { _flagRunningDial = false; } void Text::appendText(const char *s, uint &i) { while (1) { if (i >= ARRAYSIZE(_bufLine) - 1) return; uint16 chr = getNextChar(s); if (!chr) return; _bufLine[i].chr = chr; _bufLine[i].width = getCharWidth(chr); i++; } } int32 Text::getCharWidth(uint16 chr) const { if (_isShiftJIS && (chr > 0x100)) return kLBASJISCharWidth; if (chr > 0x100) chr = '?'; Common::MemoryReadStream stream(_engine->_resources->_fontPtr, _engine->_resources->_fontBufSize); stream.seek(chr * 4); stream.seek(stream.readSint16LE()); return stream.readByte(); } int32 Text::getCharHeight(uint16 chr) const { if (_isShiftJIS && (chr > 0x100)) return kLBASJISCharHeight; if (chr > 0x100) chr = '?'; Common::MemoryReadStream stream(_engine->_resources->_fontPtr, _engine->_resources->_fontBufSize); stream.seek(chr * 4); stream.seek(stream.readSint16LE() + 1); return stream.readByte(); } void Text::fadeInRemainingChars() { if (_nbChar <= 0) { return; } fadeInCharacters(_nbChar, _minDegrade); --_nbChar; } bool Text::displayText(TextId index, bool showText, bool playVox, bool loop) { debug(3, "displayText(index = %i, showText = %s, playVox = %s)", (int)index, showText ? "true" : "false", playVox ? "true" : "false"); if (playVox) { const TextEntry *textEntry = _engine->_resources->getText(_currentBankIdx, index); // get right VOX entry index initVoxToPlay(textEntry); } bool aborted = false; // if we don't display text, than still plays vox file if (showText) { commonOpenDial(index); initDialWindow(); ScopedKeyMap uiKeyMap(_engine, uiKeyMapId); ProgressiveTextState textState = ProgressiveTextState::ContinueRunning; for (;;) { FrameMarker frame(_engine, 66); _engine->readKeys(); if (textState == ProgressiveTextState::ContinueRunning) { textState = nextDialChar(); } else { fadeInRemainingChars(); } if (!loop) { if (textState == ProgressiveTextState::End) { fadeInRemainingChars(); break; } if (textState == ProgressiveTextState::NextPage) { textState = ProgressiveTextState::ContinueRunning; } } if (_engine->_input->toggleActionIfActive(TwinEActionType::UINextPage)) { if (textState == ProgressiveTextState::End) { stopVox(_currDialTextEntry); break; } if (textState == ProgressiveTextState::NextPage) { textState = ProgressiveTextState::ContinueRunning; } } if (_engine->_input->toggleAbortAction() || _engine->shouldQuit()) { stopVox(_currDialTextEntry); aborted = true; break; } if (playVox) { playVoxSimple(_currDialTextEntry); } } } // Wait until silence while (playVox && playVoxSimple(_currDialTextEntry)) { FrameMarker frame(_engine); _engine->readKeys(); if (_engine->shouldQuit() || _engine->_input->toggleAbortAction()) { stopVox(_currDialTextEntry); aborted = true; break; } } _voxHiddenIndex = 0; _hasHiddenVox = false; _flagRunningDial = false; _engine->_input->resetHeroActions(); return aborted; } bool Text::drawTextProgressive(TextId index, bool playVox, bool loop) { // Dial _engine->_interface->memoClip(); _engine->_interface->unsetClip(); _engine->saveFrontBuffer(); const bool aborted = displayText(index, _engine->_cfgfile.FlagDisplayText, playVox, loop); _engine->_interface->restoreClip(); return aborted; } void Text::setFont(int32 interLeave, int32 interSpace) { _interLeave = interLeave; _interSpace = interSpace; } void Text::setFontColor(int32 color) { // CoulFont _dialTextColor = color; } bool Text::getMenuText(TextId index, char *text, uint32 textSize) { if (index == _currMenuTextIndex) { if (_currMenuTextBank == _engine->_scene->_sceneTextBank) { Common::strlcpy(text, _currMenuTextBuffer, textSize); return true; } } if (!getText(index)) { // if doesn't have text text[0] = '\0'; return false; } if (_currDialTextSize - 1 > 0xFF) { _currDialTextSize = 0xFF; } Common::strlcpy(text, _ptText, MIN(textSize, _currDialTextSize + 1)); _currDialTextSize++; Common::strlcpy(_currMenuTextBuffer, text, MIN(sizeof(_currMenuTextBuffer), _currDialTextSize)); _currMenuTextIndex = index; _currMenuTextBank = _engine->_scene->_sceneTextBank; return true; } void Text::drawAskQuestion(TextId index) { displayText(index, true, true, true); } void Text::drawHolomapLocation(TextId index) { normalWinDial(); setFontCrossColor(COLOR_WHITE); _engine->_interface->box(_dialTextBox, COLOR_BLACK); const bool displayText = _engine->_cfgfile.FlagDisplayText; _engine->_cfgfile.FlagDisplayText = true; drawTextProgressive(index, false, false); _engine->_cfgfile.FlagDisplayText = displayText; } } // namespace TwinE