/* 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 "common/formats/winexe.h"
#include "common/config-manager.h"
#include "common/endian.h"
#include "common/events.h"
#include "common/file.h"
#include "common/math.h"
#include "common/ptr.h"
#include "common/random.h"
#include "common/savefile.h"
#include "common/system.h"
#include "common/stream.h"
#include "graphics/cursorman.h"
#include "graphics/font.h"
#include "graphics/fonts/ttf.h"
#include "graphics/fontman.h"
#include "graphics/wincursor.h"
#include "graphics/managed_surface.h"
#include "image/bmp.h"
#include "audio/decoders/wave.h"
#include "audio/decoders/vorbis.h"
#include "audio/audiostream.h"
#include "video/avi_decoder.h"
#include "gui/message.h"
#include "vcruise/audio_player.h"
#include "vcruise/circuitpuzzle.h"
#include "vcruise/sampleloop.h"
#include "vcruise/menu.h"
#include "vcruise/runtime.h"
#include "vcruise/script.h"
#include "vcruise/textparser.h"
#include "vcruise/vcruise.h"
namespace VCruise {
struct CodePageGuess {
Common::CodePage codePage;
Runtime::CharSet charSet;
const char *searchString;
const char *languageName;
};
class RuntimeMenuInterface : public MenuInterface {
public:
explicit RuntimeMenuInterface(Runtime *runtime);
void commitRect(const Common::Rect &rect) const override;
bool popOSEvent(OSEvent &evt) const override;
Graphics::Surface *getUIGraphic(uint index) const override;
Graphics::ManagedSurface *getMenuSurface() const override;
bool hasDefaultSave() const override;
bool hasAnySave() const override;
bool isInGame() const override;
Common::Point getMouseCoordinate() const override;
void restartGame() const override;
void goToCredits() const override;
void changeMenu(MenuPage *newPage) const override;
void quitGame() const override;
void quitToMenu() const override;
bool canSave() const override;
bool reloadFromCheckpoint() const override;
void setMusicMute(bool muted) const override;
void drawLabel(Graphics::ManagedSurface *surface, const Common::String &labelID, const Common::Rect &contentRect) const override;
private:
Runtime *_runtime;
};
RuntimeMenuInterface::RuntimeMenuInterface(Runtime *runtime) : _runtime(runtime) {
}
void RuntimeMenuInterface::commitRect(const Common::Rect &rect) const {
_runtime->commitSectionToScreen(_runtime->_fullscreenMenuSection, rect);
}
bool RuntimeMenuInterface::popOSEvent(OSEvent &evt) const {
return _runtime->popOSEvent(evt);
}
Graphics::Surface *RuntimeMenuInterface::getUIGraphic(uint index) const {
if (index >= _runtime->_uiGraphics.size())
return nullptr;
return _runtime->_uiGraphics[index].get();
}
Graphics::ManagedSurface *RuntimeMenuInterface::getMenuSurface() const {
return _runtime->_fullscreenMenuSection.surf.get();
}
bool RuntimeMenuInterface::hasDefaultSave() const {
return static_cast(g_engine)->hasDefaultSave();
}
bool RuntimeMenuInterface::hasAnySave() const {
return static_cast(g_engine)->hasAnySave();
}
bool RuntimeMenuInterface::isInGame() const {
return _runtime->_isInGame;
}
Common::Point RuntimeMenuInterface::getMouseCoordinate() const {
return _runtime->_mousePos;
}
void RuntimeMenuInterface::restartGame() const {
Common::SharedPtr snapshot = _runtime->generateNewGameSnapshot();
_runtime->_mostRecentValidSaveState = snapshot;
_runtime->restoreSaveGameSnapshot();
}
void RuntimeMenuInterface::goToCredits() const {
_runtime->clearScreen();
// In Schizm, exiting credits doesn't transition to the main menu screen,
// so we must force a screen change for when the user clicks Credits after
// leaving the credits screen
_runtime->_forceScreenChange = true;
if (_runtime->_gameID == GID_REAH)
_runtime->changeToScreen(40, 0xa1);
else if (_runtime->_gameID == GID_SCHIZM)
_runtime->changeToScreen(1, 0xb2);
else
error("Don't know what screen to go to for credits for this game");
}
void RuntimeMenuInterface::changeMenu(MenuPage *newPage) const {
_runtime->changeToMenuPage(newPage);
}
void RuntimeMenuInterface::quitToMenu() const {
_runtime->quitToMenu();
}
void RuntimeMenuInterface::quitGame() const {
Common::Event evt;
evt.type = Common::EVENT_QUIT;
g_engine->getEventManager()->pushEvent(evt);
}
bool RuntimeMenuInterface::canSave() const {
return _runtime->canSave(false);
}
bool RuntimeMenuInterface::reloadFromCheckpoint() const {
if (!_runtime->canSave(false))
return false;
_runtime->restoreSaveGameSnapshot();
return true;
}
void RuntimeMenuInterface::setMusicMute(bool muted) const {
_runtime->setMusicMute(muted);
}
void RuntimeMenuInterface::drawLabel(Graphics::ManagedSurface *surface, const Common::String &labelID, const Common::Rect &contentRect) const {
_runtime->drawLabel(surface, labelID, contentRect);
}
AnimationDef::AnimationDef() : animNum(0), firstFrame(0), lastFrame(0) {
}
InteractionDef::InteractionDef() : objectType(0), interactionID(0) {
}
void MapDef::clear() {
for (uint screen = 0; screen < kNumScreens; screen++)
for (uint direction = 0; direction < kNumDirections; direction++)
screenDirections[screen][direction].reset();
}
const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direction) {
if (screen < kFirstScreen)
return nullptr;
screen -= kFirstScreen;
if (screen >= kNumScreens)
return nullptr;
return screenDirections[screen][direction].get();
}
ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false), isEntryScript(false), puzzleWasSet(false),
panInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
}
OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast(0)), keymappedEvent(kKeymappedEventNone), timestamp(0) {
}
void Runtime::RenderSection::init(const Common::Rect ¶mRect, const Graphics::PixelFormat &fmt) {
rect = paramRect;
surf.reset(new Graphics::ManagedSurface(paramRect.width(), paramRect.height(), fmt));
surf->fillRect(Common::Rect(0, 0, surf->w, surf->h), 0xffffffff);
}
Runtime::StackValue::ValueUnion::ValueUnion() {
}
Runtime::StackValue::ValueUnion::ValueUnion(StackInt_t iVal) : i(iVal) {
}
Runtime::StackValue::ValueUnion::ValueUnion(const Common::String &strVal) : s(strVal) {
}
Runtime::StackValue::ValueUnion::ValueUnion(Common::String &&strVal) : s(Common::move(strVal)) {
}
Runtime::StackValue::ValueUnion::~ValueUnion() {
}
Runtime::StackValue::StackValue() : type(kNumber), value(0) {
new (&value) ValueUnion(0);
}
Runtime::StackValue::StackValue(const StackValue &other) : type(kNumber), value(0) {
(*this) = other;
}
Runtime::StackValue::StackValue(StackValue &&other) : type(kNumber), value(0) {
(*this) = Common::move(other);
}
Runtime::StackValue::StackValue(StackInt_t i) : type(kNumber), value(i) {
}
Runtime::StackValue::StackValue(const Common::String &str) : type(kString), value(str) {
}
Runtime::StackValue::StackValue(Common::String &&str) : type(kString), value(Common::move(str)) {
}
Runtime::StackValue::~StackValue() {
value.~ValueUnion();
}
Runtime::StackValue &Runtime::StackValue::operator=(const StackValue &other) {
value.~ValueUnion();
if (other.type == StackValue::kNumber)
new (&value) ValueUnion(other.value.i);
if (other.type == StackValue::kString)
new (&value) ValueUnion(other.value.s);
type = other.type;
return *this;
}
Runtime::StackValue &Runtime::StackValue::operator=(StackValue &&other) {
value.~ValueUnion();
if (other.type == StackValue::kNumber)
new (&value) ValueUnion(other.value.i);
if (other.type == StackValue::kString)
new (&value) ValueUnion(Common::move(other.value.s));
type = other.type;
return *this;
}
Runtime::CallStackFrame::CallStackFrame() : _nextInstruction(0) {
}
Runtime::Gyro::Gyro() {
reset();
}
void Runtime::Gyro::reset() {
currentState = 0;
requiredState = 0;
wrapAround = false;
requireState = false;
numPreviousStates = 0;
numPreviousStatesRequired = 0;
for (uint i = 0; i < kMaxPreviousStates; i++) {
previousStates[i] = 0;
requiredPreviousStates[i] = 0;
}
}
void Runtime::Gyro::logState() {
if (numPreviousStatesRequired > 0) {
if (numPreviousStates < numPreviousStatesRequired)
numPreviousStates++;
else {
for (uint i = 1; i < numPreviousStates; i++)
previousStates[i - 1] = previousStates[i];
}
previousStates[numPreviousStates - 1] = currentState;
}
}
Runtime::GyroState::GyroState() {
reset();
}
void Runtime::GyroState::reset() {
for (uint i = 0; i < kNumGyros; i++)
gyros[i].reset();
completeInteraction = 0;
failureInteraction = 0;
frameSeparation = 1;
activeGyro = 0;
dragMargin = 0;
maxValue = 0;
negAnim = AnimationDef();
posAnim = AnimationDef();
isVertical = false;
dragBasePoint = Common::Point(0, 0);
dragBaseState = 0;
dragCurrentState = 0;
isWaitingForAnimation = false;
}
Runtime::SubtitleDef::SubtitleDef() : color{0, 0, 0}, unknownValue1(0), durationInDeciseconds(0) {
}
SfxPlaylistEntry::SfxPlaylistEntry() : frame(0), balance(0), volume(0), isUpdate(false) {
}
SfxPlaylist::SfxPlaylist() {
}
SfxData::SfxData() {
}
void SfxData::reset() {
playlists.clear();
sounds.clear();
}
void SfxData::load(Common::SeekableReadStream &stream, Audio::Mixer *mixer) {
Common::INIFile iniFile;
iniFile.allowNonEnglishCharacters();
iniFile.suppressValuelessLineWarning();
if (!iniFile.loadFromStream(stream))
warning("SfxData::load failed to parse INI file");
const Common::INIFile::Section *samplesSection = nullptr;
const Common::INIFile::Section *playlistsSection = nullptr;
const Common::INIFile::Section *presetsSection = nullptr;
Common::INIFile::SectionList sections = iniFile.getSections(); // Why does this require a copy? Sigh.
for (const Common::INIFile::Section §ion : sections) {
if (section.name == "samples")
samplesSection = §ion;
else if (section.name == "playlists")
playlistsSection = §ion;
else if (section.name == "presets")
presetsSection = §ion;
}
Common::HashMap presets;
if (presetsSection) {
for (const Common::INIFile::KeyValue &keyValue : presetsSection->keys)
presets.setVal(keyValue.key, keyValue.value);
}
if (samplesSection) {
for (const Common::INIFile::KeyValue &keyValue : samplesSection->keys) {
Common::SharedPtr sample(new SfxSound());
// Fix up the path delimiter
Common::String sfxPath = keyValue.value;
for (char &c : sfxPath) {
if (c == '\\')
c = '/';
}
size_t commentPos = sfxPath.find(';');
if (commentPos != Common::String::npos) {
sfxPath = sfxPath.substr(0, commentPos);
sfxPath.trim();
}
sfxPath = Common::String("Sfx/") + sfxPath;
Common::File f;
if (!f.open(sfxPath)) {
warning("SfxData::load: Could not open sample file '%s'", sfxPath.c_str());
continue;
}
int64 size = f.size();
if (size <= 0 || size > 0x1fffffffu) {
warning("SfxData::load: File is oversized for some reason");
continue;
}
sample->soundData.resize(static_cast(size));
if (f.read(&sample->soundData[0], static_cast(size)) != size) {
warning("SfxData::load: Couldn't read file");
continue;
}
sample->memoryStream.reset(new Common::MemoryReadStream(&sample->soundData[0], static_cast(size)));
sample->audioStream.reset(Audio::makeWAVStream(sample->memoryStream.get(), DisposeAfterUse::NO));
sample->audioPlayer.reset(new AudioPlayer(mixer, sample->audioStream, Audio::Mixer::kSFXSoundType));
this->sounds[keyValue.key] = sample;
}
}
if (playlistsSection) {
Common::SharedPtr playlist;
for (const Common::INIFile::KeyValue &keyValue : playlistsSection->keys) {
const Common::String &baseKey = keyValue.key;
// Strip inline comments
uint keyValidLength = 0;
for (uint i = 0; i < baseKey.size(); i++) {
char c = baseKey[i];
if ((c & 0x80) == 0 && ((c & 0x7f) <= ' '))
continue;
if (c == ';')
break;
keyValidLength = i + 1;
}
Common::String key = baseKey.substr(0, keyValidLength);
if (key.size() == 0)
continue;
if (key.size() >= 2 && key.firstChar() == '\"' && key.lastChar() == '\"') {
if (!playlist) {
warning("Found playlist entry outside of a playlist");
continue;
}
Common::String workKey = key.substr(1, key.size() - 2);
Common::Array tokens;
for (;;) {
uint32 spaceSpanStart = workKey.find(' ');
if (spaceSpanStart == Common::String::npos) {
tokens.push_back(workKey);
break;
}
uint32 spaceSpanEnd = spaceSpanStart;
while (spaceSpanEnd < workKey.size() && workKey[spaceSpanEnd] == ' ')
spaceSpanEnd++;
tokens.push_back(workKey.substr(0, spaceSpanStart));
workKey = workKey.substr(spaceSpanEnd, workKey.size() - spaceSpanEnd);
}
// Strip leading and trailing spaces
while (tokens.size() > 0) {
if (tokens[0].empty()) {
tokens.remove_at(0);
continue;
}
uint lastIndex = tokens.size() - 1;
if (tokens[lastIndex].empty()) {
tokens.remove_at(lastIndex);
continue;
}
break;
}
if (tokens.size() != 4) {
warning("Found unusual playlist entry: %s", key.c_str());
continue;
}
if (!presets.empty()) {
for (uint tokenIndex = 0; tokenIndex < tokens.size(); tokenIndex++) {
// Ignore presets for the sound name. This fixes some breakage in e.g. Anim0134.sfx using elevator as both a sample and preset.
if (tokenIndex == 1)
continue;
Common::String &tokenRef = tokens[tokenIndex];
Common::HashMap::const_iterator presetIt = presets.find(tokenRef);
if (presetIt != presets.end())
tokenRef = presetIt->_value;
}
}
unsigned int frameNum = 0;
int balance = 0;
int volume = 0;
if (!sscanf(tokens[0].c_str(), "%u", &frameNum) || !sscanf(tokens[2].c_str(), "%i", &balance) || !sscanf(tokens[3].c_str(), "%i", &volume)) {
warning("Malformed playlist entry: %s", key.c_str());
continue;
}
bool isUpdate = false;
Common::String soundName = tokens[1];
if (soundName.size() >= 1 && soundName[0] == '*') {
soundName = soundName.substr(1);
isUpdate = true;
}
SoundMap_t::const_iterator soundIt = this->sounds.find(soundName);
if (soundIt == this->sounds.end()) {
warning("Playlist entry referenced non-existent sound: %s", soundName.c_str());
continue;
}
SfxPlaylistEntry plEntry;
plEntry.balance = balance;
plEntry.frame = frameNum;
plEntry.volume = volume;
plEntry.sample = soundIt->_value;
plEntry.isUpdate = isUpdate;
playlist->entries.push_back(plEntry);
} else {
playlist.reset(new SfxPlaylist());
this->playlists[key] = playlist;
}
}
}
}
SoundCache::SoundCache() : isLoopActive(false) {
}
SoundCache::~SoundCache() {
// Dispose player first so playback stops
this->player.reset();
// Dispose loopingStream before stream because stream is not refcounted by loopingStream so we need to avoid late free
this->loopingStream.reset();
this->stream.reset();
}
SoundInstance::SoundInstance()
: id(0), rampStartVolume(0), rampEndVolume(0), rampRatePerMSec(0), rampStartTime(0), rampTerminateOnCompletion(false),
volume(0), balance(0), effectiveBalance(0), effectiveVolume(0), is3D(false), isLooping(false), isSpeech(false), restartWhenAudible(false), tryToLoopWhenRestarted(false),
x(0), y(0), startTime(0), endTime(0), duration(0) {
}
SoundInstance::~SoundInstance() {
}
RandomAmbientSound::RandomAmbientSound() : volume(0), balance(0), frequency(0), sceneChangesRemaining(0) {
}
void RandomAmbientSound::write(Common::WriteStream *stream) const {
stream->writeUint32BE(name.size());
stream->writeString(name);
stream->writeSint32BE(volume);
stream->writeSint32BE(balance);
stream->writeUint32BE(frequency);
stream->writeUint32BE(sceneChangesRemaining);
}
void RandomAmbientSound::read(Common::ReadStream *stream) {
uint nameLen = stream->readUint32BE();
if (stream->eos() || stream->err())
nameLen = 0;
name = stream->readString(0, nameLen);
volume = stream->readSint32BE();
balance = stream->readSint32BE();
frequency = stream->readUint32BE();
sceneChangesRemaining = stream->readUint32BE();
}
TriggeredOneShot::TriggeredOneShot() : soundID(0), uniqueSlot(0) {
}
bool TriggeredOneShot::operator==(const TriggeredOneShot &other) const {
return soundID == other.soundID && uniqueSlot == other.uniqueSlot;
}
bool TriggeredOneShot::operator!=(const TriggeredOneShot &other) const {
return !((*this) == other);
}
void TriggeredOneShot::write(Common::WriteStream *stream) const {
stream->writeUint32BE(soundID);
stream->writeUint32BE(uniqueSlot);
}
void TriggeredOneShot::read(Common::ReadStream *stream) {
soundID = stream->readUint32BE();
uniqueSlot = stream->readUint32BE();
}
ScoreSectionDef::ScoreSectionDef() : volumeOrDurationInSeconds(0) {
}
StartConfigDef::StartConfigDef() : disc(0), room(0), screen(0), direction(0) {
}
StaticAnimParams::StaticAnimParams() : initialDelay(0), repeatDelay(0), lockInteractions(false) {
}
void StaticAnimParams::write(Common::WriteStream *stream) const {
stream->writeUint32BE(initialDelay);
stream->writeUint32BE(repeatDelay);
stream->writeByte(lockInteractions ? 1 : 0);
}
void StaticAnimParams::read(Common::ReadStream *stream) {
initialDelay = stream->readUint32BE();
repeatDelay = stream->readUint32BE();
lockInteractions = (stream->readByte() != 0);
}
StaticAnimation::StaticAnimation() : currentAlternation(0), nextStartTime(0) {
}
FrameData::FrameData() : areaID{0, 0, 0, 0}, areaFrameIndex(0), frameIndex(0), frameType(0), roomNumber(0) {
}
FrameData2::FrameData2() : x(0), y(0), angle(0), frameNumberInArea(0), unknown(0) {
}
SoundParams3D::SoundParams3D() : minRange(0), maxRange(0), unknownRange(0) {
}
void SoundParams3D::write(Common::WriteStream *stream) const {
stream->writeUint32BE(minRange);
stream->writeUint32BE(maxRange);
stream->writeUint32BE(unknownRange);
}
void SoundParams3D::read(Common::ReadStream *stream) {
minRange = stream->readUint32BE();
maxRange = stream->readUint32BE();
unknownRange = stream->readUint32BE();
}
InventoryItem::InventoryItem() : itemID(0), highlighted(false) {
}
Fraction::Fraction() : numerator(0), denominator(1) {
}
Fraction::Fraction(uint pNumerator, uint pDenominator) : numerator(pNumerator), denominator(pDenominator) {
}
SaveGameSwappableState::InventoryItem::InventoryItem() : itemID(0), highlighted(false) {
}
void SaveGameSwappableState::InventoryItem::write(Common::WriteStream *stream) const {
stream->writeUint32BE(itemID);
stream->writeByte(highlighted ? 1 : 0);
}
void SaveGameSwappableState::InventoryItem::read(Common::ReadStream *stream) {
itemID = stream->readUint32BE();
highlighted = (stream->readByte() != 0);
}
SaveGameSwappableState::Sound::Sound() : id(0), volume(0), balance(0), is3D(false), isLooping(false), tryToLoopWhenRestarted(false), isSpeech(false), x(0), y(0) {
}
void SaveGameSwappableState::Sound::write(Common::WriteStream *stream) const {
stream->writeUint32BE(name.size());
stream->writeString(name);
stream->writeUint32BE(id);
stream->writeSint32BE(volume);
stream->writeSint32BE(balance);
stream->writeByte(is3D ? 1 : 0);
stream->writeByte(isLooping ? 1 : 0);
stream->writeByte(tryToLoopWhenRestarted ? 1 : 0);
stream->writeByte(isSpeech ? 1 : 0);
stream->writeSint32BE(x);
stream->writeSint32BE(y);
params3D.write(stream);
}
void SaveGameSwappableState::Sound::read(Common::ReadStream *stream, uint saveGameVersion) {
uint nameLen = stream->readUint32BE();
if (stream->eos() || stream->err() || nameLen > 256)
nameLen = 0;
name = stream->readString(0, nameLen);
id = stream->readUint32BE();
volume = stream->readSint32BE();
balance = stream->readSint32BE();
is3D = (stream->readByte() != 0);
isLooping = (stream->readByte() != 0);
if (saveGameVersion >= 8)
tryToLoopWhenRestarted = (stream->readByte() != 0);
else
tryToLoopWhenRestarted = false;
isSpeech = (stream->readByte() != 0);
x = stream->readSint32BE();
y = stream->readSint32BE();
params3D.read(stream);
}
SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), havePendingPostSwapScreenReset(false),
musicTrack(0), musicVolume(100), musicActive(true), musicMuteDisabled(false), animVolume(100),
loadedAnimation(0), animDisplayingFrame(0) {
}
SaveGameSnapshot::SaveGameSnapshot() : hero(0), swapOutRoom(0), swapOutScreen(0), swapOutDirection(0),
escOn(false), numStates(1), listenerX(0), listenerY(0), listenerAngle(0) {
}
void SaveGameSnapshot::write(Common::WriteStream *stream) const {
stream->writeUint32BE(kSaveGameIdentifier);
stream->writeUint32BE(kSaveGameCurrentVersion);
stream->writeUint32BE(numStates);
for (uint sti = 0; sti < numStates; sti++) {
stream->writeUint32BE(states[sti]->roomNumber);
stream->writeUint32BE(states[sti]->screenNumber);
stream->writeUint32BE(states[sti]->direction);
stream->writeByte(states[sti]->havePendingPostSwapScreenReset ? 1 : 0);
}
stream->writeUint32BE(hero);
stream->writeUint32BE(swapOutRoom);
stream->writeUint32BE(swapOutScreen);
stream->writeUint32BE(swapOutDirection);
stream->writeByte(escOn ? 1 : 0);
for (uint sti = 0; sti < numStates; sti++) {
stream->writeSint32BE(states[sti]->musicTrack);
stream->writeSint32BE(states[sti]->musicVolume);
writeString(stream, states[sti]->scoreTrack);
writeString(stream, states[sti]->scoreSection);
stream->writeByte(states[sti]->musicActive ? 1 : 0);
stream->writeByte(states[sti]->musicMuteDisabled ? 1 : 0);
stream->writeUint32BE(states[sti]->loadedAnimation);
stream->writeUint32BE(states[sti]->animDisplayingFrame);
stream->writeSint32BE(states[sti]->animVolume);
}
pendingStaticAnimParams.write(stream);
pendingSoundParams3D.write(stream);
stream->writeSint32BE(listenerX);
stream->writeSint32BE(listenerY);
stream->writeSint32BE(listenerAngle);
for (uint sti = 0; sti < numStates; sti++) {
stream->writeUint32BE(states[sti]->inventory.size());
stream->writeUint32BE(states[sti]->sounds.size());
}
stream->writeUint32BE(triggeredOneShots.size());
stream->writeUint32BE(sayCycles.size());
for (uint sti = 0; sti < numStates; sti++)
stream->writeUint32BE(states[sti]->randomAmbientSounds.size());
stream->writeUint32BE(variables.size());
stream->writeUint32BE(timers.size());
for (uint sti = 0; sti < numStates; sti++) {
for (const SaveGameSwappableState::InventoryItem &invItem : states[sti]->inventory)
invItem.write(stream);
for (const SaveGameSwappableState::Sound &sound : states[sti]->sounds)
sound.write(stream);
}
for (const TriggeredOneShot &triggeredOneShot : triggeredOneShots)
triggeredOneShot.write(stream);
for (const Common::HashMap::Node &cycle : sayCycles) {
stream->writeUint32BE(cycle._key);
stream->writeUint32BE(cycle._value);
}
for (uint sti = 0; sti < numStates; sti++) {
for (const RandomAmbientSound &randomAmbientSound : states[sti]->randomAmbientSounds)
randomAmbientSound.write(stream);
}
for (const Common::HashMap::Node &var : variables) {
stream->writeUint32BE(var._key);
stream->writeSint32BE(var._value);
}
for (const Common::HashMap::Node &timer : timers) {
stream->writeUint32BE(timer._key);
stream->writeUint32BE(timer._value);
}
}
LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
uint32 saveIdentifier = stream->readUint32BE();
uint32 saveVersion = stream->readUint32BE();
if (stream->eos() || stream->err())
return kLoadGameOutcomeMissingVersion;
if (saveIdentifier != kSaveGameIdentifier)
return kLoadGameOutcomeInvalidVersion;
if (saveVersion > kSaveGameCurrentVersion)
return kLoadGameOutcomeSaveIsTooNew;
if (saveVersion < kSaveGameEarliestSupportedVersion)
return kLoadGameOutcomeSaveIsTooOld;
if (saveVersion >= 6)
numStates = stream->readUint32BE();
else
numStates = 1;
if (numStates < 1 || numStates > kMaxStates)
return kLoadGameOutcomeSaveDataCorrupted;
for (uint sti = 0; sti < numStates; sti++) {
states[sti].reset(new SaveGameSwappableState());
states[sti]->roomNumber = stream->readUint32BE();
states[sti]->screenNumber = stream->readUint32BE();
states[sti]->direction = stream->readUint32BE();
if (saveVersion >= 7)
states[sti]->havePendingPostSwapScreenReset = (stream->readByte() != 0);
}
if (saveVersion >= 6) {
hero = stream->readUint32BE();
swapOutScreen = stream->readUint32BE();
swapOutRoom = stream->readUint32BE();
swapOutDirection = stream->readUint32BE();
} else {
hero = 0;
swapOutScreen = 0;
swapOutRoom = 0;
swapOutDirection = 0;
}
escOn = (stream->readByte() != 0);
for (uint sti = 0; sti < numStates; sti++) {
states[sti]->musicTrack = stream->readSint32BE();
if (saveVersion >= 5)
states[sti]->musicVolume = stream->readSint32BE();
else
states[sti]->musicVolume = 100;
if (saveVersion >= 6) {
states[sti]->scoreTrack = safeReadString(stream);
states[sti]->scoreSection = safeReadString(stream);
states[sti]->musicActive = (stream->readByte() != 0);
} else {
states[sti]->musicActive = true;
}
if (saveVersion >= 9)
states[sti]->musicMuteDisabled = (stream->readByte() != 0);
else
states[sti]->musicMuteDisabled = false;
states[sti]->loadedAnimation = stream->readUint32BE();
states[sti]->animDisplayingFrame = stream->readUint32BE();
if (saveVersion >= 6)
states[sti]->animVolume = stream->readSint32BE();
else
states[sti]->animVolume = 100;
}
pendingStaticAnimParams.read(stream);
pendingSoundParams3D.read(stream);
listenerX = stream->readSint32BE();
listenerY = stream->readSint32BE();
listenerAngle = stream->readSint32BE();
uint numInventory[kMaxStates] = {};
uint numSounds[kMaxStates] = {};
for (uint sti = 0; sti < numStates; sti++) {
numInventory[sti] = stream->readUint32BE();
numSounds[sti] = stream->readUint32BE();
}
uint numOneShots = stream->readUint32BE();
uint numSayCycles = 0;
uint numRandomAmbientSounds[kMaxStates] = {};
if (saveVersion >= 4) {
numSayCycles = stream->readUint32BE();
}
if (saveVersion >= 3) {
for (uint sti = 0; sti < numStates; sti++)
numRandomAmbientSounds[sti] = stream->readUint32BE();
}
uint numVars = stream->readUint32BE();
uint numTimers = stream->readUint32BE();
if (stream->eos() || stream->err())
return kLoadGameOutcomeSaveDataCorrupted;
for (uint sti = 0; sti < numStates; sti++) {
states[sti]->inventory.resize(numInventory[sti]);
states[sti]->sounds.resize(numSounds[sti]);
states[sti]->randomAmbientSounds.resize(numRandomAmbientSounds[sti]);
}
triggeredOneShots.resize(numOneShots);
for (uint sti = 0; sti < numStates; sti++) {
for (uint i = 0; i < numInventory[sti]; i++)
states[sti]->inventory[i].read(stream);
for (uint i = 0; i < numSounds[sti]; i++)
states[sti]->sounds[i].read(stream, saveVersion);
}
for (uint i = 0; i < numOneShots; i++)
triggeredOneShots[i].read(stream);
for (uint i = 0; i < numSayCycles; i++) {
uint32 key = stream->readUint32BE();
uint value = stream->readUint32BE();
sayCycles[key] = value;
}
for (uint sti = 0; sti < numStates; sti++) {
for (uint i = 0; i < numRandomAmbientSounds[sti]; i++)
states[sti]->randomAmbientSounds[i].read(stream);
}
for (uint i = 0; i < numVars; i++) {
uint32 key = stream->readUint32BE();
int32 value = stream->readSint32BE();
variables[key] = value;
}
for (uint i = 0; i < numTimers; i++) {
uint32 key = stream->readUint32BE();
uint32 value = stream->readUint32BE();
timers[key] = value;
}
if (stream->eos() || stream->err())
return kLoadGameOutcomeSaveDataCorrupted;
return kLoadGameOutcomeSucceeded;
}
Common::String SaveGameSnapshot::safeReadString(Common::ReadStream *stream) {
uint len = stream->readUint32BE();
if (stream->eos() || stream->err())
len = 0;
return stream->readString(0, len);
}
void SaveGameSnapshot::writeString(Common::WriteStream *stream, const Common::String &str) {
stream->writeUint32BE(str.size());
stream->writeString(str);
}
FontCacheItem::FontCacheItem() : font(nullptr), size(0) {
}
Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
_haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
_gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false), _havePendingPostSwapScreenReset(false),
_havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
_musicTrack(0), _musicActive(true), _musicMute(false), _musicMuteDisabled(false),
_scoreSectionEndTime(0), _musicVolume(getDefaultSoundVolume()), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
_panoramaDirectionFlags(0),
_loadedAnimation(0), _loadedAnimationHasSound(false),
_animTerminateAtStartOfFrame(true), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0), _animVolume(getDefaultSoundVolume()),
_animStartTime(0), _animFramesDecoded(0), _animDecoderState(kAnimDecoderStateStopped),
_animPlayWhileIdle(false), _idleLockInteractions(false), _idleIsOnInteraction(false), _idleIsOnOpenCircuitPuzzleLink(false), _idleIsCircuitPuzzleLinkDown(false),
_forceAllowSaves(false),
_idleHaveClickInteraction(false), _idleHaveDragInteraction(false), _idleInteractionID(0), _haveIdleStaticAnimation(false),
_inGameMenuState(kInGameMenuStateInvisible), _inGameMenuActiveElement(0), _inGameMenuButtonActive {false, false, false, false, false},
_lmbDown(false), _lmbDragging(false), _lmbReleaseWasClick(false), _lmbDownTime(0), _lmbDragTolerance(0),
_delayCompletionTime(0),
_panoramaState(kPanoramaStateInactive),
_listenerX(0), _listenerY(0), _listenerAngle(0), _soundCacheIndex(0),
_isInGame(false),
_subtitleFont(nullptr), _isDisplayingSubtitles(false), _isSubtitleSourceAnimation(false),
_languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _charSet(kCharSetLatin),
_isCDVariant(false) {
for (uint i = 0; i < kNumDirections; i++) {
_haveIdleAnimations[i] = false;
_havePanUpFromDirection[i] = false;
_havePanDownFromDirection[i] = false;
}
for (uint i = 0; i < kPanCursorMaxCount; i++)
_panCursors[i] = 0;
_rng.reset(new Common::RandomSource("vcruise"));
#ifdef USE_FREETYPE2
_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive("NotoSans-Regular.ttf", 16, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight));
_subtitleFont = _subtitleFontKeepalive.get();
#endif
if (!_subtitleFont)
_subtitleFont = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
if (!_subtitleFont)
warning("Couldn't load subtitle font, subtitles will be disabled");
_menuInterface.reset(new RuntimeMenuInterface(this));
for (int32 i = 0; i < 49; i++)
_dbToVolume[i] = decibelsToLinear(i - 49, Audio::Mixer::kMaxChannelVolume / 2, Audio::Mixer::kMaxChannelVolume / 2);
}
Runtime::~Runtime() {
}
void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt) {
_gameSection.init(gameRect, pixFmt);
_menuSection.init(menuRect, pixFmt);
_traySection.init(trayRect, pixFmt);
_fullscreenMenuSection.init(fullscreenMenuRect, pixFmt);
}
void Runtime::loadCursors(const char *exeName) {
Common::SharedPtr winRes(Common::WinResources::createFromEXE(exeName));
if (!winRes)
error("Couldn't open executable file %s", exeName);
Common::Array cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
for (const Common::WinResourceID &id : cursorGroupIDs) {
Common::SharedPtr cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), id));
if (!winRes) {
warning("Couldn't load cursor group");
continue;
}
Common::String nameStr = id.getString();
if (nameStr.matchString("CURSOR_#")) {
char c = nameStr[7];
uint shortID = c - '0';
if (shortID >= _cursorsShort.size())
_cursorsShort.resize(shortID + 1);
_cursorsShort[shortID] = cursorGroup;
} else if (nameStr.matchString("CURSOR_CUR_##")) {
char c1 = nameStr[11];
char c2 = nameStr[12];
uint longID = (c1 - '0') * 10 + (c2 - '0');
if (longID >= _cursors.size())
_cursors.resize(longID + 1);
_cursors[longID] = cursorGroup;
}
}
if (_gameID == GID_REAH) {
// For some reason most cursors map to their resource IDs, except for these
_scriptCursorIDToResourceIDOverride[13] = 35; // Points to finger (instead of back up)
_scriptCursorIDToResourceIDOverride[22] = 13; // Points to back up (instead of up arrow)
_namedCursors["CUR_TYL"] = 22; // Tyl = back
//_namedCursors["CUR_NIC"] = ? // Nic = nothing
_namedCursors["CUR_WEZ"] = 90; // Wez = call? This is the pick-up hand.
_namedCursors["CUR_LUPA"] = 21; // Lupa = magnifier, could be 36 too?
_namedCursors["CUR_NAC"] = 13; // Nac = top? Not sure. But this is the finger pointer.
_namedCursors["CUR_PRZOD"] = 1; // Przod = forward
// CUR_ZOSTAW is in the executable memory but appears to be unused
}
if (_gameID == GID_SCHIZM) {
_namedCursors["curPress"] = 16;
_namedCursors["curLookFor"] = 21;
_namedCursors["curForward"] = 1;
_namedCursors["curBack"] = 13;
_namedCursors["curNothing"] = 0;
_namedCursors["curPickUp"] = 90;
_namedCursors["curDrop"] = 91;
}
_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableDown] = 3;
_panCursors[kPanCursorDraggableHoriz] = 4;
_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionRight] = 5;
_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionLeft] = 6;
_panCursors[kPanCursorDraggableUp] = 7;
_panCursors[kPanCursorDraggableDown] = 8;
_panCursors[kPanCursorDraggableUp | kPanCursorDirectionUp] = 9;
_panCursors[kPanCursorDraggableDown | kPanCursorDirectionDown] = 10;
_panCursors[kPanCursorDraggableUp | kPanCursorDraggableDown] = 11;
_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp | kPanCursorDraggableDown] = 12;
}
void Runtime::setDebugMode(bool debugMode) {
if (debugMode) {
_debugMode = true;
_gameDebugBackBuffer.init(_gameSection.rect, _gameSection.surf->format);
}
}
void Runtime::setFastAnimationMode(bool fastAnimationMode) {
_fastAnimationMode = fastAnimationMode;
}
bool Runtime::runFrame() {
bool moreActions = true;
while (moreActions) {
moreActions = false;
switch (_gameState) {
case kGameStateBoot:
moreActions = bootGame(true);
break;
case kGameStateQuit:
return false;
case kGameStateIdle:
moreActions = runIdle();
break;
case kGameStateDelay:
moreActions = runDelay();
break;
case kGameStatePanLeft:
moreActions = runHorizontalPan(false);
break;
case kGameStatePanRight:
moreActions = runHorizontalPan(true);
break;
case kGameStateScriptReset:
_gameState = kGameStateScript;
moreActions = runScript();
break;
case kGameStateScript:
moreActions = runScript();
break;
case kGameStateWaitingForAnimation:
moreActions = runWaitForAnimation();
break;
case kGameStateWaitingForAnimationToDelay:
moreActions = runWaitForAnimationToDelay();
break;
case kGameStateWaitingForFacing:
moreActions = runWaitForFacing();
break;
case kGameStateWaitingForFacingToAnim:
moreActions = runWaitForFacingToAnim();
break;
case kGameStateGyroIdle:
moreActions = runGyroIdle();
break;
case kGameStateGyroAnimation:
moreActions = runGyroAnimation();
break;
case kGameStateMenu:
moreActions = _menuPage->run();
if (_gameState != kGameStateMenu)
_menuPage.reset();
break;
default:
error("Unknown game state");
return false;
}
}
// Discard any unconsumed OS events
OSEvent evt;
while (popOSEvent(evt)) {
// Do nothing
}
uint32 timestamp = g_system->getMillis();
updateSounds(timestamp);
updateSubtitles();
return true;
}
bool Runtime::bootGame(bool newGame) {
assert(_gameState == kGameStateBoot);
if (!ConfMan.hasKey("vcruise_increase_drag_distance") || ConfMan.hasKey("vcruise_increase_drag_distance"))
_lmbDragTolerance = 3;
if (ConfMan.hasKey("vcruise_mute_music") && ConfMan.getBool("vcruise_mute_music"))
_musicMute = true;
else
_musicMute = false;
debug(1, "Booting V-Cruise game...");
loadIndex();
debug(1, "Index loaded OK");
findWaves();
debug(1, "Waves indexed OK");
if (_gameID == GID_SCHIZM) {
loadConfig("Schizm.ini");
debug(1, "Config indexed OK");
loadScore();
debug(1, "Score loaded OK");
// Duplicate rooms must be identified in advance because they can take effect before the room logic is loaded.
// For example, in room 37, when taking the hanging lift across, the room is changed to room 28 and then
// animation PortD_Zwierz_morph is used, is an animation mapped to room 25, but we can't know that room 28 is
// a duplicate of room 25 unless we check the logic file for rooms 26-28. Additionally, we can't just scan
// downward for missing animations elsewhere because PRZYCUMIE_KRZESELKO is mapped to animations 25 and 26,
// but the frame range for 27 and 28 is supposed to use room 25 (the root of the duplication), not 26.
loadDuplicateRooms();
debug(1, "Duplicated rooms identified OK");
loadAllSchizmScreenNames();
debug(1, "Screen names resolved OK");
} else {
StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
startConfig.disc = 1;
startConfig.room = 1;
startConfig.screen = 0xb0;
startConfig.direction = 0;
}
_trayBackgroundGraphic = loadGraphic("Pocket", true);
_trayHighlightGraphic = loadGraphic("Select", true);
_trayCompassGraphic = loadGraphic("Select_1", true);
_trayCornerGraphic = loadGraphic("Select_2", true);
Common::Language lang = _defaultLanguage;
if (ConfMan.hasKey("language")) {
lang = Common::parseLanguage(ConfMan.get("language"));
debug(2, "Using user-selected language %s", Common::getLanguageDescription(lang));
} else {
debug(2, "Defaulted language to %s", Common::getLanguageDescription(lang));
}
_languageIndex = 1;
_defaultLanguageIndex = 1;
if (_gameID == GID_REAH) {
_animSpeedRotation = Fraction(21, 1); // Probably accurate
_animSpeedStaticAnim = Fraction(21, 1); // Probably accurate
_animSpeedDefault = Fraction(16, 1); // Possibly not accurate
const Common::Language langIndexes[] = {
Common::PL_POL,
Common::EN_ANY,
Common::DE_DEU,
Common::FR_FRA,
Common::NL_NLD,
Common::ES_ESP,
Common::IT_ITA,
};
uint langCount = sizeof(langIndexes) / sizeof(langIndexes[0]);
for (uint li = 0; li < langCount; li++) {
if (langIndexes[li] == _defaultLanguage)
_defaultLanguageIndex = li;
}
for (uint li = 0; li < langCount; li++) {
if (langIndexes[li] == lang) {
_languageIndex = li;
break;
}
if (langIndexes[li] == _defaultLanguage)
_languageIndex = li;
}
} else if (_gameID == GID_SCHIZM) {
_animSpeedRotation = Fraction(21, 1); // Probably accurate
_animSpeedStaticAnim = Fraction(21, 1); // Probably accurate
_animSpeedDefault = Fraction(21, 1); // Probably accurate
const Common::Language langIndexes[] = {
Common::PL_POL,
Common::EN_GRB,
Common::DE_DEU,
Common::FR_FRA,
Common::NL_NLD,
Common::ES_ESP,
Common::IT_ITA,
Common::RU_RUS,
Common::EL_GRC,
Common::EN_USA,
// Additional subs present in Steam release
Common::BG_BUL,
Common::ZH_TWN,
Common::JA_JPN,
Common::HU_HUN,
Common::ZH_CHN,
Common::CS_CZE,
};
uint langCount = sizeof(langIndexes) / sizeof(langIndexes[0]);
for (uint li = 0; li < langCount; li++) {
if (langIndexes[li] == _defaultLanguage)
_defaultLanguageIndex = li;
}
for (uint li = 0; li < langCount; li++) {
if (langIndexes[li] == lang) {
_languageIndex = li;
break;
}
if (langIndexes[li] == _defaultLanguage)
_languageIndex = li;
}
}
debug(2, "Language index: %u Default language index: %u", _languageIndex, _defaultLanguageIndex);
Common::CodePage codePage = Common::CodePage::kASCII;
resolveCodePageForLanguage(lang, codePage, _charSet);
bool subtitlesLoadedOK = loadSubtitles(codePage, false);
if (!subtitlesLoadedOK) {
lang = _defaultLanguage;
_languageIndex = _defaultLanguageIndex;
warning("Localization data failed to load, retrying with default language");
resolveCodePageForLanguage(lang, codePage, _charSet);
subtitlesLoadedOK = loadSubtitles(codePage, false);
if (!subtitlesLoadedOK) {
if (_languageIndex != 0) {
codePage = Common::CodePage::kWindows1250;
_languageIndex = 0;
_defaultLanguageIndex = 0;
warning("Localization data failed to load again, trying one more time and guessing the encoding");
subtitlesLoadedOK = loadSubtitles(codePage, true);
}
}
}
debug(2, "Final language selection: %s Code page: %i Language index: %u", Common::getLanguageDescription(lang), static_cast(codePage), _languageIndex);
if (subtitlesLoadedOK)
debug(1, "Localization data loaded OK");
else
warning("Localization data failed to load! Text and subtitles will be disabled.");
_uiGraphics.resize(24);
for (uint i = 0; i < _uiGraphics.size(); i++) {
if (_gameID == GID_REAH) {
_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast(_languageIndex * 100u + i)), false);
if (_languageIndex != 0 && !_uiGraphics[i])
_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast(i)), false);
} else if (_gameID == GID_SCHIZM) {
_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), false);
}
}
_gameState = kGameStateIdle;
if (newGame) {
if (ConfMan.hasKey("vcruise_skip_menu") && ConfMan.getBool("vcruise_skip_menu")) {
_mostRecentValidSaveState = generateNewGameSnapshot();
restoreSaveGameSnapshot();
} else {
changeToScreen(1, 0xb1);
}
}
return true;
}
void Runtime::resolveCodePageForLanguage(Common::Language lang, Common::CodePage &outCodePage, CharSet &outCharSet) {
switch (lang) {
case Common::PL_POL:
case Common::CS_CZE:
case Common::HU_HUN:
outCodePage = Common::CodePage::kWindows1250;
outCharSet = kCharSetLatin;
return;
case Common::RU_RUS:
case Common::BG_BUL:
outCodePage = Common::CodePage::kWindows1251;
outCharSet = kCharSetCyrillic;
return;
case Common::EL_GRC:
outCodePage = Common::CodePage::kWindows1253;
outCharSet = kCharSetGreek;
return;
case Common::ZH_TWN:
outCodePage = Common::CodePage::kBig5;
outCharSet = kCharSetChineseTraditional;
return;
case Common::JA_JPN:
outCodePage = Common::CodePage::kWindows932; // Shift-JIS compatible
outCharSet = kCharSetJapanese;
return;
case Common::ZH_CHN:
outCodePage = Common::CodePage::kGBK;
outCharSet = kCharSetChineseSimplified;
return;
default:
outCodePage = Common::CodePage::kWindows1252;
outCharSet = kCharSetLatin;
return;
}
}
void Runtime::drawLabel(Graphics::ManagedSurface *surface, const Common::String &labelID, const Common::Rect &contentRect) {
Common::HashMap::const_iterator labelDefIt = _locUILabels.find(labelID);
if (labelDefIt == _locUILabels.end())
return;
const UILabelDef &labelDef = labelDefIt->_value;
Common::HashMap::const_iterator lineIt = _locStrings.find(labelDef.lineID);
if (lineIt == _locStrings.end())
return;
Common::HashMap::const_iterator styleIt = _locTextStyles.find(labelDef.styleDefID);
if (styleIt == _locTextStyles.end())
return;
const Graphics::Font *font = resolveFont(styleIt->_value.fontName, styleIt->_value.size);
if (!font)
return;
const Common::String &textUTF8 = lineIt->_value;
if (textUTF8.size() == 0)
return;
uint32 textColorRGB = styleIt->_value.colorRGB;
uint32 shadowColorRGB = styleIt->_value.shadowColorRGB;
uint shadowOffset = styleIt->_value.size / 10u;
Common::U32String text = textUTF8.decode(Common::kUtf8);
int strWidth = font->getStringWidth(text);
int strHeight = font->getFontHeight();
Common::Point textPos;
switch (styleIt->_value.alignment % 10u) {
case 1:
textPos.x = contentRect.left + (contentRect.width() - strWidth) / 2;
break;
case 2:
textPos.x = contentRect.left - strWidth;
break;
default:
textPos.x = contentRect.left;
break;
}
textPos.y = contentRect.top + (static_cast(labelDef.graphicHeight) - strHeight) / 2;
if (shadowColorRGB != 0) {
Common::Point shadowPos = textPos + Common::Point(shadowOffset, shadowOffset);
uint32 realShadowColor = surface->format.RGBToColor((shadowColorRGB >> 16) & 0xff, (shadowColorRGB >> 8) & 0xff, shadowColorRGB & 0xff);
font->drawString(surface, text, shadowPos.x, shadowPos.y, strWidth, realShadowColor);
}
uint32 realTextColor = surface->format.RGBToColor((textColorRGB >> 16) & 0xff, (textColorRGB >> 8) & 0xff, textColorRGB & 0xff);
font->drawString(surface, text, textPos.x, textPos.y, strWidth, realTextColor);
}
bool Runtime::runIdle() {
if (_havePendingScreenChange) {
_havePendingScreenChange = false;
_havePendingPreIdleActions = true;
changeToScreen(_roomNumber, _screenNumber);
return true;
}
if (_havePendingPlayAmbientSounds) {
_havePendingPlayAmbientSounds = false;
triggerAmbientSounds();
}
if (_havePendingPreIdleActions) {
_havePendingPreIdleActions = false;
if (triggerPreIdleActions())
return true;
}
if (_havePendingReturnToIdleState) {
_havePendingReturnToIdleState = false;
returnToIdleState();
drawCompass();
return true;
}
uint32 timestamp = g_system->getMillis();
// Try to keep this in sync with runDelay
if (_animPlayWhileIdle) {
assert(_haveIdleAnimations[_direction]);
StaticAnimation &sanim = _idleAnimations[_direction];
bool looping = (sanim.params.repeatDelay == 0);
bool animEnded = false;
continuePlayingAnimation(looping, false, animEnded);
if (!looping && animEnded) {
_animPlayWhileIdle = false;
sanim.nextStartTime = timestamp + sanim.params.repeatDelay * 1000u;
sanim.currentAlternation = 1 - sanim.currentAlternation;
if (_idleLockInteractions) {
_idleLockInteractions = false;
bool changedState = dischargeIdleMouseMove();
if (changedState) {
drawCompass();
return true;
}
}
}
} else if (_haveIdleAnimations[_direction]) {
// Try to re-trigger
StaticAnimation &sanim = _idleAnimations[_direction];
if (sanim.nextStartTime <= timestamp) {
const AnimationDef &animDef = sanim.animDefs[sanim.currentAlternation];
changeAnimation(animDef, animDef.firstFrame, false, _animSpeedStaticAnim);
_animPlayWhileIdle = true;
_idleLockInteractions = sanim.params.lockInteractions;
if (_idleLockInteractions) {
_panoramaState = kPanoramaStateInactive;
bool changedState = dischargeIdleMouseMove();
assert(!changedState); // Shouldn't be changing state from this!
(void)changedState;
}
}
}
if (_debugMode)
drawDebugOverlay();
detectPanoramaMouseMovement(timestamp);
OSEvent osEvent;
while (popOSEvent(osEvent)) {
if (osEvent.type == kOSEventTypeMouseMove) {
detectPanoramaMouseMovement(osEvent.timestamp);
bool changedState = dischargeIdleMouseMove();
if (changedState) {
drawCompass();
return true;
}
} else if (osEvent.type == kOSEventTypeLButtonUp) {
if (_inGameMenuState != kInGameMenuStateInvisible) {
dischargeInGameMenuMouseUp();
} else {
PanoramaState oldPanoramaState = _panoramaState;
_panoramaState = kPanoramaStateInactive;
// This is the correct place for matching the original game's behavior, not switching to panorama
resetInventoryHighlights();
if (_lmbReleaseWasClick) {
bool changedState = dischargeIdleClick();
if (changedState) {
drawCompass();
return true;
}
}
// If the released from panorama mode, pick up any interactions at the new mouse location, and change the mouse back
if (oldPanoramaState != kPanoramaStateInactive) {
changeToCursor(_cursors[kCursorArrow]);
// Clear idle interaction so that if a drag occurs but doesn't trigger a panorama or other state change,
// interactions are re-detected here.
_idleIsOnInteraction = false;
bool changedState = dischargeIdleMouseMove();
if (changedState) {
drawCompass();
return true;
}
}
}
} else if (osEvent.type == kOSEventTypeLButtonDown) {
bool changedState = dischargeIdleMouseDown();
if (changedState) {
drawCompass();
return true;
}
} else if (osEvent.type == kOSEventTypeKeymappedEvent) {
if (!_lmbDown) {
switch (osEvent.keymappedEvent) {
case kKeymappedEventHelp:
changeToMenuPage(createMenuHelp(_gameID == GID_SCHIZM));
return true;
case kKeymappedEventLoadGame:
if (g_engine->loadGameDialog())
return true;
break;
case kKeymappedEventSaveGame:
if (g_engine->saveGameDialog())
return true;
break;
case kKeymappedEventPause:
changeToMenuPage(createMenuPause(_gameID == GID_SCHIZM));
return true;
case kKeymappedEventQuit:
changeToMenuPage(createMenuQuit(_gameID == GID_SCHIZM));
return true;
default:
break;
}
}
}
}
// Yield
return false;
}
bool Runtime::runDelay() {
uint32 timestamp = g_system->getMillis();
if (g_system->getMillis() >= _delayCompletionTime) {
_gameState = kGameStateScript;
return true;
}
if (_havePendingPreIdleActions) {
_havePendingPreIdleActions = false;
if (triggerPreIdleActions())
return true;
}
// Play static animations. Try to keep this in sync with runIdle
if (_animPlayWhileIdle) {
assert(_haveIdleAnimations[_direction]);
StaticAnimation &sanim = _idleAnimations[_direction];
bool looping = (sanim.params.repeatDelay == 0);
bool animEnded = false;
continuePlayingAnimation(looping, false, animEnded);
if (!looping && animEnded) {
_animPlayWhileIdle = false;
sanim.nextStartTime = timestamp + sanim.params.repeatDelay * 1000u;
sanim.currentAlternation = 1 - sanim.currentAlternation;
if (_idleLockInteractions)
_idleLockInteractions = false;
}
} else if (_haveIdleAnimations[_direction]) {
StaticAnimation &sanim = _idleAnimations[_direction];
if (sanim.nextStartTime <= timestamp) {
const AnimationDef &animDef = sanim.animDefs[sanim.currentAlternation];
changeAnimation(animDef, animDef.firstFrame, false, _animSpeedStaticAnim);
_animPlayWhileIdle = true;
_idleLockInteractions = sanim.params.lockInteractions;
}
}
return false;
}
bool Runtime::runHorizontalPan(bool isRight) {
bool animEnded = false;
continuePlayingAnimation(true, false, animEnded);
Common::Point panRelMouse = _mousePos - _panoramaAnchor;
if (!_lmbDown) {
debug(1, "Terminating pan: LMB is not down");
startTerminatingHorizontalPan(isRight);
return true;
}
if (!isRight && panRelMouse.x > -kPanoramaPanningMarginX) {
debug(1, "Terminating pan: Over threshold for left movement");
startTerminatingHorizontalPan(isRight);
return true;
} else if (isRight && panRelMouse.x < kPanoramaPanningMarginX) {
debug(1, "Terminating pan: Over threshold for right movement");
startTerminatingHorizontalPan(isRight);
return true;
}
OSEvent osEvent;
while (popOSEvent(osEvent)) {
if (osEvent.type == kOSEventTypeLButtonUp) {
debug(1, "Terminating pan: LMB up");
startTerminatingHorizontalPan(isRight);
return true;
} else if (osEvent.type == kOSEventTypeMouseMove) {
panRelMouse = osEvent.pos - _panoramaAnchor;
if (!isRight && panRelMouse.x > -kPanoramaPanningMarginX) {
debug(1, "Terminating pan: Over threshold for left movement (from queue)");
startTerminatingHorizontalPan(isRight);
return true;
} else if (isRight && panRelMouse.x < kPanoramaPanningMarginX) {
debug(1, "Terminating pan: Over threshold for right movement (from queue)");
startTerminatingHorizontalPan(isRight);
return true;
}
}
}
// Didn't terminate, yield
return false;
}
bool Runtime::runWaitForAnimation() {
bool animEnded = false;
continuePlayingAnimation(false, false, animEnded);
if (animEnded) {
_gameState = kGameStateScript;
return true;
}
// Still waiting, check events
OSEvent evt;
while (popOSEvent(evt)) {
if (evt.type == kOSEventTypeKeyDown && evt.keyCode == Common::KEYCODE_ESCAPE) {
if (_escOn) {
// Terminate the animation
if (_animDecoderState == kAnimDecoderStatePlaying) {
_animDecoder->pauseVideo(true);
_animDecoderState = kAnimDecoderStatePaused;
}
_scriptEnv.esc = true;
_gameState = kGameStateScript;
return true;
}
} else if (evt.type == kOSEventTypeKeymappedEvent && evt.keymappedEvent == kKeymappedEventSkipAnimation) {
_animFrameRateLock = Fraction(600, 1);
_animFramesDecoded = 0; // Reset decoder count so the start time resyncs
}
}
// Yield
return false;
}
bool Runtime::runWaitForAnimationToDelay() {
bool animEnded = false;
continuePlayingAnimation(false, false, animEnded);
if (animEnded) {
_gameState = kGameStateDelay;
return true;
}
// Yield
return false;
}
bool Runtime::runWaitForFacingToAnim() {
bool animEnded = false;
continuePlayingAnimation(true, true, animEnded);
if (animEnded) {
changeAnimation(_postFacingAnimDef, _postFacingAnimDef.firstFrame, true, _animSpeedDefault);
_gameState = kGameStateWaitingForAnimation;
return true;
}
// Yield
return false;
}
bool Runtime::runWaitForFacing() {
bool animEnded = false;
continuePlayingAnimation(true, true, animEnded);
if (animEnded) {
_gameState = kGameStateScript;
return true;
}
// Yield
return false;
}
bool Runtime::runGyroIdle() {
if (!_lmbDown) {
exitGyroIdle();
return true;
}
int32 deltaCoordinate = 0;
if (_gyros.isVertical)
deltaCoordinate = _gyros.dragBasePoint.y - _mousePos.y;
else
deltaCoordinate = _gyros.dragBasePoint.x - _mousePos.x;
// Start the first step at half margin
int32 halfDragMargin = _gyros.dragMargin / 2;
if (deltaCoordinate < 0)
deltaCoordinate -= halfDragMargin;
else
deltaCoordinate += halfDragMargin;
int32 deltaState = deltaCoordinate / static_cast(_gyros.dragMargin);
int32 targetStateInitial = static_cast(_gyros.dragBaseState) + deltaState;
Gyro &gyro = _gyros.gyros[_gyros.activeGyro];
int32 targetState = 0;
if (gyro.wrapAround) {
targetState = targetStateInitial;
} else {
if (targetStateInitial > 0) {
targetState = targetStateInitial;
if (static_cast(targetState) > _gyros.maxValue)
targetState = _gyros.maxValue;
}
}
if (targetState < _gyros.dragCurrentState) {
AnimationDef animDef = _gyros.negAnim;
uint initialFrame = 0;
if (gyro.wrapAround) {
uint maxValuePlusOne = _gyros.maxValue + 1;
initialFrame = animDef.firstFrame + ((maxValuePlusOne - gyro.currentState) % maxValuePlusOne * _gyros.frameSeparation);
} else
initialFrame = animDef.firstFrame + ((_gyros.maxValue - gyro.currentState) * _gyros.frameSeparation);
// This is intentional instead of setting the stop frame, V-Cruise can overrun the end of the animation.
// firstFrame is left alone so playlists are based correctly.
animDef.lastFrame = initialFrame + _gyros.frameSeparation;
changeAnimation(animDef, initialFrame, false, _animSpeedDefault);
gyro.logState();
gyro.currentState--;
_gyros.dragCurrentState--;
if (gyro.currentState < 0)
gyro.currentState = _gyros.maxValue;
_gameState = kGameStateGyroAnimation;
_havePendingCompletionCheck = true;
return true;
} else if (targetState > _gyros.dragCurrentState) {
AnimationDef animDef = _gyros.posAnim;
uint initialFrame = animDef.firstFrame + gyro.currentState * _gyros.frameSeparation;
// This is intentional instead of setting the stop frame, V-Cruise can overrun the end of the animation.
// firstFrame is left alone so playlists are based correctly.
animDef.lastFrame = initialFrame + _gyros.frameSeparation;
changeAnimation(animDef, initialFrame, false, _animSpeedDefault);
gyro.logState();
gyro.currentState++;
_gyros.dragCurrentState++;
if (static_cast(gyro.currentState) > _gyros.maxValue)
gyro.currentState = 0;
_gameState = kGameStateGyroAnimation;
_havePendingCompletionCheck = true;
return true;
}
OSEvent evt;
while (popOSEvent(evt)) {
if (evt.type == kOSEventTypeLButtonUp) {
exitGyroIdle();
return true;
}
}
// Yield
return false;
}
bool Runtime::runGyroAnimation() {
bool animEnded = false;
continuePlayingAnimation(false, false, animEnded);
if (animEnded) {
_gameState = kGameStateGyroIdle;
return true;
}
// Yield
return false;
}
void Runtime::exitGyroIdle() {
_gameState = kGameStateScript;
_havePendingPreIdleActions = true;
// In Reah, gyro interactions stop the script.
if (_gameID == GID_REAH)
terminateScript();
}
void Runtime::continuePlayingAnimation(bool loop, bool useStopFrame, bool &outAnimationEnded) {
bool terminateAtStartOfFrame = _animTerminateAtStartOfFrame;
outAnimationEnded = false;
if (!_animDecoder) {
outAnimationEnded = true;
return;
}
bool needsFirstFrame = false;
if (_animDecoderState == kAnimDecoderStatePaused) {
_animDecoder->pauseVideo(false);
_animDecoderState = kAnimDecoderStatePlaying;
needsFirstFrame = true;
} else if (_animDecoderState == kAnimDecoderStateStopped) {
_animDecoder->start();
_animDecoderState = kAnimDecoderStatePlaying;
needsFirstFrame = true;
}
uint32 millis = 0;
// Avoid spamming event recorder as much if we don't actually need to fetch millis, but also only fetch it once here.
if (_animFrameRateLock.numerator)
millis = g_system->getMillis();
for (;;) {
bool needNewFrame = false;
bool needInitialTimestamp = false;
if (needsFirstFrame) {
needNewFrame = true;
needsFirstFrame = false;
needInitialTimestamp = true;
} else {
if (_animFrameRateLock.numerator) {
if (_animFramesDecoded == 0) {
needNewFrame = true;
needInitialTimestamp = true;
} else {
// if ((millis - startTime) / 1000 * frameRate / frameRateDenominator) >= framesDecoded
if ((millis - _animStartTime) * static_cast(_animFrameRateLock.numerator) >= (static_cast(_animFramesDecoded) * static_cast(_animFrameRateLock.denominator) * 1000u))
needNewFrame = true;
}
} else {
if (_animDecoder->getTimeToNextFrame() == 0)
needNewFrame = true;
}
}
if (!needNewFrame)
break;
if (!terminateAtStartOfFrame && !loop && _animPendingDecodeFrame > _animLastFrame) {
outAnimationEnded = true;
return;
}
// We check this here for timing reasons: The no-loop case after the draw terminates the animation as soon as the last frame
// starts delaying without waiting for the time until the next frame to expire.
// The loop check here DOES wait for the time until next frame to expire.
if (loop && _animPendingDecodeFrame > _animLastFrame) {
debug(4, "Looped animation at frame %u", _animLastFrame);
if (!_animDecoder->seekToFrame(_animFirstFrame)) {
outAnimationEnded = true;
return;
}
_animPendingDecodeFrame = _animFirstFrame;
}
const Graphics::Surface *surface = _animDecoder->decodeNextFrame();
// Get the start timestamp when the first frame finishes decoding so disk seeks don't cause frame skips
if (needInitialTimestamp) {
millis = g_system->getMillis();
_animStartTime = millis;
}
if (!surface) {
outAnimationEnded = true;
return;
}
_animDisplayingFrame = _animPendingDecodeFrame;
_animPendingDecodeFrame++;
_animFramesDecoded++;
if (_animDisplayingFrame < _frameData2.size()) {
const FrameData2 &fd2 = _frameData2[_animDisplayingFrame];
_listenerX = fd2.x;
_listenerY = fd2.y;
_listenerAngle = fd2.angle;
}
update3DSounds();
AnimSubtitleMap_t::const_iterator animSubtitlesIt = _animSubtitles.find(_loadedAnimation);
if (animSubtitlesIt != _animSubtitles.end()) {
const FrameToSubtitleMap_t &frameMap = animSubtitlesIt->_value;
FrameToSubtitleMap_t::const_iterator frameIt = frameMap.find(_animDisplayingFrame);
if (frameIt != frameMap.end() && ConfMan.getBool("subtitles")) {
if (!millis)
millis = g_system->getMillis();
const SubtitleDef &subDef = frameIt->_value;
_subtitleQueue.clear();
_isDisplayingSubtitles = false;
_isSubtitleSourceAnimation = true;
SubtitleQueueItem queueItem;
queueItem.startTime = millis;
queueItem.endTime = millis + 1000u;
for (int ch = 0; ch < 3; ch++)
queueItem.color[ch] = subDef.color[ch];
if (subDef.durationInDeciseconds != 1)
queueItem.endTime = queueItem.startTime + subDef.durationInDeciseconds * 100u;
queueItem.str = subDef.str.decode(Common::kUtf8);
_subtitleQueue.push_back(queueItem);
}
}
if (_animPlaylist) {
uint decodeFrameInPlaylist = _animDisplayingFrame - _animFirstFrame;
for (const SfxPlaylistEntry &playlistEntry : _animPlaylist->entries) {
if (playlistEntry.frame == decodeFrameInPlaylist) {
VCruise::AudioPlayer &audioPlayer = *playlistEntry.sample->audioPlayer;
if (playlistEntry.isUpdate) {
audioPlayer.setVolumeAndBalance(applyVolumeScale(playlistEntry.volume), applyBalanceScale(playlistEntry.balance));
} else {
audioPlayer.stop();
playlistEntry.sample->audioStream->seek(0);
audioPlayer.play(applyVolumeScale(playlistEntry.volume), applyBalanceScale(playlistEntry.balance));
}
// No break, it's possible for there to be multiple sounds in the same frame
}
}
}
Common::Rect copyRect = Common::Rect(0, 0, surface->w, surface->h);
if (!_animConstraintRect.isEmpty())
copyRect = copyRect.findIntersectingRect(_animConstraintRect);
Common::Rect constraintRect = Common::Rect(0, 0, _gameSection.rect.width(), _gameSection.rect.height());
copyRect = copyRect.findIntersectingRect(constraintRect);
if (copyRect.isValidRect() || !copyRect.isEmpty()) {
_gameSection.surf->blitFrom(*surface, copyRect, copyRect, _animDecoder->getPalette());
drawSectionToScreen(_gameSection, copyRect);
}
if (!loop) {
if (terminateAtStartOfFrame && _animDisplayingFrame >= _animLastFrame) {
_animDecoder->pauseVideo(true);
_animDecoderState = kAnimDecoderStatePaused;
outAnimationEnded = true;
return;
}
}
if (useStopFrame && _animDisplayingFrame == _animStopFrame) {
outAnimationEnded = true;
return;
}
}
}
void Runtime::drawSectionToScreen(const RenderSection §ion, const Common::Rect &rect) {
const RenderSection *sourceSection = §ion;
if (_debugMode && (&_gameSection == §ion)) {
_gameDebugBackBuffer.surf->blitFrom(*sourceSection->surf, rect, rect);
commitSectionToScreen(_gameDebugBackBuffer, rect);
} else
commitSectionToScreen(*sourceSection, rect);
}
void Runtime::commitSectionToScreen(const RenderSection §ion, const Common::Rect &rect) {
_system->copyRectToScreen(section.surf->getBasePtr(rect.left, rect.top), section.surf->pitch, rect.left + section.rect.left, rect.top + section.rect.top, rect.width(), rect.height());
}
bool Runtime::requireAvailableStack(uint n) {
if (_scriptStack.size() < n) {
error("Script stack underflow");
return false;
}
return true;
}
void Runtime::terminateScript() {
_scriptCallStack.clear();
// Collect any script env vars that affect script termination, then clear so this doesn't leak into
// future executions.
bool puzzleWasSet = _scriptEnv.puzzleWasSet;
bool exitToMenu = _scriptEnv.exitToMenu;
_scriptEnv = ScriptEnvironmentVars();
if (_gameState == kGameStateScript)
_gameState = kGameStateIdle;
if (_havePendingCompletionCheck) {
_havePendingCompletionCheck = false;
if (checkCompletionConditions())
return;
}
drawCompass();
if (exitToMenu && _gameState == kGameStateIdle) {
quitToMenu();
return;
}
if (_havePendingScreenChange) {
// TODO: Check Reah to see if this condition is okay there too.
// This is needed to avoid resetting static animations twice, which causes problems with,
// for example, the second screen on Hannah's path resetting the idle animations after
// the VO stops.
if (_gameID == GID_SCHIZM) {
_havePendingScreenChange = false;
// The circuit puzzle doesn't call puzzleDone unless you zoom back into the puzzle,
// which can cause the puzzle to leak. Clean it up here instead.
if (!puzzleWasSet)
clearCircuitPuzzle();
}
changeToScreen(_roomNumber, _screenNumber);
// Run any return-to-idle actions so idle mouse moves are discharged again, even if the screen didn't change.
// This is necessary to ensure that dischargeIdleMouseMove is called after animS even if it goes back to the same
// screen, which is necessary to make sure that clicking the pegs on top of the mechanical computer in Schizm
// resets the mouse cursor to interactive again.
if (_gameID == GID_SCHIZM)
_havePendingReturnToIdleState = true;
}
}
void Runtime::quitToMenu() {
changeToCursor(_cursors[kCursorArrow]);
if (_gameID == GID_SCHIZM && _musicActive) {
_scoreTrack = "music99";
_scoreSection = "start";
startScoreSection();
}
for (Common::SharedPtr &snd : _activeSounds)
stopSound(*snd);
_activeSounds.clear();
_isInGame = false;
if (_gameID == GID_REAH || _gameID == GID_SCHIZM)
changeToMenuPage(createMenuMain(_gameID == GID_SCHIZM));
else
error("Missing main menu behavior for this game");
}
RoomScriptSet *Runtime::getRoomScriptSetForCurrentRoom() const {
if (!_scriptSet)
return nullptr;
uint roomNumber = _roomNumber;
if (roomNumber < _roomDuplicationOffsets.size())
roomNumber -= _roomDuplicationOffsets[roomNumber];
RoomScriptSetMap_t::const_iterator it = _scriptSet->roomScripts.find(roomNumber);
if (it == _scriptSet->roomScripts.end())
return nullptr;
return it->_value.get();
}
bool Runtime::checkCompletionConditions() {
bool succeeded = true;
for (uint i = 0; i < GyroState::kNumGyros; i++) {
const Gyro &gyro = _gyros.gyros[i];
if (gyro.requireState && gyro.currentState != gyro.requiredState) {
succeeded = false;
break;
}
if (gyro.numPreviousStates != gyro.numPreviousStatesRequired) {
succeeded = false;
break;
}
bool prevStatesMatch = true;
for (uint j = 0; j < gyro.numPreviousStates; j++) {
if (gyro.previousStates[j] != gyro.requiredPreviousStates[j]) {
prevStatesMatch = false;
break;
}
}
if (!prevStatesMatch) {
succeeded = false;
break;
}
}
// Activate the corresponding failure or success interaction if present
if (_scriptSet) {
RoomScriptSet *roomScriptSet = getRoomScriptSetForCurrentRoom();
if (roomScriptSet) {
const ScreenScriptSetMap_t &screenScriptsMap = roomScriptSet->screenScripts;
ScreenScriptSetMap_t::const_iterator screenScriptIt = screenScriptsMap.find(_screenNumber);
if (screenScriptIt != screenScriptsMap.end()) {
const ScreenScriptSet &screenScriptSet = *screenScriptIt->_value;
ScriptMap_t::const_iterator interactionScriptIt = screenScriptSet.interactionScripts.find(succeeded ? _gyros.completeInteraction : _gyros.failureInteraction);
if (interactionScriptIt != screenScriptSet.interactionScripts.end()) {
const Common::SharedPtr