[Scummvm-git-logs] scummvm master -> f7a3e571f06a00b26c82d662efcb09cb175821c0
elasota
noreply at scummvm.org
Wed Aug 10 01:55:05 UTC 2022
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
f7a3e571f0 MTROPOLIS: Add subtitles support
Commit: f7a3e571f06a00b26c82d662efcb09cb175821c0
https://github.com/scummvm/scummvm/commit/f7a3e571f06a00b26c82d662efcb09cb175821c0
Author: elasota (ejlasota at gmail.com)
Date: 2022-08-09T21:54:08-04:00
Commit Message:
MTROPOLIS: Add subtitles support
Changed paths:
A engines/mtropolis/subtitles.cpp
A engines/mtropolis/subtitles.h
engines/mtropolis/boot.cpp
engines/mtropolis/detection.cpp
engines/mtropolis/detection_tables.h
engines/mtropolis/elements.cpp
engines/mtropolis/elements.h
engines/mtropolis/module.mk
engines/mtropolis/render.cpp
engines/mtropolis/render.h
engines/mtropolis/runtime.cpp
engines/mtropolis/runtime.h
diff --git a/engines/mtropolis/boot.cpp b/engines/mtropolis/boot.cpp
index 19b838ae0f7..6774bb6ac89 100644
--- a/engines/mtropolis/boot.cpp
+++ b/engines/mtropolis/boot.cpp
@@ -31,6 +31,7 @@
#include "mtropolis/boot.h"
#include "mtropolis/detection.h"
#include "mtropolis/runtime.h"
+#include "mtropolis/subtitles.h"
#include "mtropolis/plugin/obsidian.h"
#include "mtropolis/plugin/standard.h"
@@ -535,9 +536,32 @@ Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription
Common::SharedPtr<Boot::GameDataHandler> gameDataHandler;
+ Common::SharedPtr<SubtitleAssetMappingTable> subsAssetMappingTable;
+ Common::SharedPtr<SubtitleModifierMappingTable> subsModifierMappingTable;
+ Common::SharedPtr<SubtitleSpeakerTable> subsSpeakerTable;
+ Common::SharedPtr<SubtitleLineTable> subsLineTable;
+
+ Common::String speakerTablePath;
+ Common::String linesTablePath;
+ Common::String assetMappingTablePath;
+ Common::String modifierMappingTablePath;
+
switch (gameDesc.gameID) {
case GID_OBSIDIAN:
gameDataHandler.reset(new Boot::ObsidianGameDataHandler(gameDesc));
+
+ if ((gameDesc.desc.flags & ADGF_DEMO) == 0 && gameDesc.desc.language == Common::EN_ANY) {
+ linesTablePath = "subtitles_lines_obsidian_en.csv";
+ speakerTablePath = "subtitles_speakers_obsidian_en.csv";
+
+ if (gameDesc.desc.platform == Common::kPlatformWindows) {
+ assetMappingTablePath = "subtitles_asset_mapping_obsidian_win_en.csv";
+ modifierMappingTablePath = "subtitles_modifier_mapping_obsidian_win_en.csv";
+ } else if (gameDesc.desc.platform == Common::kPlatformMacintosh) {
+ assetMappingTablePath = "subtitles_asset_mapping_obsidian_mac_en.csv";
+ modifierMappingTablePath = "subtitles_modifier_mapping_obsidian_mac_en.csv";
+ }
+ }
break;
default:
gameDataHandler.reset(new Boot::GameDataHandler());
@@ -885,6 +909,41 @@ Common::SharedPtr<ProjectDescription> bootProject(const MTropolisGameDescription
desc->setResources(resources);
+ if (assetMappingTablePath.size() > 0 && linesTablePath.size() > 0) {
+ subsAssetMappingTable.reset(new SubtitleAssetMappingTable());
+ subsModifierMappingTable.reset(new SubtitleModifierMappingTable());
+ subsSpeakerTable.reset(new SubtitleSpeakerTable());
+ subsLineTable.reset(new SubtitleLineTable());
+
+ Common::ErrorCode assetMappingError = subsAssetMappingTable->load(assetMappingTablePath);
+ Common::ErrorCode modifierMappingError = subsModifierMappingTable->load(modifierMappingTablePath);
+ Common::ErrorCode speakerError = subsSpeakerTable->load(speakerTablePath);
+ Common::ErrorCode linesError = speakerError;
+
+ if (speakerError == Common::kNoError)
+ linesError = subsLineTable->load(linesTablePath, *subsSpeakerTable);
+
+ if (assetMappingError != Common::kNoError || modifierMappingError != Common::kNoError || linesError != Common::kNoError) {
+ // If all sub files are missing, then the user hasn't installed them
+ if (assetMappingError != Common::kPathDoesNotExist || modifierMappingError != Common::kPathDoesNotExist || linesError != Common::kPathDoesNotExist) {
+ warning("Failed to load subtitles data");
+ }
+
+ subsAssetMappingTable.reset();
+ subsModifierMappingTable.reset();
+ subsLineTable.reset();
+ subsSpeakerTable.reset();
+ }
+ }
+
+ SubtitleTables subTables;
+ subTables.assetMapping = subsAssetMappingTable;
+ subTables.lines = subsLineTable;
+ subTables.modifierMapping = subsModifierMappingTable;
+ subTables.speakers = subsSpeakerTable;
+
+ desc->getSubtitles(subTables);
+
return desc;
}
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index c004792abc7..25a02eca970 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -60,7 +60,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
}
},
{
- GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS,
+ GAMEOPTION_SOUND_GAMEPLAY_SUBTITLES,
{
_s("Autosave at progress points"),
_s("Automatically saves the game after completing puzzles and chapters."),
@@ -81,6 +81,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+ {
+ GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS,
+ {
+ _s("Use subtitles for sound-based gameplay elements"),
+ _s("Enables subtitles for gameplay elements that depend on hearing an audible sound."),
+ "mtropolis_mod_sound_gameplay_subtitles",
+ false,
+ 0,
+ 0
+ }
+ },
{
GAMEOPTION_LAUNCH_DEBUG,
{
diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index e69a504ee20..ac1e262a232 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -26,12 +26,12 @@
#include "mtropolis/detection.h"
-#define GAMEOPTION_WIDESCREEN_MOD GUIO_GAMEOPTIONS1
-#define GAMEOPTION_DYNAMIC_MIDI GUIO_GAMEOPTIONS2
-#define GAMEOPTION_LAUNCH_DEBUG GUIO_GAMEOPTIONS3
-//#define GAMEOPTION_LAUNCH_BREAK GUIO_GAMEOPTIONS4 // Disabled due to not being functional
-#define GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS GUIO_GAMEOPTIONS5
-#define GAMEOPTION_ENABLE_SHORT_TRANSITIONS GUIO_GAMEOPTIONS6
+#define GAMEOPTION_WIDESCREEN_MOD GUIO_GAMEOPTIONS1
+#define GAMEOPTION_DYNAMIC_MIDI GUIO_GAMEOPTIONS2
+#define GAMEOPTION_LAUNCH_DEBUG GUIO_GAMEOPTIONS3
+#define GAMEOPTION_SOUND_GAMEPLAY_SUBTITLES GUIO_GAMEOPTIONS4
+#define GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS GUIO_GAMEOPTIONS5
+#define GAMEOPTION_ENABLE_SHORT_TRANSITIONS GUIO_GAMEOPTIONS6
namespace MTropolis {
@@ -53,7 +53,7 @@ static const MTropolisGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformMacintosh,
ADGF_TESTING,
- GUIO2(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS)
+ GUIO3(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS, GAMEOPTION_SOUND_GAMEPLAY_SUBTITLES)
},
GID_OBSIDIAN,
0,
@@ -75,7 +75,7 @@ static const MTropolisGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformMacintosh,
ADGF_TESTING,
- GUIO2(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS)
+ GUIO3(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS, GAMEOPTION_SOUND_GAMEPLAY_SUBTITLES)
},
GID_OBSIDIAN,
0,
@@ -102,7 +102,7 @@ static const MTropolisGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformWindows,
ADGF_TESTING,
- GUIO2(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS)
+ GUIO3(GAMEOPTION_WIDESCREEN_MOD, GAMEOPTION_AUTO_SAVE_AT_CHECKPOINTS, GAMEOPTION_SOUND_GAMEPLAY_SUBTITLES)
},
GID_OBSIDIAN,
0,
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index bcccbd60921..50257d9ce6d 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -434,6 +434,8 @@ MovieElement::~MovieElement() {
_unloadSignaller->removeReceiver(this);
if (_playMediaSignaller)
_playMediaSignaller->removeReceiver(this);
+
+ stopSubtitles();
}
bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
@@ -513,6 +515,8 @@ VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::Shared
}
if (Event(EventIDs::kStop, 0).respondsTo(msg->getEvent())) {
if (!_paused) {
+ stopSubtitles();
+
_paused = true;
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
@@ -583,6 +587,18 @@ void MovieElement::activate() {
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
+
+ const SubtitleTables &subtitleTables = project->getSubtitles();
+ if (subtitleTables.assetMapping) {
+ const Common::String *subSetIDPtr = subtitleTables.assetMapping->findSubtitleSetForAssetID(_assetID);
+ if (!subSetIDPtr) {
+ Common::String assetName = project->getAssetNameByID(_assetID);
+ subSetIDPtr = subtitleTables.assetMapping->findSubtitleSetForAssetName(assetName);
+ }
+
+ if (subSetIDPtr)
+ _subtitles.reset(new SubtitlePlayer(_runtime, *subSetIDPtr, subtitleTables));
+ }
}
void MovieElement::deactivate() {
@@ -747,6 +763,9 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
for (MediaCueState *mediaCue : _mediaCues)
mediaCue->checkTimestampChange(runtime, _currentTimestamp, targetTS, checkContinuously, true);
+ if (_subtitles)
+ _subtitles->update(_currentTimestamp * 1000 / _timeScale, targetTS * 1000 / _timeScale);
+
_currentTimestamp = targetTS;
if (_currentTimestamp == maxTS) {
@@ -761,6 +780,7 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
if (triggerEndEvents) {
if (!_loop) {
_paused = true;
+ stopSubtitles();
Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
@@ -778,6 +798,8 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
_currentPlayState = kMediaStateStopped;
if (_loop) {
+ stopSubtitles();
+
_needsReset = true;
_currentTimestamp = _reversed ? realRange.max : realRange.min;
_contentsDirty = true;
@@ -801,6 +823,18 @@ IntRange MovieElement::computeRealRange() const {
return _playRange;
}
+void MovieElement::stopSubtitles() {
+ if (_subtitles)
+ _subtitles->stop();
+}
+
+void MovieElement::onPauseStateChanged() {
+ VisualElement::onPauseStateChanged();
+
+ if (_paused && _subtitles)
+ _subtitles->stop();
+}
+
MiniscriptInstructionOutcome MovieElement::scriptSetRange(MiniscriptThread *thread, const DynamicValue &value) {
if (value.getType() != DynamicValueTypes::kIntegerRange) {
thread->error("Wrong type for movie element range");
@@ -936,6 +970,8 @@ VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData
_shouldPlayIfNotPaused = true;
_paused = false;
+
+ stopSubtitles();
}
return kVThreadReturn;
@@ -963,6 +999,8 @@ VThreadState MovieElement::seekToTimeTask(const SeekToTimeTaskData &taskData) {
_needsReset = true;
_contentsDirty = true;
+ stopSubtitles();
+
return kVThreadReturn;
}
@@ -1952,8 +1990,8 @@ MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::refA
}
SoundElement::SoundElement()
- : _leftVolume(0), _rightVolume(0), _balance(0), _assetID(0), _finishTime(0),
- _shouldPlayIfNotPaused(true), _needsReset(true), _runtime(nullptr) {
+ : _leftVolume(0), _rightVolume(0), _balance(0), _assetID(0), _startTime(0), _finishTime(0), _cueCheckTime(0),
+ _startTimestamp(0), _shouldPlayIfNotPaused(true), _needsReset(true), _runtime(nullptr) {
}
SoundElement::~SoundElement() {
@@ -2041,6 +2079,19 @@ void SoundElement::activate() {
if (_name.empty())
_name = project->getAssetNameByID(_assetID);
+
+ const SubtitleTables &subTables = project->getSubtitles();
+ if (subTables.assetMapping) {
+ const Common::String *subtitleSetIDPtr = subTables.assetMapping->findSubtitleSetForAssetID(_assetID);
+ if (!subtitleSetIDPtr) {
+ Common::String assetName = project->getAssetNameByID(_assetID);
+ if (assetName.size() > 0)
+ subtitleSetIDPtr = subTables.assetMapping->findSubtitleSetForAssetName(assetName);
+ }
+
+ if (subtitleSetIDPtr)
+ _subtitlePlayer.reset(new SubtitlePlayer(_runtime, *subtitleSetIDPtr, subTables));
+ }
}
@@ -2064,31 +2115,45 @@ void SoundElement::playMedia(Runtime *runtime, Project *project) {
if (_paused) {
// Goal state is paused
// TODO: Track pause time
- _player.reset();
+ stopPlayer();
} else {
// Goal state is playing
if (_needsReset) {
// TODO: Reset to start time
- _player.reset();
+ stopPlayer();
_needsReset = false;
}
if (!_player) {
_finishTime = _runtime->getPlayTime() + _metadata->durationMSec;
- _player.reset();
-
int normalizedVolume = (_leftVolume + _rightVolume) * 255 / 200;
int normalizedBalance = _balance * 127 / 100;
// TODO: Support ranges
size_t numSamples = _cachedAudio->getNumSamples(*_metadata);
_player.reset(new AudioPlayer(_runtime->getAudioMixer(), normalizedVolume, normalizedBalance, _metadata, _cachedAudio, _loop, 0, 0, numSamples));
+
+ _startTime = runtime->getPlayTime();
+ _cueCheckTime = _startTime;
+ _startTimestamp = 0;
+ }
+
+ uint64 newTime = _runtime->getPlayTime();
+ if (newTime > _cueCheckTime) {
+ uint64 oldTimeRelative = _cueCheckTime - _startTime + _startTimestamp;
+ uint64 newTimeRelative = newTime - _startTime + _startTimestamp;
+
+ _cueCheckTime = newTime;
+
+ if (_subtitlePlayer)
+ _subtitlePlayer->update(oldTimeRelative, newTimeRelative);
+
+ // TODO: Check cue points and queue them here
}
- // TODO: Check cue points and queue them here
- if (!_loop && _runtime->getPlayTime() >= _finishTime) {
+ if (!_loop && newTime >= _finishTime) {
// Don't throw out the handle - It can still be playing but we just treat it like it's not.
// If it has anything left, then we let it finish and avoid clipping the sound, but we need
// to know that the handle is still here so we can actually stop it if the element is
@@ -2099,14 +2164,22 @@ void SoundElement::playMedia(Runtime *runtime, Project *project) {
runtime->queueMessage(dispatch);
_shouldPlayIfNotPaused = false;
+ if (_subtitlePlayer)
+ _subtitlePlayer->stop();
}
}
} else {
// Goal state is stopped
- _player.reset();
+ stopPlayer();
}
}
+void SoundElement::stopPlayer() {
+ _player.reset();
+ if (_subtitlePlayer)
+ _subtitlePlayer->stop();
+}
+
#ifdef MTROPOLIS_DEBUG_ENABLE
void SoundElement::debugInspect(IDebugInspectionReport *report) const {
NonVisualElement::debugInspect(report);
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 94044aa0e95..a024f56fa0c 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -109,11 +109,15 @@ public:
SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
#endif
-private:
+protected:
+ void onPauseStateChanged() override;
void onSegmentUnloaded(int segmentIndex) override;
+private:
IntRange computeRealRange() const;
+ void stopSubtitles();
+
MiniscriptInstructionOutcome scriptSetRange(MiniscriptThread *thread, const DynamicValue &value);
MiniscriptInstructionOutcome scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value);
MiniscriptInstructionOutcome scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value);
@@ -164,6 +168,8 @@ private:
Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
+ Common::SharedPtr<SubtitlePlayer> _subtitles;
+
Common::Array<int> _damagedFrames;
Runtime *_runtime;
@@ -380,6 +386,8 @@ public:
#endif
private:
+ void stopPlayer();
+
MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
MiniscriptInstructionOutcome scriptSetBalance(MiniscriptThread *thread, const DynamicValue &value);
@@ -405,12 +413,17 @@ private:
Common::SharedPtr<CachedAudio> _cachedAudio;
Common::SharedPtr<AudioMetadata> _metadata;
Common::SharedPtr<AudioPlayer> _player;
+ uint64 _startTime;
uint64 _finishTime;
+ uint64 _startTimestamp; // Time in the sound corresponding to the start time
+ uint64 _cueCheckTime;
bool _shouldPlayIfNotPaused;
bool _needsReset;
Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
+ Common::SharedPtr<SubtitlePlayer> _subtitlePlayer;
+
Runtime *_runtime;
};
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index c8f0a9ce5dc..523ceed12d2 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -23,6 +23,7 @@ MODULE_OBJS = \
render.o \
runtime.o \
saveload.o \
+ subtitles.o \
vthread.o
# This module can be built as a plugin
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 083265167a2..86c2d49fb57 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -259,7 +259,7 @@ static void renderDirectElement(const RenderItem &item, Window *mainWindow) {
renderNormalElement(item, mainWindow); // Meh
}
-void renderProject(Runtime *runtime, Window *mainWindow) {
+void renderProject(Runtime *runtime, Window *mainWindow, bool *outSkipped) {
bool sceneChanged = runtime->isSceneGraphDirty();
Common::Array<Structural *> scenes;
@@ -296,6 +296,9 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
}
if (sceneChanged) {
+ if (outSkipped)
+ *outSkipped = false;
+
for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
renderNormalElement(*it, mainWindow);
@@ -304,6 +307,9 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
for (const IPostEffect *postEffect : runtime->getPostEffects())
postEffect->renderPostEffect(*mainWindow->getSurface());
+ } else {
+ if (outSkipped)
+ *outSkipped = true;
}
runtime->clearSceneGraphDirty();
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 1d3ba7b7a7d..9b0305f837e 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -135,7 +135,7 @@ protected:
namespace Render {
uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt);
-void renderProject(Runtime *runtime, Window *mainWindow);
+void renderProject(Runtime *runtime, Window *mainWindow, bool *outSkipped);
void renderSceneTransition(Runtime *runtime, Window *mainWindow, const SceneTransitionEffect &effect, uint32 startTime, uint32 endTime, uint32 currentTime, const Graphics::ManagedSurface &oldFrame, const Graphics::ManagedSurface &newFrame);
void convert32To16(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index acdaaaa3b79..1a739f2baee 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2330,6 +2330,14 @@ ProjectPlatform ProjectDescription::getPlatform() const {
return _platform;
}
+const SubtitleTables &ProjectDescription::getSubtitles() const {
+ return _subtitles;
+}
+
+void ProjectDescription::getSubtitles(const SubtitleTables &subs) {
+ _subtitles = subs;
+}
+
const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
return _modifiers;
}
@@ -3855,6 +3863,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
_random.reset(new Common::RandomSource("mtropolis"));
_vthread.reset(new VThread());
+ _subtitleRenderer.reset(new SubtitleRenderer());
for (int i = 0; i < kColorDepthModeCount; i++) {
_displayModeSupported[i] = false;
@@ -3901,6 +3910,13 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
_getSetAttribIDsToAttribName[AttributeIDs::kAttribUserTimeout] = "usertimeout";
}
+Runtime::~Runtime() {
+ // Clear the project first, which should detach any references to other things
+ _project.reset();
+
+ _subtitleRenderer.reset();
+}
+
bool Runtime::runFrame() {
uint32 timeMillis = _system->getMillis();
@@ -4106,7 +4122,7 @@ bool Runtime::runFrame() {
_sceneTransitionOldFrame->copyFrom(*mainWindow->getSurface());
- Render::renderProject(this, mainWindow.get());
+ Render::renderProject(this, mainWindow.get(), nullptr);
_sceneTransitionNewFrame->copyFrom(*mainWindow->getSurface());
}
@@ -4194,14 +4210,26 @@ void Runtime::drawFrame() {
_system->fillScreen(Render::resolveRGB(0, 0, 0, getRenderPixelFormat()));
+ bool needToRenderSubtitles = false;
+ if (_subtitleRenderer->update(_playTime))
+ setSceneGraphDirty();
+
{
Common::SharedPtr<Window> mainWindow = _mainWindow.lock();
if (mainWindow) {
if (_sceneTransitionState == kSceneTransitionStateTransitioning) {
assert(_activeSceneTransitionEffect != nullptr);
Render::renderSceneTransition(this, mainWindow.get(), *_activeSceneTransitionEffect, _sceneTransitionStartTime, _sceneTransitionEndTime, _playTime, *_sceneTransitionOldFrame, *_sceneTransitionNewFrame);
- } else
- Render::renderProject(this, mainWindow.get());
+ needToRenderSubtitles = true;
+ } else {
+ bool skipped = false;
+ Render::renderProject(this, mainWindow.get(), &skipped);
+
+ needToRenderSubtitles = !skipped;
+ }
+
+ if (needToRenderSubtitles)
+ _subtitleRenderer->composite(*mainWindow);
}
}
@@ -5833,6 +5861,10 @@ bool Runtime::isIdle() const {
return true;
}
+const Common::SharedPtr<SubtitleRenderer> &Runtime::getSubtitleRenderer() const {
+ return _subtitleRenderer;
+}
+
void Runtime::ensureMainWindowExists() {
// Maybe there's a better spot for this
if (_mainWindow.expired() && _project) {
@@ -6279,6 +6311,7 @@ VThreadState Project::consumeCommand(Runtime *runtime, const Common::SharedPtr<M
void Project::loadFromDescription(const ProjectDescription &desc, const Hacks &hacks) {
_resources = desc.getResources();
_cursorGraphics = desc.getCursorGraphics();
+ _subtitles = desc.getSubtitles();
debug(1, "Loading new project...");
@@ -6682,6 +6715,10 @@ void Project::loadBootStream(size_t streamIndex, const Hacks &hacks) {
assignAssets(assetDefLoader.assets, hacks);
}
+const SubtitleTables &Project::getSubtitles() const {
+ return _subtitles;
+}
+
void Project::loadPresentationSettings(const Data::PresentationSettings &presentationSettings) {
_presentationSettings.bitsPerPixel = presentationSettings.bitsPerPixel;
if (_presentationSettings.bitsPerPixel != 16) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 23841938e8c..3686da31101 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -38,6 +38,7 @@
#include "mtropolis/data.h"
#include "mtropolis/debug.h"
#include "mtropolis/hacks.h"
+#include "mtropolis/subtitles.h"
#include "mtropolis/vthread.h"
class OSystem;
@@ -1215,12 +1216,16 @@ public:
ProjectPlatform getPlatform() const;
+ const SubtitleTables &getSubtitles() const;
+ void getSubtitles(const SubtitleTables &subs);
+
private:
Common::Array<SegmentDescription> _segments;
Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
Common::SharedPtr<ProjectResources> _resources;
Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
Common::Language _language;
+ SubtitleTables _subtitles;
ProjectPlatform _platform;
};
@@ -1483,6 +1488,7 @@ public:
class Runtime {
public:
explicit Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider);
+ ~Runtime();
bool runFrame();
void drawFrame();
@@ -1607,6 +1613,8 @@ public:
bool isIdle() const;
+ const Common::SharedPtr<SubtitleRenderer> &getSubtitleRenderer() const;
+
#ifdef MTROPOLIS_DEBUG_ENABLE
void debugSetEnabled(bool enabled);
void debugBreak();
@@ -1843,6 +1851,8 @@ private:
Common::Array<IPostEffect *> _postEffects;
+ Common::SharedPtr<SubtitleRenderer> _subtitleRenderer;
+
Hacks _hacks;
Common::HashMap<uint32, Common::String> _getSetAttribIDsToAttribName;
@@ -2256,6 +2266,8 @@ public:
const Common::SharedPtr<CursorGraphicCollection> &getCursorGraphics() const;
+ const SubtitleTables &getSubtitles() const;
+
#ifdef MTROPOLIS_DEBUG_ENABLE
const char *debugGetTypeName() const override { return "Project"; }
#endif
@@ -2365,6 +2377,8 @@ private:
Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
Common::SharedPtr<KeyboardEventSignaller> _keyboardEventSignaller;
+ SubtitleTables _subtitles;
+
Runtime *_runtime;
};
diff --git a/engines/mtropolis/subtitles.cpp b/engines/mtropolis/subtitles.cpp
new file mode 100644
index 00000000000..f5745e6c81b
--- /dev/null
+++ b/engines/mtropolis/subtitles.cpp
@@ -0,0 +1,742 @@
+/* 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 "mtropolis/subtitles.h"
+
+#include "common/archive.h"
+#include "common/array.h"
+#include "common/file.h"
+#include "common/hashmap.h"
+#include "common/stream.h"
+#include "common/hash-str.h"
+
+#include "graphics/font.h"
+#include "graphics/fonts/ttf.h"
+#include "graphics/fontman.h"
+#include "graphics/managed_surface.h"
+
+#include "mtropolis/render.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+class SubtitleCSVLoader {
+public:
+ explicit SubtitleCSVLoader(Common::ReadStream *stream);
+
+ bool readLine(Common::Array<Common::String> &outStrings);
+
+private:
+ bool readQuotedCel(Common::String &str);
+ bool readUnquotedCel(Common::String &str);
+
+ Common::Array<char> _contents;
+ uint _readOffset;
+ uint _line;
+};
+
+SubtitleCSVLoader::SubtitleCSVLoader(Common::ReadStream *stream) : _readOffset(0), _line(1) {
+ char chunk[4096];
+ while (!stream->eos() && !stream->err()) {
+ uint32 countRead = stream->read(chunk, sizeof(chunk));
+ if (countRead == 0)
+ return;
+
+ _contents.resize(_contents.size() + countRead);
+ memcpy(&_contents[_contents.size() - countRead], chunk, countRead);
+ }
+}
+
+bool SubtitleCSVLoader::readLine(Common::Array<Common::String> &outStrings) {
+ uint numStrs = 0;
+ while (_readOffset < _contents.size()) {
+ if (outStrings.size() == numStrs)
+ outStrings.push_back(Common::String());
+
+ Common::String &celStr = outStrings[numStrs];
+
+ char nextChar = _contents[_readOffset];
+ bool celStatus = false;
+ if (nextChar == '\"')
+ celStatus = readQuotedCel(celStr);
+ else
+ celStatus = readUnquotedCel(celStr);
+
+ if (!celStatus)
+ return false;
+
+ numStrs++;
+
+ if (_readOffset == _contents.size())
+ break;
+
+ char dividerChar = _contents[_readOffset];
+ if (dividerChar == ',')
+ _readOffset++;
+ else if (dividerChar == '\r' || dividerChar == '\n') {
+ _readOffset++;
+ if (dividerChar == '\r' && _readOffset < _contents.size() && _contents[_readOffset] == '\n')
+ _readOffset++;
+ break;
+ } else {
+ return false;
+ }
+ }
+
+ outStrings.resize(numStrs);
+ _line++;
+ return true;
+}
+
+bool SubtitleCSVLoader::readQuotedCel(Common::String &str) {
+ assert(_contents[_readOffset] == '\"');
+ _readOffset++;
+
+ str.clear();
+ for (;;) {
+ if (_readOffset == _contents.size())
+ return false;
+
+ char nextChar = _contents[_readOffset];
+ if (nextChar == '\"') {
+ _readOffset++;
+ if (_readOffset < _contents.size()) {
+ char subsequentChar = _contents[_readOffset];
+ if (subsequentChar == '\"') {
+ str += '\"';
+ _readOffset++;
+ } else
+ break;
+ }
+ } else {
+ str += nextChar;
+ _readOffset++;
+ }
+ }
+
+ return true;
+}
+
+bool SubtitleCSVLoader::readUnquotedCel(Common::String &str) {
+ assert(_contents[_readOffset] != '\"');
+
+ str.clear();
+ for (;;) {
+ if (_readOffset == _contents.size())
+ return true;
+
+ char nextChar = _contents[_readOffset];
+ if (nextChar == ',' || nextChar == '\n' || nextChar == '\r')
+ break;
+
+ str += nextChar;
+ _readOffset++;
+ }
+
+ return true;
+}
+
+SubtitleAssetMappingTable::SubtitleAssetMappingTable() {
+}
+
+Common::ErrorCode SubtitleAssetMappingTable::load(const Common::String &filePath) {
+ Common::File f;
+ if (!f.open(Common::Path(filePath)))
+ return Common::kPathDoesNotExist;
+
+ SubtitleCSVLoader loader(&f);
+
+ Common::Array<Common::String> strs;
+ if (!loader.readLine(strs))
+ return Common::kReadingFailed;
+
+ if (strs.size() != 3 || strs[0] != "subtitle_set_id" || strs[1] != "asset_id" || strs[2] != "asset_name")
+ return Common::kReadingFailed;
+
+ while (loader.readLine(strs)) {
+ if (strs.size() == 0)
+ break;
+
+ if (strs.size() != 3)
+ return Common::kReadingFailed;
+
+ uint assetID = 0;
+ if (sscanf(strs[1].c_str(), "%u", &assetID) == 1 && assetID != 0)
+ _assetIDToSubtitleSet[assetID] = strs[0];
+
+ if (strs[2].size() > 0)
+ _assetNameToSubtitleSet[strs[2]] = strs[0];
+ }
+
+ return Common::kNoError;
+}
+
+const Common::String *SubtitleAssetMappingTable::findSubtitleSetForAssetID(uint32 assetID) const {
+ Common::HashMap<uint32, Common::String>::const_iterator it = _assetIDToSubtitleSet.find(assetID);
+ if (it == _assetIDToSubtitleSet.end())
+ return nullptr;
+ return &it->_value;
+}
+
+const Common::String *SubtitleAssetMappingTable::findSubtitleSetForAssetName(const Common::String &assetName) const {
+ Common::HashMap<Common::String, Common::String>::const_iterator it = _assetNameToSubtitleSet.find(assetName);
+ if (it == _assetNameToSubtitleSet.end())
+ return nullptr;
+ return &it->_value;
+}
+
+
+SubtitleModifierMappingTable::SubtitleModifierMappingTable() {
+}
+
+Common::ErrorCode SubtitleModifierMappingTable::load(const Common::String &filePath) {
+ Common::File f;
+ if (!f.open(Common::Path(filePath)))
+ return Common::kPathDoesNotExist;
+
+ SubtitleCSVLoader loader(&f);
+
+ Common::Array<Common::String> strs;
+ if (!loader.readLine(strs))
+ return Common::kReadingFailed;
+
+ if (strs.size() != 2 || strs[0] != "subtitle_set_id" || strs[1] != "modifier_guid")
+ return Common::kReadingFailed;
+
+ while (loader.readLine(strs)) {
+ if (strs.size() == 0)
+ break;
+
+ if (strs.size() != 2)
+ return Common::kReadingFailed;
+
+ uint modifierGUID = 0;
+ if (sscanf(strs[1].c_str(), "%u", &modifierGUID) == 1 && modifierGUID != 0)
+ _guidToSubtitleSet[modifierGUID] = strs[0];
+ else
+ return Common::kReadingFailed;
+ }
+
+ return Common::kNoError;
+}
+
+const Common::String *SubtitleModifierMappingTable::findSubtitleSetForModifierGUID(uint32 guid) const {
+ Common::HashMap<uint32, Common::String>::const_iterator it = _guidToSubtitleSet.find(guid);
+ if (it == _guidToSubtitleSet.end())
+ return nullptr;
+ return &it->_value;
+}
+
+SubtitleSpeakerTable::SubtitleSpeakerTable() {
+}
+
+Common::ErrorCode SubtitleSpeakerTable::load(const Common::String &filePath) {
+ Common::File f;
+ if (!f.open(Common::Path(filePath)))
+ return Common::kPathDoesNotExist;
+
+ SubtitleCSVLoader loader(&f);
+
+ Common::Array<Common::String> strs;
+ if (!loader.readLine(strs))
+ return Common::kReadingFailed;
+
+ if (strs.size() != 2 || strs[0] != "speaker" || strs[1] != "text")
+ return Common::kReadingFailed;
+
+ _speakers.resize(1);
+
+ while (loader.readLine(strs)) {
+ if (strs.size() == 0)
+ break;
+
+ if (strs.size() != 2)
+ return Common::kReadingFailed;
+
+ _speakerToID[strs[0]] = _speakers.size();
+ _speakers.push_back(strs[1]);
+ }
+
+ return Common::kNoError;
+}
+
+const Common::Array<Common::String> &SubtitleSpeakerTable::getSpeakers() const {
+ return _speakers;
+}
+
+uint SubtitleSpeakerTable::getSpeakerID(const Common::String &speakerName) const {
+ Common::HashMap<Common::String, uint>::const_iterator it = _speakerToID.find(speakerName);
+ if (it == _speakerToID.end())
+ return 0;
+ return it->_value;
+}
+
+
+SubtitleLineTable::SubtitleLineTable() {
+}
+
+Common::ErrorCode SubtitleLineTable::load(const Common::String &filePath, const SubtitleSpeakerTable &speakerTable) {
+ Common::File f;
+ if (!f.open(Common::Path(filePath)))
+ return Common::kPathDoesNotExist;
+
+ SubtitleCSVLoader loader(&f);
+
+ Common::Array<Common::String> strs;
+ if (!loader.readLine(strs))
+ return Common::kReadingFailed;
+
+ if (strs.size() != 7 || strs[0] != "subtitle_set_id" || strs[1] != "text" || strs[2] != "time" || strs[3] != "duration" || strs[4] != "location" || strs[5] != "speaker" || strs[6] != "is_gameplay")
+ return Common::kReadingFailed;
+
+ uint currentLine = 0;
+ while (loader.readLine(strs)) {
+ if (strs.size() == 0)
+ break;
+
+ if (strs.size() != 7)
+ return Common::kReadingFailed;
+
+ double timestamp = 0;
+ double duration = 0;
+ uint location = 0;
+ uint isGameplay = 0;
+ if (sscanf(strs[2].c_str(), "%lf", ×tamp) != 1)
+ timestamp = 0;
+
+ if (sscanf(strs[3].c_str(), "%lf", &duration) != 1)
+ duration = 0;
+
+ if (sscanf(strs[4].c_str(), "%u", &location) != 1)
+ location = 0;
+
+ if (sscanf(strs[6].c_str(), "%u", &isGameplay) != 1)
+ isGameplay = 0;
+
+ LineData lineData;
+ lineData.durationMSec = static_cast<uint32>(duration * 1000.0);
+ lineData.timeOffsetMSec = static_cast<uint32>(timestamp * 1000.0);
+ lineData.textUTF8 = strs[1];
+ lineData.location = location;
+ lineData.speakerID = speakerTable.getSpeakerID(strs[5]);
+ lineData.isGameplay = isGameplay;
+
+ LineRange &range = _lineRanges[strs[0]];
+ if (range.numLines == 0)
+ range.linesStart = currentLine;
+ else if (range.linesStart + range.numLines != currentLine) {
+ warning("Failed to load lines table, subtitle set '%s' was not contiguous", strs[0].c_str());
+ return Common::kReadingFailed;
+ }
+
+ range.numLines++;
+
+ _lines.push_back(lineData);
+
+ currentLine++;
+ }
+
+ return Common::kNoError;
+}
+
+SubtitleLineTable::LineRange::LineRange() : linesStart(0), numLines(0) {
+}
+
+
+SubtitleLineTable::LineData::LineData() : timeOffsetMSec(0), location(0), durationMSec(0), speakerID(0), isGameplay(false) {
+}
+
+const Common::Array<SubtitleLineTable::LineData> &SubtitleLineTable::getAllLines() const {
+ return _lines;
+}
+
+const SubtitleLineTable::LineRange *SubtitleLineTable::getLinesForSubtitleSetID(const Common::String &subtitleSetID) const {
+ Common::HashMap<Common::String, LineRange>::const_iterator it = _lineRanges.find(subtitleSetID);
+ if (it == _lineRanges.end())
+ return nullptr;
+ return &it->_value;
+}
+
+SubtitleRenderer::DisplayItem::DisplayItem() : expireTime(0) {
+}
+
+SubtitleDisplayItem::SubtitleDisplayItem(const Common::String &text, const Common::String &speaker, uint slot) : _slot(slot) {
+ _text = text.decode(Common::kUtf8);
+ _speaker = speaker.decode(Common::kUtf8);
+}
+
+const Common::U32String &SubtitleDisplayItem::getText() const {
+ return _text;
+}
+
+const Common::U32String &SubtitleDisplayItem::getSpeaker() const {
+ return _speaker;
+}
+
+uint SubtitleDisplayItem::getSlot() const {
+ return _slot;
+}
+
+SubtitleRenderer::SubtitleRenderer() : _nonImmediateDisappearTime(3500), _isDirty(true), _lastTime(0) {
+ Common::File fontFile;
+ const char *fontPath = "LiberationSans-Bold.ttf";
+
+ _font.reset(Graphics::loadTTFFontFromArchive(fontPath, 14, Graphics::kTTFSizeModeCell));
+
+ if (!_font)
+ warning("Couldn't open '%s', subtitles will not work", fontPath);
+}
+
+void SubtitleRenderer::addDisplayItem(const Common::SharedPtr<SubtitleDisplayItem> &item, uint duration) {
+ assert(item.get() != nullptr);
+
+ _isDirty = true;
+
+ for (DisplayItem &existingItem : _displayItems) {
+ if (existingItem.item->getSlot() == item->getSlot()) {
+ existingItem.item = item;
+ if (duration > 0)
+ existingItem.expireTime = _lastTime + duration;
+ else
+ existingItem.expireTime = 0;
+ return;
+ }
+ }
+
+ DisplayItem newItem;
+ newItem.expireTime = 0;
+ newItem.item = item;
+
+ if (duration > 0)
+ newItem.expireTime = _lastTime + duration;
+
+ _displayItems.push_back(newItem);
+}
+
+void SubtitleRenderer::removeDisplayItem(const SubtitleDisplayItem *item, bool immediately) {
+ if (item == nullptr)
+ return;
+
+ for (uint i = 0; i < _displayItems.size(); i++) {
+ DisplayItem &existingItem = _displayItems[i];
+ if (existingItem.item.get() == item) {
+ if (immediately) {
+ _displayItems.remove_at(i);
+ _isDirty = true;
+ } else {
+ if (existingItem.expireTime == 0)
+ existingItem.expireTime = _lastTime + _nonImmediateDisappearTime;
+ }
+
+ return;
+ }
+ }
+}
+
+bool SubtitleRenderer::update(uint64 currentTime) {
+ _lastTime = currentTime;
+
+ for (uint ridx = _displayItems.size(); ridx > 0; ridx--) {
+ uint i = ridx - 1;
+ DisplayItem &item = _displayItems[i];
+ if (item.expireTime != 0 && item.expireTime <= currentTime) {
+ _displayItems.remove_at(i);
+ _isDirty = true;
+ }
+ }
+
+ if (_isDirty) {
+ render();
+ _isDirty = false;
+ return true;
+ }
+
+ return false;
+}
+
+void SubtitleRenderer::composite(Window &window) const {
+ if (_surface) {
+ Graphics::ManagedSurface *windowSurf = window.getSurface().get();
+ if (windowSurf) {
+ int x = (windowSurf->w - _surface->w) / 2;
+ int y = (windowSurf->h + 300) / 2 - _surface->h;
+ windowSurf->blitFrom(*_surface, Common::Point(x, y));
+ }
+ }
+}
+
+bool SubtitleRenderer::isDirty() const {
+ return _isDirty;
+}
+
+void SubtitleRenderer::render() {
+ if (!_font)
+ return;
+
+ uint widestLine = 0;
+ uint numLines = 0;
+ for (const DisplayItem &item : _displayItems) {
+ if (item.item) {
+ const SubtitleDisplayItem &dispItem = *item.item;
+ Common::Array<Common::U32String> lines;
+
+ splitLines(dispItem.getText(), lines);
+
+ for (const Common::U32String &str : lines) {
+ uint width = _font->getStringWidth(str);
+ if (width > widestLine)
+ widestLine = width;
+ }
+
+ uint itemLines = lines.size();
+
+ const Common::U32String &speaker = item.item->getSpeaker();
+
+ if (speaker.size() > 0) {
+ itemLines++;
+ uint speakerWidth = _font->getStringWidth(speaker);
+ if (speakerWidth > widestLine)
+ widestLine = speakerWidth;
+ }
+
+ if (itemLines > numLines)
+ numLines = itemLines;
+ }
+ }
+
+ _surface.reset();
+
+ if (numLines == 0 || widestLine == 0)
+ return;
+
+ int fontHeight = _font->getFontHeight();
+
+ const int borderWidth = 1;
+ const int verticalPadding = 5;
+ const int horizontalPadding = 20;
+
+ int surfaceWidth = static_cast<int>(widestLine) + borderWidth * 2 + horizontalPadding * 2;
+ int surfaceHeight = static_cast<int>(numLines) * fontHeight + borderWidth * 2 + verticalPadding * 2;
+
+ Graphics::PixelFormat fmt = Graphics::createPixelFormat<8888>();
+ _surface.reset(new Graphics::ManagedSurface(surfaceWidth, surfaceHeight, fmt));
+
+ _surface->fillRect(Common::Rect(0, 0, surfaceWidth, surfaceHeight), fmt.RGBToColor(0, 0, 0));
+
+ for (int drawPass = 0; drawPass < 2; drawPass++) {
+ for (const DisplayItem &item : _displayItems) {
+ if (item.item) {
+ const SubtitleDisplayItem &dispItem = *item.item;
+ Common::Array<Common::U32String> lines;
+
+ const Common::U32String &itemText = dispItem.getText();
+
+ int speakerLine = 0;
+ for (uint i = 0; i < itemText.size(); i++) {
+ if (itemText[i] == '\\')
+ speakerLine++;
+ else
+ break;
+ }
+
+ splitLines(itemText, lines);
+
+ for (const Common::U32String &str : lines) {
+ uint width = _font->getStringWidth(str);
+ if (width > widestLine)
+ widestLine = width;
+ }
+
+ const Common::U32String &speaker = item.item->getSpeaker();
+
+ int textStartLine = 0;
+ if (speaker.size() > 0) {
+ textStartLine++;
+
+ int speakerWidth = _font->getStringWidth(speaker);
+
+ int startX = (surfaceWidth - speakerWidth) / 2;
+
+ uint32 drawColor = 0;
+ if (drawPass == 0)
+ drawColor = fmt.RGBToColor(255, 0, 0);
+ else
+ drawColor = fmt.RGBToColor(255, 255, 127);
+
+ _font->drawString(_surface.get(), speaker, startX, speakerLine * fontHeight + verticalPadding + borderWidth, speakerWidth, drawColor);
+ }
+
+ for (uint lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
+ const Common::U32String &line = lines[lineIndex];
+
+ int lineWidth = _font->getStringWidth(line);
+
+ int startX = (surfaceWidth - lineWidth) / 2;
+
+ uint32 drawColor = 0;
+ if (drawPass == 0)
+ drawColor = fmt.RGBToColor(255, 0, 0);
+ else
+ drawColor = fmt.RGBToColor(255, 255, 255);
+
+ _font->drawString(_surface.get(), line, startX, (textStartLine + static_cast<int>(lineIndex)) * fontHeight + verticalPadding + borderWidth, lineWidth, drawColor);
+ }
+ }
+ }
+
+ if (drawPass == 0) {
+ int w = _surface->w;
+ int h = _surface->h;
+
+ // Horizontal pass (max r -> g)
+ for (int y = borderWidth; y < h - borderWidth; y++) {
+ uint32 *rowPixels = static_cast<uint32 *>(_surface->getBasePtr(0, y));
+
+ for (int x = borderWidth; x < w - borderWidth; x++) {
+ uint8 r, g, b, a;
+ uint8 brightest = 0;
+
+ for (int kxo = -borderWidth; kxo < borderWidth; kxo++) {
+ fmt.colorToARGB(rowPixels[x + kxo], a, r, g, b);
+
+ if (r > brightest)
+ brightest = r;
+ }
+
+ fmt.colorToARGB(rowPixels[x], a, r, g, b);
+ rowPixels[x] = fmt.ARGBToColor(a, r, brightest, b);
+ }
+ }
+
+ // Vertical pass (max g -> b)
+ int pitch = _surface->pitch;
+ for (int x = borderWidth; x < w - borderWidth; x++) {
+ char *basePixelPtr = static_cast<char *>(_surface->getBasePtr(x, 0));
+
+ for (int y = borderWidth; y < h - borderWidth; y++) {
+ uint8 r, g, b, a;
+ uint8 brightest = 0;
+
+ for (int kyo = -borderWidth; kyo < borderWidth; kyo++) {
+ uint32 *offsetPxPtr = reinterpret_cast<uint32 *>(basePixelPtr + (y + kyo) * pitch);
+ fmt.colorToARGB(*offsetPxPtr, a, r, g, b);
+
+ if (g > brightest)
+ brightest = g;
+ }
+
+ uint32 *pxPtr = reinterpret_cast<uint32 *>(basePixelPtr + y * pitch);
+ fmt.colorToARGB(*pxPtr, a, r, g, b);
+ *pxPtr = fmt.ARGBToColor(a, r, g, brightest);
+ }
+ }
+
+ // Shadow backdrop pass
+ for (int y = 0; y < h; y++) {
+ uint32 *rowPixels = static_cast<uint32 *>(_surface->getBasePtr(0, y));
+
+ for (int x = 0; x < w; x++) {
+ uint8 a, r, g, b;
+ fmt.colorToARGB(rowPixels[x], a, r, g, b);
+
+ uint8 minGrayAlpha = 224;
+ uint8 grayAlpha = (((256 - minGrayAlpha) * b) >> 8) + minGrayAlpha;
+
+ rowPixels[x] = fmt.ARGBToColor(grayAlpha, 0, 0, 0);
+ }
+ }
+ }
+ }
+}
+
+void SubtitleRenderer::splitLines(const Common::U32String &str, Common::Array<Common::U32String> &outLines) {
+ uint32 splitScanStart = 0;
+ while (splitScanStart < str.size()) {
+ size_t splitLoc = str.find('\\', splitScanStart);
+ if (splitLoc == Common::U32String::npos)
+ break;
+
+ outLines.push_back(str.substr(splitScanStart, splitLoc - splitScanStart));
+ splitScanStart = static_cast<uint32>(splitLoc + 1);
+ }
+
+ outLines.push_back(str.substr(splitScanStart));
+}
+
+
+SubtitlePlayer::SubtitlePlayer(Runtime *runtime, const Common::String &subtitleSetID, const SubtitleTables &tables) : _runtime(runtime) {
+ const SubtitleLineTable::LineRange *lineRange = tables.lines->getLinesForSubtitleSetID(subtitleSetID);
+ if (lineRange) {
+ _lineRange = *lineRange;
+ } else {
+ warning("Subtitle set '%s' was defined, but no lines were defined", subtitleSetID.c_str());
+ return;
+ }
+
+ _speakers = tables.speakers;
+ _lines = tables.lines;
+}
+
+SubtitlePlayer::~SubtitlePlayer() {
+ stop();
+}
+
+void SubtitlePlayer::update(uint64 prevTime, uint64 newTime) {
+ if (!_lineRange.numLines)
+ return;
+
+ const Common::Array<SubtitleLineTable::LineData> &allLines = _lines->getAllLines();
+
+ uint numLines = _lineRange.numLines;
+ for (uint i = 0; i < _lineRange.numLines; i++) {
+ const SubtitleLineTable::LineData &line = allLines[_lineRange.linesStart + i];
+ if (line.timeOffsetMSec >= prevTime && line.timeOffsetMSec < newTime)
+ triggerSubtitleLine(line);
+ }
+}
+
+void SubtitlePlayer::stop() {
+ SubtitleRenderer *renderer = _runtime->getSubtitleRenderer().get();
+ if (renderer) {
+ for (const Common::SharedPtr<SubtitleDisplayItem> &item : _items)
+ renderer->removeDisplayItem(item.get(), false);
+ }
+ _items.clear();
+}
+
+void SubtitlePlayer::triggerSubtitleLine(const SubtitleLineTable::LineData &line) {
+ SubtitleRenderer *renderer = _runtime->getSubtitleRenderer().get();
+ if (renderer) {
+ Common::SharedPtr<SubtitleDisplayItem> dispItem(new SubtitleDisplayItem(line.textUTF8, _speakers->getSpeakers()[line.speakerID], line.location));
+ for (uint i = 0; i < _items.size(); i++) {
+ if (_items[i]->getSlot() == line.location) {
+ renderer->removeDisplayItem(_items[i].get(), true);
+ _items.remove_at(i);
+ break;
+ }
+ }
+
+ renderer->addDisplayItem(dispItem, line.durationMSec);
+ _items.push_back(dispItem);
+ }
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/subtitles.h b/engines/mtropolis/subtitles.h
new file mode 100644
index 00000000000..d226bb59fc6
--- /dev/null
+++ b/engines/mtropolis/subtitles.h
@@ -0,0 +1,191 @@
+/* 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/>.
+ *
+ */
+
+#ifndef MTROPOLIS_SUBTITLES_H
+#define MTROPOLIS_SUBTITLES_H
+
+#include "common/array.h"
+#include "common/error.h"
+#include "common/str.h"
+#include "common/hash-str.h"
+#include "common/hashmap.h"
+#include "common/ptr.h"
+
+namespace Graphics {
+
+class Font;
+class ManagedSurface;
+
+} // End of namespace Graphics
+
+namespace MTropolis {
+
+class Runtime;
+class Window;
+
+class SubtitleAssetMappingTable {
+public:
+ SubtitleAssetMappingTable();
+
+ Common::ErrorCode load(const Common::String &filePath);
+
+ const Common::String *findSubtitleSetForAssetID(uint32 assetID) const;
+ const Common::String *findSubtitleSetForAssetName(const Common::String &assetName) const;
+
+private:
+ Common::HashMap<uint32, Common::String> _assetIDToSubtitleSet;
+ Common::HashMap<Common::String, Common::String> _assetNameToSubtitleSet;
+};
+
+class SubtitleModifierMappingTable {
+public:
+ SubtitleModifierMappingTable();
+
+ Common::ErrorCode load(const Common::String &filePath);
+
+ const Common::String *findSubtitleSetForModifierGUID(uint32 guid) const;
+
+private:
+ Common::HashMap<uint32, Common::String> _guidToSubtitleSet;
+};
+
+class SubtitleSpeakerTable {
+public:
+ SubtitleSpeakerTable();
+
+ Common::ErrorCode load(const Common::String &filePath);
+
+ const Common::Array<Common::String> &getSpeakers() const;
+ uint getSpeakerID(const Common::String &speakerName) const;
+
+private:
+ Common::Array<Common::String> _speakers;
+ Common::HashMap<Common::String, uint> _speakerToID;
+};
+
+class SubtitleLineTable {
+public:
+ SubtitleLineTable();
+
+ Common::ErrorCode load(const Common::String &filePath, const SubtitleSpeakerTable &speakerTable);
+
+ struct LineRange {
+ LineRange();
+
+ uint linesStart;
+ uint numLines;
+ };
+
+ struct LineData {
+ LineData();
+
+ uint32 timeOffsetMSec;
+ uint location;
+ uint durationMSec;
+ Common::String textUTF8;
+ uint speakerID;
+ bool isGameplay;
+ };
+
+ const Common::Array<LineData> &getAllLines() const;
+ const LineRange *getLinesForSubtitleSetID(const Common::String &subtitleSetID) const;
+
+private:
+ Common::Array<LineData> _lines;
+ Common::HashMap<Common::String, LineRange> _lineRanges;
+};
+
+
+struct SubtitleTables {
+ Common::SharedPtr<SubtitleAssetMappingTable> assetMapping;
+ Common::SharedPtr<SubtitleModifierMappingTable> modifierMapping;
+ Common::SharedPtr<SubtitleSpeakerTable> speakers;
+ Common::SharedPtr<SubtitleLineTable> lines;
+};
+
+class SubtitleDisplayItem {
+public:
+ SubtitleDisplayItem(const Common::String &text, const Common::String &speaker, uint slot);
+
+ const Common::U32String &getText() const;
+ const Common::U32String &getSpeaker() const;
+ uint getSlot() const;
+
+private:
+ Common::U32String _text;
+ Common::U32String _speaker;
+ uint _slot;
+};
+
+class SubtitleRenderer {
+public:
+ SubtitleRenderer();
+
+ void addDisplayItem(const Common::SharedPtr<SubtitleDisplayItem> &item, uint duration);
+ void removeDisplayItem(const SubtitleDisplayItem *item, bool immediately);
+
+ bool update(uint64 currentTime); // Updates the subtitle player, returns true if any changes occurred since last update
+
+ void composite(Window &window) const;
+ bool isDirty() const;
+
+private:
+ struct DisplayItem {
+ DisplayItem();
+
+ Common::SharedPtr<SubtitleDisplayItem> item;
+ uint64 expireTime;
+ };
+
+ void render();
+ static void splitLines(const Common::U32String &str, Common::Array<Common::U32String> &outLines);
+
+ Common::Array<DisplayItem> _displayItems;
+ Common::SharedPtr<Graphics::ManagedSurface> _surface;
+ Common::SharedPtr<Graphics::Font> _font;
+ uint64 _lastTime;
+ uint _nonImmediateDisappearTime;
+ bool _isDirty;
+};
+
+class SubtitlePlayer {
+public:
+ SubtitlePlayer(Runtime *runtime, const Common::String &subtitleSetID, const SubtitleTables &tables);
+ ~SubtitlePlayer();
+
+ void update(uint64 prevTime, uint64 newTime);
+ void stop();
+
+private:
+ void triggerSubtitleLine(const SubtitleLineTable::LineData &line);
+
+ Common::Array<Common::SharedPtr<SubtitleDisplayItem> > _items;
+
+ Common::SharedPtr<SubtitleSpeakerTable> _speakers;
+ Common::SharedPtr<SubtitleLineTable> _lines;
+
+ SubtitleLineTable::LineRange _lineRange;
+ Runtime *_runtime;
+};
+
+} // End of namespace MTropolis
+
+#endif
More information about the Scummvm-git-logs
mailing list