/* 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 "engines/util.h" #include "common/compression/gentee_installer.h" #include "common/config-manager.h" #include "common/events.h" #include "common/file.h" #include "common/stream.h" #include "common/savefile.h" #include "common/system.h" #include "common/algorithm.h" #include "common/translation.h" #include "audio/mididrv.h" #include "audio/mixer.h" #include "gui/message.h" #include "vcruise/runtime.h" #include "vcruise/vcruise.h" namespace VCruise { VCruiseEngine::VCruiseEngine(OSystem *syst, const VCruiseGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) { const Common::FSNode gameDataDir(ConfMan.getPath("path")); } VCruiseEngine::~VCruiseEngine() { } void VCruiseEngine::handleEvents() { Common::Event evt; Common::EventManager *eventMan = _system->getEventManager(); while (eventMan->pollEvent(evt)) { switch (evt.type) { case Common::EVENT_LBUTTONDOWN: _runtime->onLButtonDown(evt.mouse.x, evt.mouse.y); break; case Common::EVENT_LBUTTONUP: _runtime->onLButtonUp(evt.mouse.x, evt.mouse.y); break; case Common::EVENT_MOUSEMOVE: _runtime->onMouseMove(evt.mouse.x, evt.mouse.y); break; case Common::EVENT_CUSTOM_ENGINE_ACTION_START: _runtime->onKeymappedEvent(static_cast(evt.customType)); break; default: break; } } } void VCruiseEngine::staticHandleMidiTimer(void *refCon) { static_cast(refCon)->handleMidiTimer(); } void VCruiseEngine::handleMidiTimer() { _runtime->onMidiTimer(); } Common::Error VCruiseEngine::run() { #if !defined(USE_JPEG) if (_gameDescription->desc.flags & VCRUISE_GF_NEED_JPEG) { return Common::Error(Common::kUnknownError, _s("This game requires JPEG support, which was not compiled in.")); } #endif #if !defined(USE_OGG) || !defined(USE_VORBIS) if (_gameDescription->desc.flags & VCRUISE_GF_WANT_OGG_VORBIS) { GUI::MessageDialog dialog( _("Music for this game requires Ogg Vorbis support, which was not compiled in.\n" "The game will still play, but will not have any music."), _("OK")); dialog.runModal(); } #endif #if !defined(USE_MAD) if (_gameDescription->desc.flags & VCRUISE_GF_WANT_MP3) { GUI::MessageDialog dialog( _("Music for this game requires MP3 support, which was not compiled in.\n" "The game will still play, but will not have any music."), _("OK")); dialog.runModal(); } #endif Common::ScopedPtr midiDrv; if (_gameDescription->gameID == GID_AD2044) { MidiDriver::DeviceHandle midiDevHdl = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM); if (midiDevHdl) { midiDrv.reset(MidiDriver::createMidi(midiDevHdl)); if (midiDrv->open() != 0) midiDrv.reset(); } } if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE) { Common::File *f = new Common::File(); if (!f->open(_gameDescription->desc.filesDescriptions[0].fileName)) error("Couldn't open installer package '%s'", _gameDescription->desc.filesDescriptions[0].fileName); Common::Archive *installerPackageArchive = Common::createGenteeInstallerArchive(f, "#setuppath#\\", true); if (!installerPackageArchive) error("Couldn't load installer package '%s'", _gameDescription->desc.filesDescriptions[0].fileName); SearchMan.add("VCruiseInstallerPackage", installerPackageArchive); } syncSoundSettings(); // Figure out screen layout Common::Point size; if (_gameDescription->gameID == GID_AD2044) { size = Common::Point(640, 480); Common::Point traySize = Common::Point(640, 97); Common::Point menuBarSize = Common::Point(188, 102); Common::Point videoTL = Common::Point(20, 21); Common::Point videoSize = Common::Point(432, 307); _menuBarRect = Common::Rect(size.x - menuBarSize.x, 0, size.x, menuBarSize.y); _videoRect = Common::Rect(videoTL, videoTL + videoSize); _trayRect = Common::Rect(0, size.y - traySize.y, size.x, size.y); _subtitleRect = Common::Rect(_videoRect.left, _videoRect.bottom, _videoRect.right, _trayRect.top); } else { Common::Point videoSize; Common::Point traySize; Common::Point menuBarSize; if (_gameDescription->gameID == GID_REAH) { videoSize = Common::Point(608, 348); menuBarSize = Common::Point(640, 44); traySize = Common::Point(640, 88); } else if (_gameDescription->gameID == GID_SCHIZM) { videoSize = Common::Point(640, 360); menuBarSize = Common::Point(640, 32); traySize = Common::Point(640, 88); } else { error("Unknown game"); } size.x = videoSize.x; if (menuBarSize.x > size.x) size.x = menuBarSize.x; if (traySize.x > size.x) size.x = traySize.x; size.y = videoSize.y + menuBarSize.y + traySize.y; Common::Point menuTL = Common::Point((size.x - menuBarSize.x) / 2, 0); Common::Point videoTL = Common::Point((size.x - videoSize.x) / 2, menuTL.y + menuBarSize.y); Common::Point trayTL = Common::Point((size.x - traySize.x) / 2, videoTL.y + videoSize.y); _menuBarRect = Common::Rect(menuTL.x, menuTL.y, menuTL.x + menuBarSize.x, menuTL.y + menuBarSize.y); _videoRect = Common::Rect(videoTL.x, videoTL.y, videoTL.x + videoSize.x, videoTL.y + videoSize.y); _trayRect = Common::Rect(trayTL.x, trayTL.y, trayTL.x + traySize.x, trayTL.y + traySize.y); } // TODO: Optionally support CLUT8 for AD 2044 initGraphics(size.x, size.y, nullptr); if (_system->getScreenFormat().isCLUT8()) return Common::kUnsupportedColorMode; _system->fillScreen(0); _runtime.reset(new Runtime(_system, _mixer, midiDrv.get(), _rootFSNode, _gameDescription->gameID, _gameDescription->defaultLanguage)); _runtime->initSections(_videoRect, _menuBarRect, _trayRect, _subtitleRect, Common::Rect(640, 480), _system->getScreenFormat()); if (midiDrv) midiDrv->setTimerCallback(this, VCruiseEngine::staticHandleMidiTimer); const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName; if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE) exeName = "Schizm.exe"; _runtime->loadCursors(exeName); if (ConfMan.getBool("vcruise_debug")) { _runtime->setDebugMode(true); } if (ConfMan.getBool("vcruise_fast_animations")) { _runtime->setFastAnimationMode(true); } if (ConfMan.getBool("vcruise_preload_sounds")) { _runtime->setPreloadSounds(true); } if (ConfMan.getBool("vcruise_use_4bit")) { _runtime->setLowQualityGraphicsMode(true); } if (ConfMan.hasKey("save_slot")) { int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0) { (void)_runtime->bootGame(false); Common::Error err = loadGameState(saveSlot); if (err.getCode() != Common::kNoError) return err; } } // Run the game while (!shouldQuit()) { handleEvents(); if (!_runtime->runFrame()) break; _runtime->drawFrame(); _system->delayMillis(5); } if (midiDrv) midiDrv->setTimerCallback(nullptr, nullptr); _runtime.reset(); if (midiDrv) midiDrv->close(); if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE) SearchMan.remove("VCruiseInstallerPackage"); // Flush any settings changes made in-game ConfMan.flushToDisk(); return Common::kNoError; } void VCruiseEngine::pauseEngineIntern(bool pause) { Engine::pauseEngineIntern(pause); } bool VCruiseEngine::hasFeature(EngineFeature f) const { switch (f) { case kSupportsReturnToLauncher: case kSupportsSavingDuringRuntime: case kSupportsLoadingDuringRuntime: return true; default: return false; }; } void VCruiseEngine::syncSoundSettings() { // Sync the engine with the config manager int soundVolumeMusic = ConfMan.getInt("music_volume"); int soundVolumeSFX = ConfMan.getInt("sfx_volume"); int soundVolumeSpeech = ConfMan.getInt("speech_volume"); bool mute = false; if (ConfMan.hasKey("mute")) mute = ConfMan.getBool("mute"); // We need to handle the speech mute separately here. This is because the // engine code should be able to rely on all speech sounds muted when the // user specified subtitles only mode, which results in "speech_mute" to // be set to "true". The global mute setting has precedence over the // speech mute setting though. bool speechMute = mute; if (!speechMute) speechMute = ConfMan.getBool("speech_mute"); bool muteSound = ConfMan.getBool("vcruise_mute_sound"); if (ConfMan.hasKey("vcruise_mute_sound")) muteSound = ConfMan.getBool("vcruise_mute_sound"); // We don't mute music here because Schizm has a special behavior that bypasses music mute when using one // of the ships to transition zones. _mixer->muteSoundType(Audio::Mixer::kPlainSoundType, mute || muteSound); _mixer->muteSoundType(Audio::Mixer::kMusicSoundType, mute); _mixer->muteSoundType(Audio::Mixer::kSFXSoundType, mute || muteSound); _mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, speechMute || muteSound); _mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, Audio::Mixer::kMaxMixerVolume); _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic); _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, soundVolumeSFX); _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, soundVolumeSpeech); } Common::Error VCruiseEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) { _runtime->saveGame(stream); if (stream->err()) return Common::Error(Common::kWritingFailed); return Common::Error(Common::kNoError); } Common::Error VCruiseEngine::loadGameStream(Common::SeekableReadStream *stream) { LoadGameOutcome loadGameOutcome = _runtime->loadGame(stream); switch (loadGameOutcome) { case kLoadGameOutcomeSucceeded: return Common::Error(Common::kNoError); case kLoadGameOutcomeSaveDataCorrupted: { GUI::MessageDialog dialog(_("Failed to load save, the save data appears to be damaged.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); case kLoadGameOutcomeMissingVersion: { GUI::MessageDialog dialog(_("Failed to read version information from save file.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); case kLoadGameOutcomeInvalidVersion: { GUI::MessageDialog dialog(_("Failed to load save, the save file doesn't contain valid version information.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); case kLoadGameOutcomeSaveIsTooNew: { GUI::MessageDialog dialog(_("Saved game was created with a newer version of ScummVM. Unable to load.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); case kLoadGameOutcomeSaveIsTooOld: { GUI::MessageDialog dialog(_("Saved game was created with an earlier, incompatible version of ScummVM. Unable to load.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); default: { GUI::MessageDialog dialog(_("An unknown error occurred while attempting to load the saved game.")); dialog.runModal(); } return Common::Error(Common::kReadingFailed); }; return Common::Error(Common::kNoError); } bool VCruiseEngine::canSaveAutosaveCurrently() { return _runtime->canSave(false); } bool VCruiseEngine::canSaveGameStateCurrently(Common::U32String *msg) { return _runtime->canSave(false); } bool VCruiseEngine::canLoadGameStateCurrently(Common::U32String *msg) { return _runtime->canLoad(); } void VCruiseEngine::initializePath(const Common::FSNode &gamePath) { Engine::initializePath(gamePath); const char *gameSubPath = nullptr; if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE) { if (_gameDescription->gameID == GID_SCHIZM) gameSubPath = "Schizm"; } if (gameSubPath) { const int kNumCasePasses = 3; for (int casePass = 0; casePass < kNumCasePasses; casePass++) { Common::String subPathStr(gameSubPath); if (casePass == 1) subPathStr.toUppercase(); else if (casePass == 2) subPathStr.toLowercase(); Common::FSNode gameSubDir = gamePath.getChild(subPathStr); if (gameSubDir.isDirectory()) { SearchMan.addDirectory("VCruiseGameDir", gameSubDir, 0, 3); break; } if (casePass != kNumCasePasses - 1) warning("Expected to find subpath '%s' in the game directory but couldn't find it", gameSubPath); } } _rootFSNode = gamePath; } bool VCruiseEngine::hasDefaultSave() { return hasAnySave(); } bool VCruiseEngine::hasAnySave() { Common::StringArray saveFiles = getSaveFileManager()->listSavefiles(_targetName + ".*"); return saveFiles.size() > 0; } Common::Error VCruiseEngine::loadMostRecentSave() { Common::SaveFileManager *sfm = getSaveFileManager(); Common::StringArray saveFiles = sfm->listSavefiles(_targetName + ".*"); uint64 highestDateTime = 0; uint32 highestPlayTime = 0; Common::String highestSaveFileName; for (const Common::String &saveFileName : saveFiles) { Common::InSaveFile *saveFile = sfm->openForLoading(saveFileName); if (!saveFile) { warning("Couldn't load save file '%s' to determine recency", saveFileName.c_str()); continue; } ExtendedSavegameHeader header; bool loadedHeader = getMetaEngine()->readSavegameHeader(saveFile, &header, true); if (!loadedHeader) { warning("Couldn't parse header from '%s'", saveFileName.c_str()); continue; } uint8 day = 0; uint8 month = 0; uint16 year = 0; uint8 hour = 0; uint8 minute = 0; MetaEngine::decodeSavegameDate(&header, year, month, day); MetaEngine::decodeSavegameTime(&header, hour, minute); uint64 dateTime = static_cast(year) << 32; dateTime |= static_cast(month) << 24; dateTime |= static_cast(day) << 16; dateTime |= static_cast(hour) << 8; dateTime |= minute; uint32 playTime = header.playtime; if (dateTime > highestDateTime || (dateTime == highestDateTime && playTime > highestPlayTime)) { highestSaveFileName = saveFileName; highestDateTime = dateTime; highestPlayTime = playTime; } } if (highestSaveFileName.empty()) { warning("Couldn't find any valid saves to load"); return Common::Error(Common::kReadingFailed); } Common::String slotStr = highestSaveFileName.substr(_targetName.size() + 1); int slot = 0; if (sscanf(slotStr.c_str(), "%i", &slot) != 1) { warning("Couldn't parse save slot ID from %s", highestSaveFileName.c_str()); return Common::Error(Common::kReadingFailed); } return loadGameState(slot); } } // End of namespace VCruise