/* 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/crc.h"
#include "common/endian.h"
#include "common/events.h"
#include "common/file.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 "graphics/palette.h"
#include "image/ani.h"
#include "image/bmp.h"
#include "image/icocur.h"
#include "math/utils.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/ad2044_items.h"
#include "vcruise/ad2044_ui.h"
#include "vcruise/audio_player.h"
#include "vcruise/circuitpuzzle.h"
#include "vcruise/sampleloop.h"
#include "vcruise/midi_player.h"
#include "vcruise/menu.h"
#include "vcruise/runtime.h"
#include "vcruise/script.h"
#include "vcruise/textparser.h"
#include "vcruise/vcruise.h"
namespace VCruise {
struct InitialItemPlacement {
uint roomNumber;
uint screenNumber;
uint itemID;
};
const InitialItemPlacement g_ad2044InitialItemPlacements[] = {
{1, 0xb0, 18}, // Spoon
{1, 0xb8, 24}, // Cigarette pack
{1, 0xac, 27}, // Matches
{1, 0x62, 2}, // "A" tag
{1, 0x64, 58}, // Newspaper
};
struct AD2044Graphics {
explicit AD2044Graphics(const Common::SharedPtr &resources, bool lowQuality, const Graphics::PixelFormat &pixFmt);
Common::SharedPtr invDownClicked;
Common::SharedPtr invUpClicked;
Common::SharedPtr musicClicked;
Common::SharedPtr musicClickedDeep;
Common::SharedPtr soundClicked;
Common::SharedPtr soundClickedDeep;
Common::SharedPtr exitClicked;
Common::SharedPtr loadClicked;
Common::SharedPtr saveClicked;
Common::SharedPtr resizeClicked;
Common::SharedPtr musicVolUpClicked;
Common::SharedPtr musicVolDownClicked;
Common::SharedPtr music;
Common::SharedPtr musicVol;
Common::SharedPtr sound;
Common::SharedPtr soundVol;
Common::SharedPtr musicDisabled;
Common::SharedPtr musicVolDisabled;
Common::SharedPtr soundDisabled;
Common::SharedPtr soundVolDisabled;
Common::SharedPtr examine;
Common::SharedPtr examineDisabled;
Common::SharedPtr invPage[8];
void loadGraphic(Common::SharedPtr AD2044Graphics::*field, const Common::String &resName);
Common::SharedPtr loadGraphic(const Common::String &resName) const;
void finishLoading();
private:
AD2044Graphics() = delete;
bool _lowQuality;
Common::SharedPtr _resources;
Common::Array _resourceIDs;
const Graphics::PixelFormat _pixFmt;
};
AD2044Graphics::AD2044Graphics(const Common::SharedPtr &resources, bool lowQuality, const Graphics::PixelFormat &pixFmt)
: _resources(resources), _lowQuality(lowQuality), _pixFmt(pixFmt) {
_resourceIDs = resources->getIDList(Common::kWinBitmap);
}
void AD2044Graphics::loadGraphic(Common::SharedPtr AD2044Graphics::*field, const Common::String &resNameBase) {
this->*field = loadGraphic(resNameBase);
}
Common::SharedPtr AD2044Graphics::loadGraphic(const Common::String &resNameBase) const {
Common::String resName = _lowQuality ? (Common::String("D") + resNameBase) : resNameBase;
const Common::WinResourceID *resID = nullptr;
for (const Common::WinResourceID &resIDCandidate : _resourceIDs) {
if (resIDCandidate.getString() == resName) {
resID = &resIDCandidate;
break;
}
}
if (!resID)
error("Couldn't find bitmap graphic %s", resName.c_str());
Common::ScopedPtr bmpResource(_resources->getResource(Common::kWinBitmap, *resID));
if (!bmpResource)
error("Couldn't open bitmap graphic %s", resName.c_str());
Image::BitmapDecoder decoder;
if (!decoder.loadStream(*bmpResource))
error("Couldn't load bitmap graphic %s", resName.c_str());
const Graphics::Surface *bmpSurf = decoder.getSurface();
Common::SharedPtr surf(bmpSurf->convertTo(_pixFmt, decoder.getPalette().data(), decoder.getPalette().size()), Graphics::SurfaceDeleter());
return surf;
}
void AD2044Graphics::finishLoading() {
_resources.reset();
}
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::ManagedSurface *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::ManagedSurface *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) {
}
MapLoader::~MapLoader() {
}
Common::SharedPtr MapLoader::loadScreenDirectionDef(Common::ReadStream &stream) {
byte screenDefHeader[16];
if (stream.read(screenDefHeader, 16) != 16)
error("Error reading screen def header");
uint16 numInteractions = READ_LE_UINT16(screenDefHeader + 0);
if (numInteractions > 0) {
Common::SharedPtr screenDirectionDef(new MapScreenDirectionDef());
screenDirectionDef->interactions.resize(numInteractions);
for (uint i = 0; i < numInteractions; i++) {
InteractionDef &idef = screenDirectionDef->interactions[i];
byte interactionData[12];
if (stream.read(interactionData, 12) != 12)
error("Error reading interaction data");
idef.rect = Common::Rect(READ_LE_INT16(interactionData + 0), READ_LE_INT16(interactionData + 2), READ_LE_INT16(interactionData + 4), READ_LE_INT16(interactionData + 6));
idef.interactionID = READ_LE_UINT16(interactionData + 8);
idef.objectType = READ_LE_UINT16(interactionData + 10);
}
return screenDirectionDef;
}
return nullptr;
}
class ReahSchizmMapLoader : public MapLoader {
public:
ReahSchizmMapLoader();
void setRoomNumber(uint roomNumber) override;
const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction) override;
void unload() override;
private:
void load();
static const uint kNumScreens = 96;
static const uint kFirstScreen = 0xa0;
uint _roomNumber;
bool _isLoaded;
Common::SharedPtr _screenDirections[kNumScreens][kNumDirections];
};
ReahSchizmMapLoader::ReahSchizmMapLoader() : _roomNumber(0), _isLoaded(false) {
}
void ReahSchizmMapLoader::setRoomNumber(uint roomNumber) {
if (_roomNumber != roomNumber)
unload();
_roomNumber = roomNumber;
}
const MapScreenDirectionDef *ReahSchizmMapLoader::getScreenDirection(uint screen, uint direction) {
if (screen < kFirstScreen)
return nullptr;
screen -= kFirstScreen;
if (screen >= kNumScreens)
return nullptr;
if (!_isLoaded)
load();
return _screenDirections[screen][direction].get();
}
void ReahSchizmMapLoader::load() {
// This is loaded even if the open fails
_isLoaded = true;
Common::Path mapFileName(Common::String::format("Map/Room%02i.map", static_cast(_roomNumber)));
Common::File mapFile;
if (!mapFile.open(mapFileName))
return;
byte screenDefOffsets[kNumScreens * kNumDirections * 4];
if (!mapFile.seek(16))
error("Error skipping map file header");
if (mapFile.read(screenDefOffsets, sizeof(screenDefOffsets)) != sizeof(screenDefOffsets))
error("Error reading map offset table");
for (uint screen = 0; screen < kNumScreens; screen++) {
for (uint direction = 0; direction < kNumDirections; direction++) {
uint32 offset = READ_LE_UINT32(screenDefOffsets + (kNumDirections * screen + direction) * 4);
if (!offset)
continue;
// QUIRK: The stone game in the tower in Reah (Room 06) has two 0cb screens and the second one is damaged,
// so it must be ignored.
if (!_screenDirections[screen][direction]) {
if (!mapFile.seek(offset))
error("Error seeking to screen data");
_screenDirections[screen][direction] = loadScreenDirectionDef(mapFile);
}
}
}
}
void ReahSchizmMapLoader::unload() {
for (uint screen = 0; screen < kNumScreens; screen++)
for (uint direction = 0; direction < kNumDirections; direction++)
_screenDirections[screen][direction].reset();
_isLoaded = false;
}
class AD2044MapLoader : public MapLoader {
public:
AD2044MapLoader();
void setRoomNumber(uint roomNumber) override;
const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction) override;
void unload() override;
private:
struct ScreenOverride {
uint roomNumber;
uint screenNumber;
int actualMapFileID;
};
void load();
static const uint kFirstScreen = 0xa0;
uint _roomNumber;
uint _screenNumber;
bool _isLoaded;
Common::SharedPtr _currentMap;
static const ScreenOverride sk_screenOverrides[];
};
const AD2044MapLoader::ScreenOverride AD2044MapLoader::sk_screenOverrides[] = {
// Room 1
{1, 0xb6, 145}, // After pushing the button to open the capsule
{1, 0x66, 138}, // Looking at banner
{1, 0x6a, 142}, // Opening an apple on the table
{1, 0x6b, 143}, // Clicking the tablet in the apple
{1, 0x6c, 144}, // Table facing the center of the room with soup bowl empty
// Room 23
{23, 0xa3, 103}, // Looking at high mirror
{23, 0xa4, 104}, // After taking mirror
{23, 0xb9, 125}, // Bathroom looking down the stairs
//{23, 0xb9, 126}, // ???
{23, 0xbb, 127}, // Bathroom entry point
{23, 0xbc, 128}, // Sink
{23, 0xbd, 129}, // Looking at toilet, seat down
{23, 0xbe, 130}, // Looking at toilet, seat up
{23, 0x61, 133}, // Bathroom looking at boots
{23, 0x62, 134}, // Looking behind boots
{23, 0x63, 135}, // Standing behind toilet looking at sink
{23, 0x64, 136}, // Looking under toilet
};
AD2044MapLoader::AD2044MapLoader() : _roomNumber(0), _screenNumber(0), _isLoaded(false) {
}
void AD2044MapLoader::setRoomNumber(uint roomNumber) {
if (_roomNumber != roomNumber)
unload();
_roomNumber = roomNumber;
}
const MapScreenDirectionDef *AD2044MapLoader::getScreenDirection(uint screen, uint direction) {
if (screen != _screenNumber)
unload();
_screenNumber = screen;
if (!_isLoaded)
load();
return _currentMap.get();
}
void AD2044MapLoader::load() {
// This is loaded even if the open fails
_isLoaded = true;
int scrFileID = -1;
for (const ScreenOverride &screenOverride : sk_screenOverrides) {
if (screenOverride.roomNumber == _roomNumber && screenOverride.screenNumber == _screenNumber) {
scrFileID = screenOverride.actualMapFileID;
break;
}
}
if (_roomNumber == 87) {
uint highDigit = (_screenNumber & 0xf0) >> 4;
uint lowDigit = _screenNumber & 0x0f;
scrFileID = 8700 + static_cast(highDigit * 10u + lowDigit);
}
if (scrFileID < 0) {
if (_screenNumber < kFirstScreen)
return;
uint adjustedScreenNumber = _screenNumber - kFirstScreen;
if (adjustedScreenNumber > 99)
return;
scrFileID = static_cast(_roomNumber * 100u + adjustedScreenNumber);
}
Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", scrFileID));
Common::File mapFile;
debug(1, "Loading screen map %s", mapFileName.toString(Common::Path::kNativeSeparator).c_str());
if (!mapFile.open(mapFileName)) {
error("Couldn't resolve map file for room %u screen %x", _roomNumber, _screenNumber);
}
_currentMap = loadScreenDirectionDef(mapFile);
}
void AD2044MapLoader::unload() {
_currentMap.reset();
_isLoaded = false;
}
ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false), isEntryScript(false), puzzleWasSet(false),
panInteractionID(0), clickInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
}
OSEvent::OSEvent() : type(kOSEventTypeInvalid), keymappedEvent(kKeymappedEventNone), timestamp(0) {
}
void Runtime::RenderSection::init(const Common::Rect ¶mRect, const Graphics::PixelFormat &fmt) {
rect = paramRect;
pixFmt = fmt;
if (paramRect.isEmpty())
surf.reset();
else {
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();
}
Common::Path sfxPath_("Sfx/");
sfxPath_.appendInPlace(sfxPath);
Common::File f;
if (!f.open(sfxPath_)) {
warning("SfxData::load: Could not open sample file '%s'", sfxPath_.toString(Common::Path::kNativeSeparator).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) {
}
AnimFrameRange::AnimFrameRange() : animationNum(0), firstFrame(0), lastFrame(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), disc(0), havePendingPostSwapScreenReset(false),
musicTrack(0), musicVolume(100), musicActive(true), musicMuteDisabled(false), animVolume(100),
loadedAnimation(0), animDisplayingFrame(0), haveIdleAnimationLoop(false), idleAnimNum(0), idleFirstFrame(0), idleLastFrame(0)
{
}
SaveGameSnapshot::PagedInventoryItem::PagedInventoryItem() : page(0), slot(0), itemID(0) {
}
void SaveGameSnapshot::PagedInventoryItem::write(Common::WriteStream *stream) const {
stream->writeByte(page);
stream->writeByte(slot);
stream->writeByte(itemID);
}
void SaveGameSnapshot::PagedInventoryItem::read(Common::ReadStream *stream, uint saveGameVersion) {
page = stream->readByte();
slot = stream->readByte();
itemID = stream->readByte();
}
SaveGameSnapshot::PlacedInventoryItem::PlacedInventoryItem() : locationID(0), itemID(0) {
}
void SaveGameSnapshot::PlacedInventoryItem::write(Common::WriteStream *stream) const {
stream->writeUint32BE(locationID);
stream->writeByte(itemID);
}
void SaveGameSnapshot::PlacedInventoryItem::read(Common::ReadStream *stream, uint saveGameVersion) {
locationID = stream->readUint32BE();
itemID = stream->readByte();
}
SaveGameSnapshot::SaveGameSnapshot() : hero(0), swapOutRoom(0), swapOutScreen(0), swapOutDirection(0),
escOn(false), numStates(1), listenerX(0), listenerY(0), listenerAngle(0), inventoryPage(0), inventoryActiveItem(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->writeUint32BE(states[sti]->disc);
stream->writeByte(states[sti]->havePendingPostSwapScreenReset ? 1 : 0);
stream->writeByte(states[sti]->haveIdleAnimationLoop ? 1 : 0);
if (states[sti]->haveIdleAnimationLoop) {
stream->writeUint32BE(states[sti]->idleAnimNum);
stream->writeUint32BE(states[sti]->idleFirstFrame);
stream->writeUint32BE(states[sti]->idleLastFrame);
}
}
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());
stream->writeUint32BE(placedItems.size());
stream->writeUint32BE(pagedItems.size());
stream->writeByte(inventoryPage);
stream->writeByte(inventoryActiveItem);
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);
}
for (const PlacedInventoryItem &item : placedItems)
item.write(stream);
for (const PagedInventoryItem &item : pagedItems)
item.write(stream);
}
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 >= 10)
states[sti]->disc = stream->readUint32BE();
if (saveVersion >= 7)
states[sti]->havePendingPostSwapScreenReset = (stream->readByte() != 0);
if (saveVersion >= 10)
states[sti]->haveIdleAnimationLoop = (stream->readByte() != 0);
else
states[sti]->haveIdleAnimationLoop = false;
if (states[sti]->haveIdleAnimationLoop) {
states[sti]->idleAnimNum = stream->readUint32BE();
states[sti]->idleFirstFrame = stream->readUint32BE();
states[sti]->idleLastFrame = stream->readUint32BE();
}
}
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();
uint numPlacedItems = 0;
uint numPagedItems = 0;
if (saveVersion >= 10) {
numPlacedItems = stream->readUint32BE();
numPagedItems = stream->readUint32BE();
this->inventoryPage = stream->readByte();
this->inventoryActiveItem = stream->readByte();
} else {
this->inventoryPage = 0;
this->inventoryActiveItem = 0;
}
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;
}
for (uint i = 0; i < numPlacedItems; i++) {
PlacedInventoryItem item;
item.read(stream, saveVersion);
placedItems.push_back(item);
}
for (uint i = 0; i < numPagedItems; i++) {
PagedInventoryItem item;
item.read(stream, saveVersion);
pagedItems.push_back(item);
}
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, MidiDriver *midiDrv, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
: _system(system), _mixer(mixer), _midiDrv(midiDrv), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _disc(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), _preloadSounds(false),
_lowQualityGraphicsMode(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), _language(defaultLanguage), _charSet(kCharSetLatin),
_isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0),
_inventoryActivePage(0), _keepStaticAnimationInIdle(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"));
_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 &subtitleRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt) {
_gameSection.init(gameRect, pixFmt);
_menuSection.init(menuRect, pixFmt);
_traySection.init(trayRect, pixFmt);
_fullscreenMenuSection.init(fullscreenMenuRect, pixFmt);
if (!subtitleRect.isEmpty())
_subtitleSection.init(subtitleRect, pixFmt);
_placedItemBackBufferSection.init(Common::Rect(), pixFmt);
}
void Runtime::loadCursors(const char *exeName) {
if (_gameID == GID_AD2044) {
const int staticCursorIDs[] = {0, 29, 30, 31, 32, 33, 34, 35, 36, 39, 40, 41, 50, 96, 97, 99};
const int animatedCursorIDs[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
_cursors.resize(100);
for (int cid : staticCursorIDs) {
Common::String cursorPath = Common::String::format("gfx/CURSOR%02i.CUR", static_cast(cid));
Common::File f;
Image::IcoCurDecoder decoder;
if (!f.open(Common::Path(cursorPath)) || !decoder.open(f))
error("Couldn't load cursor %s", cursorPath.c_str());
uint numItems = decoder.numItems();
if (numItems < 1)
error("Cursor %s had no items", cursorPath.c_str());
Graphics::Cursor *cursor = decoder.loadItemAsCursor(0);
if (!cursor)
error("Couldn't load cursor %s", cursorPath.c_str());
_cursors[cid] = staticCursorToAnimatedCursor(Common::SharedPtr(cursor));
}
for (int cid : animatedCursorIDs) {
Common::String cursorPath = Common::String::format("gfx/CURSOR%i.ani", static_cast(cid));
Common::File f;
Image::AniDecoder decoder;
if (!f.open(Common::Path(cursorPath)) || !decoder.open(f))
error("Couldn't load cursor %s", cursorPath.c_str());
_cursors[cid] = aniFileToAnimatedCursor(decoder);
}
} else {
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] = winCursorGroupToAnimatedCursor(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] = winCursorGroupToAnimatedCursor(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;
}
if (_gameID == GID_AD2044) {
_namedCursors["CUR_PRZOD"] = 4; // Przod = forward
_namedCursors["CUR_PRAWO"] = 3; // Prawo = right
_namedCursors["CUR_LEWO"] = 1; // Lewo = left
_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier
_namedCursors["CUR_NAC"] = 5; // Nac = top? Not sure. But this is the finger pointer.
_namedCursors["CUR_TYL"] = 2; // Tyl = back
_namedCursors["CUR_OTWORZ"] = 11; // Otworz = open
_namedCursors["CUR_WEZ"] = 8; // Wez = Pick up
_namedCursors["CUR_ZOSTAW"] = 7; // Put down
}
_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;
}
void Runtime::setPreloadSounds(bool preloadSounds) {
_preloadSounds = preloadSounds;
}
void Runtime::setLowQualityGraphicsMode(bool lowQualityGraphicsMode) {
_lowQualityGraphicsMode = lowQualityGraphicsMode;
}
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();
refreshCursor(timestamp);
return true;
}
bool Runtime::bootGame(bool newGame) {
assert(_gameState == kGameStateBoot);
if (!ConfMan.hasKey("vcruise_increase_drag_distance") || ConfMan.getBool("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...");
if (_gameID == GID_AD2044) {
loadAD2044ExecutableResources();
Common::File tabFile;
if (tabFile.open(Common::Path("anims/ANIM0087.TAB")))
loadTabData(_examineAnimIDToFrameRange, 87, &tabFile);
else
error("Failed to load inspection animations");
} else
loadReahSchizmIndex();
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 if (_gameID == GID_REAH) {
StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
startConfig.disc = 1;
startConfig.room = 1;
startConfig.screen = 0xb0;
startConfig.direction = 0;
} else if (_gameID == GID_AD2044) {
StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
startConfig.disc = 1;
startConfig.room = 1;
startConfig.screen = 0xa5;
startConfig.direction = 0;
} else
error("Don't have a start config for this game");
if (_gameID != GID_AD2044) {
_trayBackgroundGraphic = loadGraphic("Pocket", true);
_trayHighlightGraphic = loadGraphic("Select", true);
_trayCompassGraphic = loadGraphic("Select_1", true);
_trayCornerGraphic = loadGraphic("Select_2", true);
}
if (_gameID == GID_AD2044)
_backgroundGraphic = loadGraphicFromPath("SCR0.BMP", 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;
}
}
_language = lang;
debug(2, "Language index: %u Default language index: %u", _languageIndex, _defaultLanguageIndex);
Common::CodePage codePage = Common::CodePage::kASCII;
resolveCodePageForLanguage(lang, codePage, _charSet);
bool subtitlesLoadedOK = false;
if (_gameID == GID_AD2044) {
subtitlesLoadedOK = true;
} else {
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.");
#ifdef USE_FREETYPE2
if (_gameID == GID_AD2044) {
Common::File *f = new Common::File();
if (f->open("gfx/AD2044.TTF"))
_subtitleFontKeepalive.reset(Graphics::loadTTFFont(f, DisposeAfterUse::YES, 16, Graphics::kTTFSizeModeCharacter, 108, 72, Graphics::kTTFRenderModeLight));
else
delete f;
} else {
Common::String fontFile;
switch (_charSet) {
case kCharSetGreek:
case kCharSetLatin:
case kCharSetCyrillic:
default:
fontFile = "NotoSans-Regular.ttf";
break;
case kCharSetChineseSimplified:
fontFile = "NotoSansSC-Regular.otf";
break;
case kCharSetChineseTraditional:
fontFile = "NotoSansTC-Regular.otf";
break;
case kCharSetJapanese:
fontFile = "NotoSansJP-Regular.otf";
break;
}
_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive(fontFile, 16, Graphics::kTTFSizeModeCharacter, 0, 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");
if (_gameID != GID_AD2044) {
_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);
}
}
}
if (_gameID == GID_AD2044)
_mapLoader.reset(new AD2044MapLoader());
else
_mapLoader.reset(new ReahSchizmMapLoader());
_gameState = kGameStateIdle;
if (newGame) {
if (_gameID == GID_AD2044 || (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);
}
void Runtime::onMidiTimer() {
Common::StackLock lock(_midiPlayerMutex);
if (_musicMidiPlayer)
_musicMidiPlayer->onMidiTimer();
}
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;
case kKeymappedEventPutItem:
cheatPutItem();
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 == kOSEventTypeKeymappedEvent && evt.keymappedEvent == kKeymappedEventEscape) {
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()) {
Graphics::Palette p(_animDecoder->getPalette(), 256);
_gameSection.surf->simpleBlitFrom(*surface, copyRect, copyRect.origin(), Graphics::FLIP_NONE, false, 255, &p);
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->simpleBlitFrom(*sourceSection->surf, rect, rect.origin());
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;
}
redrawTray();
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