Files
scummvm-cursorfix/engines/zvision/text/subtitle_manager.cpp
2026-02-02 04:50:13 +01:00

431 lines
14 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "common/config-manager.h"
#include "common/file.h"
#include "common/system.h"
#include "zvision/detection.h"
#include "zvision/graphics/render_manager.h"
#include "zvision/scripting/script_manager.h"
#include "zvision/text/subtitle_manager.h"
#include "zvision/text/text.h"
namespace ZVision {
SubtitleManager::SubtitleManager(ZVision *engine, const ScreenLayout layout, const Graphics::PixelFormat pixelFormat, bool doubleFPS) :
_engine(engine),
_system(engine->_system),
_renderManager(engine->getRenderManager()),
_pixelFormat(pixelFormat),
_textOffset(layout.workingArea.origin() - layout.textArea.origin()),
_textArea(layout.textArea.width(), layout.textArea.height()),
_redraw(false),
_doubleFPS(doubleFPS),
_subId(0) {
}
SubtitleManager::~SubtitleManager() {
// Delete all subtitles referenced in subslist
for (auto &it : _subsList)
delete it._value;
}
void SubtitleManager::process(int32 deltatime) {
for (SubtitleMap::iterator it = _subsList.begin(); it != _subsList.end(); it++) {
// Update all automatic subtitles
if (it->_value->selfUpdate())
_redraw = true;
// Update all subtitles' respective deletion timers
if (it->_value->process(deltatime)) {
debugC(4, kDebugSubtitle, "Deleting subtitle, subId=%d", it->_key);
_subsFocus.remove(it->_key);
delete it->_value;
_subsList.erase(it);
_redraw = true;
}
}
if (_subsList.size() == 0)
if (_subId != 0) {
debugC(4, kDebugSubtitle, "Resetting subId to 0");
_subId = 0;
_subsFocus.clear();
}
if (_redraw) {
debugC(4, kDebugSubtitle, "Redrawing subtitles");
// Blank subtitle buffer
_renderManager->clearTextSurface();
// Render just the most recent subtitle
if (_subsFocus.size()) {
uint16 curSub = _subsFocus.front();
debugC(4, kDebugSubtitle, "Rendering subtitle %d", curSub);
Subtitle *sub = _subsList[curSub];
if (sub->_lineId >= 0) {
Graphics::Surface textSurface;
//TODO - make this surface a persistent member of the manager; only call create() when currently displayed subtitle is changed.
textSurface.create(sub->_textArea.width(), sub->_textArea.height(), _engine->_resourcePixelFormat);
textSurface.fillRect(Common::Rect(sub->_textArea.width(), sub->_textArea.height()), (uint32)-1); // TODO Unnecessary operation? Check later.
_engine->getTextRenderer()->drawTextWithWordWrapping(sub->_lines[sub->_lineId].subStr, textSurface, _engine->isWidescreen());
_renderManager->blitSurfaceToText(textSurface, sub->_textArea.left, sub->_textArea.top, -1);
textSurface.free();
sub->_redraw = false;
}
}
_redraw = false;
}
}
void SubtitleManager::update(int32 count, uint16 subid) {
if (_subsList.contains(subid))
if (_subsList[subid]->update(count)) {
// _subsFocus.set(subid);
_redraw = true;
}
}
uint16 SubtitleManager::create(const Common::Path &subname, bool vob) {
_subId++;
debugC(2, kDebugSubtitle, "Creating scripted subtitle, subId=%d", _subId);
_subsList[_subId] = new Subtitle(_engine, subname, vob);
_subsFocus.set(_subId);
return _subId;
}
uint16 SubtitleManager::create(const Common::Path &subname, Audio::SoundHandle handle) {
_subId++;
debugC(2, kDebugSubtitle, "Creating scripted subtitle, subId=%d", _subId);
_subsList[_subId] = new AutomaticSubtitle(_engine, subname, handle);
_subsFocus.set(_subId);
return _subId;
}
uint16 SubtitleManager::create(const Common::String &str) {
_subId++;
debugC(2, kDebugSubtitle, "Creating simple subtitle, subId=%d, message %s", _subId, str.c_str());
_subsList[_subId] = new Subtitle(_engine, str, _textArea);
_subsFocus.set(_subId);
return _subId;
}
void SubtitleManager::destroy(uint16 id) {
if (_subsList.contains(id)) {
debugC(2, kDebugSubtitle, "Marking subtitle %d for immediate deletion", id);
_subsList[id]->_toDelete = true;
}
}
void SubtitleManager::destroy(uint16 id, int16 delay) {
if (_subsList.contains(id)) {
debugC(2, kDebugSubtitle, "Marking subtitle %d for deletion in %dms", id, delay);
_subsList[id]->_timer = delay;
}
}
void SubtitleManager::timedMessage(const Common::String &str, uint16 milsecs) {
uint16 msgid = create(str);
debugC(1, kDebugSubtitle, "initiating timed message: %s to subtitle id %d, time %d", str.c_str(), msgid, milsecs);
update(0, msgid);
process(0);
destroy(msgid, milsecs);
}
bool SubtitleManager::askQuestion(const Common::String &str, bool streaming, bool safeDefault) {
uint16 msgid = create(str);
debugC(1, kDebugSubtitle, "initiating user question: %s to subtitle id %d", str.c_str(), msgid);
update(0, msgid);
process(0);
if(streaming)
_renderManager->renderSceneToScreen(true,true,true);
else
_renderManager->renderSceneToScreen(true);
_engine->stopClock();
int result = 0;
while (result == 0) {
Common::Event evnt;
while (_engine->getEventManager()->pollEvent(evnt)) {
switch (evnt.type) {
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
if ((ZVisionAction)evnt.customType != kZVisionActionQuit)
break;
// fall through
case Common::EVENT_QUIT:
debugC(1, kDebugEvent, "Attempting to quit within quit dialog!");
_engine->quit(false);
return safeDefault;
break;
case Common::EVENT_KEYDOWN:
// English: yes/no
// German: ja/nein
// Spanish: si/no
// French Nemesis: F4/any other key _engine(engine),
// French ZGI: oui/non
// TODO: Handle this using the keymapper
switch (evnt.kbd.keycode) {
case Common::KEYCODE_y:
if (_engine->getLanguage() == Common::EN_ANY)
result = 2;
break;
case Common::KEYCODE_j:
if (_engine->getLanguage() == Common::DE_DEU)
result = 2;
break;
case Common::KEYCODE_s:
if (_engine->getLanguage() == Common::ES_ESP)
result = 2;
break;
case Common::KEYCODE_o:
if (_engine->getLanguage() == Common::FR_FRA && _engine->getGameId() == GID_GRANDINQUISITOR)
result = 2;
break;
case Common::KEYCODE_F4:
if (_engine->getLanguage() == Common::FR_FRA && _engine->getGameId() == GID_NEMESIS)
result = 2;
break;
case Common::KEYCODE_n:
result = 1;
break;
default:
if (_engine->getLanguage() == Common::FR_FRA && _engine->getGameId() == GID_NEMESIS)
result = 1;
break;
}
break;
default:
break;
}
}
if(streaming)
_renderManager->renderSceneToScreen(true,true,false);
else
_renderManager->renderSceneToScreen(true);
if (_doubleFPS)
_system->delayMillis(33);
else
_system->delayMillis(66);
}
destroy(msgid);
_engine->startClock();
return result == 2;
}
void SubtitleManager::delayedMessage(const Common::String &str, uint16 milsecs) {
uint16 msgid = create(str);
debugC(1, kDebugSubtitle, "initiating delayed message: %s to subtitle id %d, delay %dms", str.c_str(), msgid, milsecs);
update(0, msgid);
process(0);
_renderManager->renderSceneToScreen(true);
_engine->stopClock();
uint32 stopTime = _system->getMillis() + milsecs;
while (_system->getMillis() < stopTime) {
Common::Event evnt;
while (_engine->getEventManager()->pollEvent(evnt)) {
switch (evnt.type) {
case Common::EVENT_KEYDOWN:
switch (evnt.kbd.keycode) {
case Common::KEYCODE_SPACE:
case Common::KEYCODE_RETURN:
case Common::KEYCODE_ESCAPE:
goto skip_delayed_message;
break;
default:
break;
}
break;
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
if ((ZVisionAction)evnt.customType != kZVisionActionQuit)
break;
// fall through
case Common::EVENT_QUIT:
if (ConfMan.hasKey("confirm_exit") && ConfMan.getBool("confirm_exit"))
_engine->quit(true);
else
_engine->quit(false);
break;
default:
break;
}
}
skip_delayed_message:
_renderManager->renderSceneToScreen(true);
if (_doubleFPS)
_system->delayMillis(17);
else
_system->delayMillis(33);
}
destroy(msgid);
_engine->startClock();
}
void SubtitleManager::showDebugMsg(const Common::String &msg, int16 delay) {
uint16 msgid = create(msg);
debugC(1, kDebugSubtitle, "initiating in-game debug message: %s to subtitle id %d, delay %dms", msg.c_str(), msgid, delay);
update(0, msgid);
process(0);
destroy(msgid, delay);
}
Subtitle::Subtitle(ZVision *engine, const Common::Path &subname, bool vob) :
_engine(engine),
_lineId(-1),
_timer(-1),
_toDelete(false),
_redraw(false) {
Common::File subFile;
Common::Point _textOffset = _engine->getSubtitleManager()->getTextOffset();
if (!subFile.open(subname)) {
warning("Failed to open subtitle %s", subname.toString().c_str());
_toDelete = true;
return;
}
// Parse subtitle parameters from script
while (!subFile.eos()) {
Common::String str = subFile.readLine();
if (str.lastChar() == '~')
str.deleteLastChar();
if (str.matchString("*Initialization*", true)) {
// Not used
} else if (str.matchString("*Rectangle*", true)) {
int32 x1, y1, x2, y2;
if (sscanf(str.c_str(), "%*[^:]:%d %d %d %d", &x1, &y1, &x2, &y2) == 4) {
_textArea = Common::Rect(x1, y1, x2, y2);
debugC(1, kDebugSubtitle, "Original subtitle script rectangle coordinates: l%d, t%d, r%d, b%d", x1, y1, x2, y2);
// Original game subtitle scripts appear to define subtitle rectangles relative to origin of working area.
// To allow arbitrary aspect ratios, we need to instead place these relative to origin of text area.
// This will allow the managed text area to then be arbitrarily placed on the screen to suit different aspect ratios.
_textArea.translate(_textOffset.x, _textOffset.y); // Convert working area coordinates to text area coordinates
debugC(1, kDebugSubtitle, "Text area coordinates: l%d, t%d, r%d, b%d", _textArea.left, _textArea.top, _textArea.right, _textArea.bottom);
}
} else if (str.matchString("*TextFile*", true)) {
char filename[64];
if (sscanf(str.c_str(), "%*[^:]:%s", filename) == 1) {
Common::File txtFile;
if (txtFile.open(Common::Path(filename))) {
while (!txtFile.eos()) {
Common::String txtline = readWideLine(txtFile).encode();
Line curLine;
curLine.start = -1;
curLine.stop = -1;
curLine.subStr = txtline;
_lines.push_back(curLine);
}
txtFile.close();
}
}
} else {
int32 st; // Line start time
int32 en; // Line end time
int32 sb; // Line number
if (sscanf(str.c_str(), "%*[^:]:(%d,%d)=%d", &st, &en, &sb) == 3) {
if (sb <= (int32)_lines.size()) {
if (vob) {
// Convert frame number from 15FPS (AVI) to 29.97FPS (VOB) to synchronise with video
// st = st * 2997 / 1500;
// en = en * 2997 / 1500;
st = st * 2900 / 1500; // TODO: Subtitles only synchronise correctly at 29fps, but vob files should be 29.97fps; check if video codec is rounding this value down!
en = en * 2900 / 1500;
}
_lines[sb].start = st;
_lines[sb].stop = en;
}
}
}
}
subFile.close();
}
Subtitle::Subtitle(ZVision *engine, const Common::String &str, const Common::Rect &textArea) :
_engine(engine),
_lineId(-1),
_timer(-1),
_toDelete(false),
_redraw(false) {
_textArea = textArea;
debugC(1, kDebugSubtitle, "Text area coordinates: l%d, t%d, r%d, b%d", _textArea.left, _textArea.top, _textArea.right, _textArea.bottom);
Line curLine;
curLine.start = -1;
curLine.stop = 0;
curLine.subStr = str;
_lines.push_back(curLine);
}
Subtitle::~Subtitle() {
_lines.clear();
}
bool Subtitle::process(int32 deltatime) {
if (_timer != -1) {
_timer -= deltatime;
if (_timer <= 0)
_toDelete = true;
}
return _toDelete;
}
bool Subtitle::update(int32 count) {
int16 j = -1;
// Search all lines to find first line that encompasses current time/framecount, set j to this
for (uint16 i = (_lineId >= 0 ? _lineId : 0); i < _lines.size(); i++)
if (count >= _lines[i].start && count <= _lines[i].stop) {
j = i;
break;
}
if (j == -1) {
// No line exists for current time/framecount
if (_lineId != -1) {
// Line is set
_lineId = -1; // Unset line
_redraw = true;
}
} else {
// Line exists for current time/framecount
if (j != _lineId && _lines[j].subStr.size()) {
// Set line is not equal to current line & current line is not blank
_lineId = j; // Set line to current
_redraw = true;
}
}
return _redraw;
}
AutomaticSubtitle::AutomaticSubtitle(ZVision *engine, const Common::Path &subname, Audio::SoundHandle handle) :
Subtitle(engine, subname, false),
_handle(handle) {
}
bool AutomaticSubtitle::selfUpdate() {
if (_engine->_mixer->isSoundHandleActive(_handle) && _engine->getScriptManager()->getStateValue(StateKey_Subtitles) == 1)
return update(_engine->_mixer->getSoundElapsedTime(_handle) / 100);
else {
_toDelete = true;
return false;
}
}
bool AutomaticSubtitle::process(int32 deltatime) {
Subtitle::process(deltatime);
if (!_engine->_mixer->isSoundHandleActive(_handle))
_toDelete = true;
return _toDelete;
}
} // End of namespace ZVision