/* 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