Initial commit
This commit is contained in:
430
engines/zvision/text/subtitle_manager.cpp
Normal file
430
engines/zvision/text/subtitle_manager.cpp
Normal file
@@ -0,0 +1,430 @@
|
||||
/* 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
|
||||
Reference in New Issue
Block a user