/* 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/system.h" #include "common/random.h" #include "common/debug-channels.h" #include "common/config-manager.h" #include "common/memstream.h" #include "common/compression/installshield_cab.h" #include "common/serializer.h" #include "engines/nancy/nancy.h" #include "engines/nancy/resource.h" #include "engines/nancy/cif.h" #include "engines/nancy/iff.h" #include "engines/nancy/input.h" #include "engines/nancy/sound.h" #include "engines/nancy/graphics.h" #include "engines/nancy/console.h" #include "engines/nancy/util.h" #include "engines/nancy/action/conversation.h" #include "engines/nancy/state/logo.h" #include "engines/nancy/state/scene.h" #include "engines/nancy/state/help.h" #include "engines/nancy/state/map.h" #include "engines/nancy/state/credits.h" #include "engines/nancy/state/mainmenu.h" #include "engines/nancy/state/setupmenu.h" #include "engines/nancy/state/loadsave.h" #include "engines/nancy/state/savedialog.h" namespace Nancy { NancyEngine *g_nancy; NancyEngine::NancyEngine(OSystem *syst, const NancyGameDescription *gd) : Engine(syst), _gameDescription(gd), _system(syst), _datFileMajorVersion(1), _datFileMinorVersion(0), _false(gd->gameType <= kGameTypeNancy2 ? 1 : 0), _true(gd->gameType <= kGameTypeNancy2 ? 2 : 1) { g_nancy = this; _randomSource = new Common::RandomSource("Nancy"); _randomSource->setSeed(Common::RandomSource::generateNewSeed()); _input = new InputManager(); _sound = new SoundManager(); _graphics = new GraphicsManager(); _cursor = new CursorManager(); _resource = new ResourceManager(); _hasJustSaved = false; } NancyEngine::~NancyEngine() { destroyState(NancyState::kLogo); destroyState(NancyState::kCredits); destroyState(NancyState::kMap); destroyState(NancyState::kHelp); destroyState(NancyState::kScene); destroyState(NancyState::kMainMenu); destroyState(NancyState::kSetup); destroyState(NancyState::kLoadSave); destroyState(NancyState::kSaveDialog); delete _randomSource; delete _graphics; delete _cursor; delete _input; delete _sound; delete _resource; for (auto &data : _engineData) { delete data._value; } } NancyEngine *NancyEngine::create(GameType type, OSystem *syst, const NancyGameDescription *gd) { if (type >= kGameTypeVampire && type <= kGameTypeNancy11) { return new NancyEngine(syst, gd); } error("Unknown GameType"); } Common::Error NancyEngine::loadGameStream(Common::SeekableReadStream *stream) { Common::Serializer ser(stream, nullptr); return synchronize(ser); } Common::Error NancyEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) { Common::Serializer ser(nullptr, stream); return synchronize(ser); } bool NancyEngine::canLoadGameStateCurrently(Common::U32String *msg) { return canSaveGameStateCurrently(); } bool NancyEngine::canSaveGameStateCurrently(Common::U32String *msg) { return State::Scene::hasInstance() && NancySceneState._state == State::Scene::kRun && NancySceneState.getActiveConversation() == nullptr && NancySceneState.getActiveMovie() == nullptr && !NancySceneState.isRunningAd(); } void NancyEngine::secondChance() { uint secondChanceSlot = getMetaEngine()->getMaximumSaveSlot(); saveGameState(secondChanceSlot, "SECOND CHANCE", true); } void NancyEngine::errorString(const char *buf_input, char *buf_output, int buf_output_size) { if (State::Scene::hasInstance()) { if (NancySceneState._state == State::Scene::kLoad) { // Error while loading scene snprintf(buf_output, buf_output_size, "While loading scene S%u, frame %u, action record %u:\n%s", NancySceneState._sceneState.currentScene.sceneID, NancySceneState._sceneState.currentScene.frameID, NancySceneState._actionManager.getActionRecords().size(), buf_input); } else { // Error while running snprintf(buf_output, buf_output_size, "In current scene S%u, frame %u:\n%s", NancySceneState._sceneState.currentScene.sceneID, NancySceneState._sceneState.currentScene.frameID, buf_input); } } else { strncpy(buf_output, buf_input, buf_output_size); if (buf_output_size > 0) buf_output[buf_output_size - 1] = '\0'; } } bool NancyEngine::hasFeature(EngineFeature f) const { return (f == kSupportsReturnToLauncher) || (f == kSupportsLoadingDuringRuntime) || (f == kSupportsSavingDuringRuntime) || (f == kSupportsChangingOptionsDuringRuntime) || (f == kSupportsSubtitleOptions); } const char *NancyEngine::getCopyrightString() const { return "Copyright 1989-1997 David P Gray, All Rights Reserved."; } uint32 NancyEngine::getGameFlags() const { return _gameDescription->desc.flags; } const char *NancyEngine::getGameId() const { return _gameDescription->desc.gameId; } GameType NancyEngine::getGameType() const { return _gameDescription->gameType; } Common::Language NancyEngine::getGameLanguage() const { return _gameDescription->desc.language; } Common::Platform NancyEngine::getPlatform() const { return _gameDescription->desc.platform; } const StaticData &NancyEngine::getStaticData() const { return _staticData; } const EngineData *NancyEngine::getEngineData(const Common::String &name) const { if (_engineData.contains(name)) { return _engineData[name]; } return nullptr; } void NancyEngine::setState(NancyState::NancyState state, NancyState::NancyState overridePrevious) { // Handle special cases first switch (state) { case NancyState::kBoot: bootGameEngine(); setState(NancyState::kLogo); return; case NancyState::kMainMenu: { if (!ConfMan.hasKey("original_menus") || ConfMan.getBool("original_menus")) { break; } // Do not use the original engine's menus, call the GMM instead openMainMenuDialog(); if (shouldQuit()) { return; } _input->forceCleanInput(); return; } default: break; } if (overridePrevious != NancyState::kNone) { _gameFlow.prevState = overridePrevious; } else { _gameFlow.prevState = _gameFlow.curState; } _gameFlow.nextState = state; _gameFlow.changingState = true; } void NancyEngine::setToPreviousState() { setState(_gameFlow.prevState); } void NancyEngine::setMouseEnabled(bool enabled) { _cursor->showCursor(enabled); _input->setMouseInputEnabled(enabled); } void NancyEngine::addDeferredLoader(Common::SharedPtr &loaderPtr) { _deferredLoaderObjects.push_back(Common::WeakPtr(loaderPtr)); } Common::Error NancyEngine::run() { setDebugger(new NancyConsole()); // Set the default number of saves for earlier games if (!ConfMan.hasKey("nancy_max_saves", ConfMan.getActiveDomainName())) { if (getGameType() <= kGameTypeNancy7) { ConfMan.setInt("nancy_max_saves", 8, ConfMan.getActiveDomainName()); } } // Boot the engine setState(NancyState::kBoot); // Check if we need to load a save state from the launcher if (ConfMan.hasKey("save_slot")) { int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0 && saveSlot <= getMetaEngine()->getMaximumSaveSlot()) { // Set to Scene but do not do the loading yet setState(NancyState::kScene); } } bool graphicsWereSuppressed = false; // Main loop while (true) { _input->processEvents(); if (shouldQuit()) { break; } uint32 frameEndTime = _system->getMillis() + 16; if (!graphicsWereSuppressed) { _cursor->setCursorType(CursorManager::kNormalArrow); } State::State *s; if (_gameFlow.changingState) { _gameFlow.curState = _gameFlow.nextState; _gameFlow.nextState = NancyState::kNone; s = getStateObject(_gameFlow.curState); if (s) { s->onStateEnter(_gameFlow.curState); } _gameFlow.changingState = false; } s = getStateObject(_gameFlow.curState); if (s) { s->process(); } graphicsWereSuppressed = _graphics->_isSuppressed; _graphics->draw(); if (_gameFlow.changingState) { _graphics->clearObjects(); s = getStateObject(_gameFlow.curState); if (s) { if (s->onStateExit(_gameFlow.nextState)) { destroyState(_gameFlow.curState); } } } _system->updateScreen(); // In cases where the graphics were not drawn for a frame, we want to make sure the next // frame is processed as fast as possible. Thus, we skip deferred loaders and the time // delay that normally maintains 60fps if (!graphicsWereSuppressed) { // Use the spare time until the next frame to load larger data objects // Some loading is guaranteed to happen even with no time left, to ensure // slower systems won't be stuck waiting forever if (_deferredLoaderObjects.size()) { uint i = _deferredLoaderObjects.size() - 1; int32 timePerObj = (frameEndTime - g_system->getMillis()) / _deferredLoaderObjects.size(); if (timePerObj < 0) { timePerObj = 0; } for (auto *iter = _deferredLoaderObjects.begin(); iter < _deferredLoaderObjects.end(); ++iter) { if (iter->expired()) { iter = _deferredLoaderObjects.erase(iter); } else { auto objectPtr = iter->lock(); if (objectPtr) { if (objectPtr->load(frameEndTime - (i * timePerObj))) { iter = _deferredLoaderObjects.erase(iter); } --i; } if (_system->getMillis() > frameEndTime) { break; } } } } uint32 frameFinishTime = _system->getMillis(); if (frameFinishTime < frameEndTime) { _system->delayMillis(frameEndTime - frameFinishTime); } } } return Common::kNoError; } void NancyEngine::pauseEngineIntern(bool pause) { State::State *s = getStateObject(_gameFlow.curState); if (s) { if (pause) { s->onStateExit(NancyState::kPause); } else { s->onStateEnter(NancyState::kPause); } } Engine::pauseEngineIntern(pause); } void NancyEngine::bootGameEngine() { // Load paths const Common::FSNode gameDataDir(ConfMan.getPath("path")); SearchMan.addSubDirectoryMatching(gameDataDir, "game"); SearchMan.addSubDirectoryMatching(gameDataDir, "datafiles"); SearchMan.addSubDirectoryMatching(gameDataDir, "ciftree"); SearchMan.addSubDirectoryMatching(gameDataDir, "hdsound"); SearchMan.addSubDirectoryMatching(gameDataDir, "cdsound"); SearchMan.addSubDirectoryMatching(gameDataDir, "hdvideo"); SearchMan.addSubDirectoryMatching(gameDataDir, "cdvideo"); SearchMan.addSubDirectoryMatching(gameDataDir, "iff"); SearchMan.addSubDirectoryMatching(gameDataDir, "art"); SearchMan.addSubDirectoryMatching(gameDataDir, "font"); // Load archive if running a compressed variant if (isCompressed()) { Common::Archive *cabinet = Common::makeInstallShieldArchive("data"); if (cabinet) { SearchMan.add("data1.cab", cabinet); } } _resource->readCifTree("ciftree", "dat", 1); _resource->readCifTree("promotree", "dat", 1); // Read nancy.dat readDatFile(); // Setup mixer syncSoundSettings(); if (getGameType() >= kGameTypeNancy10) { error("Game not supported; Use console to inspect game data"); } IFF *iff = _resource->loadIFF("boot"); if (!iff) error("Failed to load boot script"); // Load BOOT chunks data Common::SeekableReadStream *chunkStream = nullptr; #define LOAD_BOOT_L(t, s) if (chunkStream = iff->getChunkStream(s), chunkStream) { \ _engineData.setVal(s, new t(chunkStream)); \ delete chunkStream; \ } #define LOAD_BOOT(t) LOAD_BOOT_L(t, #t) LOAD_BOOT_L(ImageChunk, "OB0") LOAD_BOOT_L(ImageChunk, "FR0") LOAD_BOOT_L(ImageChunk, "LG0") // One weird version of nancy3 has a partner logo implemented the same way as the other image chunks LOAD_BOOT_L(ImageChunk, "PLG0") // For all other games (starting with nancy4) the partner logo is a larger struct, // containing video and sound data as well. Those go unused, however, so we still // treat is as a simple image. Note the O instead of the 0 above. LOAD_BOOT_L(ImageChunk, "PLGO") LOAD_BOOT(BSUM) // This checks for PLG0, do NOT reorder LOAD_BOOT(VIEW) LOAD_BOOT(PCAL) LOAD_BOOT(INV) LOAD_BOOT(TBOX) LOAD_BOOT(HELP) LOAD_BOOT(CRED) LOAD_BOOT(MENU) LOAD_BOOT(LOAD) LOAD_BOOT(SET) LOAD_BOOT(SDLG) LOAD_BOOT(MAP) LOAD_BOOT(HINT) LOAD_BOOT(SPUZ) LOAD_BOOT(CLOK) LOAD_BOOT(SPEC) LOAD_BOOT(RCPR) LOAD_BOOT(RCLB) LOAD_BOOT(TABL) LOAD_BOOT(MARK) _cursor->init(iff->getChunkStream("CURS")); _graphics->init(); _graphics->loadFonts(iff->getChunkStream("FONT")); preloadCals(); _sound->initSoundChannels(); _sound->loadCommonSounds(iff); delete iff; // Load convo texts and autotext auto *bsum = GetEngineData(BSUM); if (bsum && !bsum->conversationTextsFilename.empty() && !bsum->autotextFilename.empty()) { iff = _resource->loadIFF(bsum->conversationTextsFilename); if (!iff) { error("Could not load CONVO IFF"); } if (chunkStream = iff->getChunkStream("CVTX"), chunkStream) { _engineData.setVal("CONVO", new CVTX(chunkStream)); delete chunkStream; } delete iff; iff = _resource->loadIFF(bsum->autotextFilename); if (!iff) { error("Could not load AUTOTEXT IFF"); } if (chunkStream = iff->getChunkStream("CVTX"), chunkStream) { _engineData.setVal("AUTOTEXT", new CVTX(chunkStream)); delete chunkStream; } delete iff; } #undef LOAD_BOOT_L #undef LOAD_BOOT } State::State *NancyEngine::getStateObject(NancyState::NancyState state) const { switch (state) { case NancyState::kLogo: return &State::Logo::instance(); case NancyState::kCredits: return &State::Credits::instance(); case NancyState::kMap: return &State::Map::instance(); case NancyState::kSetup: return &State::SetupMenu::instance(); case NancyState::kHelp: return &State::Help::instance(); case NancyState::kScene: return &State::Scene::instance(); case NancyState::kMainMenu: return &State::MainMenu::instance(); case NancyState::kLoadSave: return &State::LoadSaveMenu::instance(); case NancyState::kSaveDialog: return &State::SaveDialog::instance(); default: return nullptr; } } void NancyEngine::destroyState(NancyState::NancyState state) const { switch (state) { case NancyState::kLogo: if (State::Logo::hasInstance()) { State::Logo::instance().destroy(); } break; case NancyState::kCredits: if (State::Credits::hasInstance()) { State::Credits::instance().destroy(); } break; case NancyState::kMap: if (State::Map::hasInstance()) { State::Map::instance().destroy(); } break; case NancyState::kHelp: if (State::Help::hasInstance()) { State::Help::instance().destroy(); } break; case NancyState::kScene: if (State::Scene::hasInstance()) { State::Scene::instance().destroy(); } break; case NancyState::kMainMenu: if (State::MainMenu::hasInstance()) { State::MainMenu::instance().destroy(); } break; case NancyState::kSetup: if (State::SetupMenu::hasInstance()) { State::SetupMenu::instance().destroy(); } break; case NancyState::kLoadSave: if (State::LoadSaveMenu::hasInstance()) { State::LoadSaveMenu::instance().destroy(); } break; case NancyState::kSaveDialog: if (State::SaveDialog::hasInstance()) { State::SaveDialog::instance().destroy(); } break; default: break; } } void NancyEngine::preloadCals() { auto *pcal = GetEngineData(PCAL); if (!pcal) { // CALs only appeared in nancy2 so a PCAL chunk may not exist return; } for (const Common::String &name : pcal->calNames) { if (!_resource->readCifTree(name, "cal", 2)) { error("Failed to preload CAL '%s'", name.c_str()); } } } void NancyEngine::readDatFile() { Common::SeekableReadStream *datFile = SearchMan.createReadStreamForMember("nancy.dat"); if (!datFile) { error("Unable to find nancy.dat"); } if (datFile->readUint32BE() != MKTAG('N', 'N', 'C', 'Y')) { error("nancy.dat is invalid"); } int8 major = datFile->readSByte(); int8 minor = datFile->readSByte(); if (major != _datFileMajorVersion) { error("Incorrect nancy.dat version. Expected '%d.%d', found %d.%d", _datFileMajorVersion, _datFileMinorVersion, major, minor); } else { if (minor < _datFileMinorVersion) { warning("Incorrect nancy.dat version. Expected at least '%d.%d', found %d.%d. Game may still work, but expect bugs", _datFileMajorVersion, _datFileMinorVersion, major, minor); } } uint16 numGames = datFile->readUint16LE(); uint16 gameType = getGameType(); if (gameType > numGames) { // Fallback for when no data is present for the current game: // throw a warning and use the last available game data warning("Data for game type %d is not in nancy.dat", getGameType()); gameType = numGames; } // Seek to offset containing current game datFile->skip((gameType - 1) * 4); uint32 thisGameOffset = datFile->readUint32LE(); uint32 nextGameOffset = gameType == numGames ? datFile->size() : datFile->readUint32LE(); datFile->seek(thisGameOffset); _staticData.readData(*datFile, _gameDescription->desc.language, nextGameOffset, major, minor); delete datFile; } Common::Error NancyEngine::synchronize(Common::Serializer &ser) { auto *bootSummary = GetEngineData(BSUM); assert(bootSummary); // Sync boot summary header, which includes full game title ser.syncVersion(kSavegameVersion); ser.matchBytes((const char *)bootSummary->header, 90); // Sync scene and action records NancySceneState.synchronize(ser); NancySceneState._actionManager.synchronize(ser); return Common::kNoError; } bool NancyEngine::isCompressed() { return getGameFlags() & GF_COMPRESSED; } } // End of namespace Nancy