/* 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. * * Additional copyright for this file: * Copyright (C) 1994-1998 Revolution Software Ltd. * * 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "base/plugins.h" #include "common/config-manager.h" #include "common/file.h" #include "common/fs.h" #include "common/events.h" #include "common/savefile.h" #include "common/system.h" #include "common/textconsole.h" #include "engines/metaengine.h" #include "engines/util.h" #include "sword2/sword2.h" #include "sword2/defs.h" #include "sword2/header.h" #include "sword2/console.h" #include "sword2/controls.h" #include "sword2/logic.h" #include "sword2/maketext.h" #include "sword2/memory.h" #include "sword2/mouse.h" #include "sword2/resman.h" #include "sword2/router.h" #include "sword2/screen.h" #include "sword2/sound.h" #include "sword2/saveload.h" namespace Sword2 { Common::Platform Sword2Engine::_platform; struct GameSettings { const char *gameid; const char *description; uint32 features; const char *detectname; }; static const GameSettings sword2_settings[] = { /* Broken Sword II */ {"sword2", "Broken Sword II: The Smoking Mirror", 0, "players.clu" }, {"sword2alt", "Broken Sword II: The Smoking Mirror (alt)", 0, "r2ctlns.ocx" }, {"sword2psx", "Broken Sword II: The Smoking Mirror (PlayStation)", 0, "screens.clu"}, {"sword2psxdemo", "Broken Sword II: The Smoking Mirror (PlayStation/Demo)", Sword2::GF_DEMO, "screens.clu"}, {"sword2demo", "Broken Sword II: The Smoking Mirror (Demo)", Sword2::GF_DEMO, "players.clu" }, {NULL, NULL, 0, NULL} }; } // End of namespace Sword2 class Sword2MetaEngine : public MetaEngine { public: virtual const char *getName() const { return "Sword2"; } virtual const char *getOriginalCopyright() const { return "Broken Sword Games (C) Revolution"; } virtual bool hasFeature(MetaEngineFeature f) const; virtual GameList getSupportedGames() const; virtual GameDescriptor findGame(const char *gameid) const; virtual GameList detectGames(const Common::FSList &fslist) const; virtual SaveStateList listSaves(const char *target) const; virtual int getMaximumSaveSlot() const; virtual void removeSaveState(const char *target, int slot) const; virtual Common::Error createInstance(OSystem *syst, Engine **engine) const; }; bool Sword2MetaEngine::hasFeature(MetaEngineFeature f) const { return (f == kSupportsListSaves) || (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave); } bool Sword2::Sword2Engine::hasFeature(EngineFeature f) const { return (f == kSupportsRTL) || (f == kSupportsSubtitleOptions) || (f == kSupportsSavingDuringRuntime) || (f == kSupportsLoadingDuringRuntime); } GameList Sword2MetaEngine::getSupportedGames() const { const Sword2::GameSettings *g = Sword2::sword2_settings; GameList games; while (g->gameid) { games.push_back(GameDescriptor(g->gameid, g->description)); g++; } return games; } GameDescriptor Sword2MetaEngine::findGame(const char *gameid) const { const Sword2::GameSettings *g = Sword2::sword2_settings; while (g->gameid) { if (0 == scumm_stricmp(gameid, g->gameid)) break; g++; } return GameDescriptor(g->gameid, g->description); } GameList Sword2MetaEngine::detectGames(const Common::FSList &fslist) const { GameList detectedGames; const Sword2::GameSettings *g; Common::FSList::const_iterator file; // TODO: It would be nice if we had code here which distinguishes // between the 'sword2' and 'sword2demo' targets. The current code // can't do that since they use the same detectname. for (g = Sword2::sword2_settings; g->gameid; ++g) { // Iterate over all files in the given directory for (file = fslist.begin(); file != fslist.end(); ++file) { if (!file->isDirectory()) { const char *fileName = file->getName().c_str(); if (0 == scumm_stricmp(g->detectname, fileName)) { // Match found, add to list of candidates, then abort inner loop. detectedGames.push_back(GameDescriptor(g->gameid, g->description, Common::UNK_LANG, Common::kPlatformUnknown, GUIO2(GUIO_NOMIDI, GUIO_NOASPECT))); break; } } } } if (detectedGames.empty()) { // Nothing found -- try to recurse into the 'clusters' subdirectory, // present e.g. if the user copied the data straight from CD. for (file = fslist.begin(); file != fslist.end(); ++file) { if (file->isDirectory()) { const char *fileName = file->getName().c_str(); if (0 == scumm_stricmp("clusters", fileName)) { Common::FSList recList; if (file->getChildren(recList, Common::FSNode::kListAll)) { GameList recGames(detectGames(recList)); if (!recGames.empty()) { detectedGames.push_back(recGames); break; } } } } } } return detectedGames; } SaveStateList Sword2MetaEngine::listSaves(const char *target) const { Common::SaveFileManager *saveFileMan = g_system->getSavefileManager(); Common::StringArray filenames; char saveDesc[SAVE_DESCRIPTION_LEN]; Common::String pattern = target; pattern += ".???"; filenames = saveFileMan->listSavefiles(pattern); sort(filenames.begin(), filenames.end()); // Sort (hopefully ensuring we are sorted numerically..) SaveStateList saveList; for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) { // Obtain the last 3 digits of the filename, since they correspond to the save slot int slotNum = atoi(file->c_str() + file->size() - 3); if (slotNum >= 0 && slotNum <= 999) { Common::InSaveFile *in = saveFileMan->openForLoading(*file); if (in) { in->readUint32LE(); in->read(saveDesc, SAVE_DESCRIPTION_LEN); saveList.push_back(SaveStateDescriptor(slotNum, saveDesc)); delete in; } } } return saveList; } int Sword2MetaEngine::getMaximumSaveSlot() const { return 999; } void Sword2MetaEngine::removeSaveState(const char *target, int slot) const { Common::String filename = target; filename += Common::String::format(".%03d", slot); g_system->getSavefileManager()->removeSavefile(filename); } Common::Error Sword2MetaEngine::createInstance(OSystem *syst, Engine **engine) const { assert(syst); assert(engine); Common::FSList fslist; Common::FSNode dir(ConfMan.get("path")); if (!dir.getChildren(fslist, Common::FSNode::kListAll)) { return Common::kNoGameDataFoundError; } // Invoke the detector Common::String gameid = ConfMan.get("gameid"); GameList detectedGames = detectGames(fslist); for (uint i = 0; i < detectedGames.size(); i++) { if (detectedGames[i].gameid() == gameid) { *engine = new Sword2::Sword2Engine(syst); return Common::kNoError; } } return Common::kNoGameDataFoundError; } #if PLUGIN_ENABLED_DYNAMIC(SWORD2) REGISTER_PLUGIN_DYNAMIC(SWORD2, PLUGIN_TYPE_ENGINE, Sword2MetaEngine); #else REGISTER_PLUGIN_STATIC(SWORD2, PLUGIN_TYPE_ENGINE, Sword2MetaEngine); #endif namespace Sword2 { Sword2Engine::Sword2Engine(OSystem *syst) : Engine(syst), _rnd("sword2") { // Add default file directories const Common::FSNode gameDataDir(ConfMan.get("path")); SearchMan.addSubDirectoryMatching(gameDataDir, "clusters"); SearchMan.addSubDirectoryMatching(gameDataDir, "sword2"); SearchMan.addSubDirectoryMatching(gameDataDir, "video"); SearchMan.addSubDirectoryMatching(gameDataDir, "smacks"); SearchMan.addSubDirectoryMatching(gameDataDir, "streams"); // PSX video if (!scumm_stricmp(ConfMan.get("gameid").c_str(), "sword2demo") || !scumm_stricmp(ConfMan.get("gameid").c_str(), "sword2psxdemo")) _features = GF_DEMO; else _features = 0; // Check if we are running PC or PSX version. if (!scumm_stricmp(ConfMan.get("gameid").c_str(), "sword2psx") || !scumm_stricmp(ConfMan.get("gameid").c_str(), "sword2psxdemo")) Sword2Engine::_platform = Common::kPlatformPSX; else Sword2Engine::_platform = Common::kPlatformPC; _bootParam = ConfMan.getInt("boot_param"); _saveSlot = ConfMan.getInt("save_slot"); _memory = NULL; _resman = NULL; _sound = NULL; _screen = NULL; _mouse = NULL; _logic = NULL; _fontRenderer = NULL; _debugger = NULL; _keyboardEvent.pending = false; _mouseEvent.pending = false; _wantSfxDebug = false; _gameCycle = 0; _gameSpeed = 1; _gmmLoadSlot = -1; // Used to manage GMM Loading } Sword2Engine::~Sword2Engine() { delete _debugger; delete _sound; delete _fontRenderer; delete _screen; delete _mouse; delete _logic; delete _resman; delete _memory; } GUI::Debugger *Sword2Engine::getDebugger() { return _debugger; } void Sword2Engine::registerDefaultSettings() { ConfMan.registerDefault("gfx_details", 2); ConfMan.registerDefault("reverse_stereo", false); } void Sword2Engine::syncSoundSettings() { Engine::syncSoundSettings(); bool mute = ConfMan.getBool("mute"); setSubtitles(ConfMan.getBool("subtitles")); // Our own settings dialog can mute the music, speech and sound effects // individually. ScummVM's settings dialog has one master mute setting. if (ConfMan.hasKey("mute")) { ConfMan.setBool("music_mute", ConfMan.getBool("mute")); ConfMan.setBool("speech_mute", ConfMan.getBool("mute")); ConfMan.setBool("sfx_mute", ConfMan.getBool("mute")); if (!mute) // it is false // So remove it in order to let individual volumes work ConfMan.removeKey("mute", ConfMan.getActiveDomainName()); } _sound->muteMusic(ConfMan.getBool("music_mute")); _sound->muteSpeech(ConfMan.getBool("speech_mute")); _sound->muteFx(ConfMan.getBool("sfx_mute")); _sound->setReverseStereo(ConfMan.getBool("reverse_stereo")); } void Sword2Engine::readSettings() { syncSoundSettings(); _mouse->setObjectLabels(ConfMan.getBool("object_labels")); _screen->setRenderLevel(ConfMan.getInt("gfx_details")); } void Sword2Engine::writeSettings() { ConfMan.setInt("music_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType)); ConfMan.setInt("speech_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType)); ConfMan.setInt("sfx_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType)); ConfMan.setBool("music_mute", _sound->isMusicMute()); ConfMan.setBool("speech_mute", _sound->isSpeechMute()); ConfMan.setBool("sfx_mute", _sound->isFxMute()); ConfMan.setInt("gfx_details", _screen->getRenderLevel()); ConfMan.setBool("subtitles", getSubtitles()); ConfMan.setBool("object_labels", _mouse->getObjectLabels()); ConfMan.setInt("reverse_stereo", _sound->isReverseStereo()); // If even one sound type is unmuted, we can't say that all sound is // muted. if (!_sound->isMusicMute() || !_sound->isSpeechMute() || !_sound->isFxMute()) { ConfMan.setBool("mute", false); } ConfMan.flushToDisk(); } int Sword2Engine::getFramesPerSecond() { return _gameSpeed * FRAMES_PER_SECOND; } /** * The global script variables and player object should be kept open throughout * the game, so that they are never expelled by the resource manager. */ void Sword2Engine::setupPersistentResources() { _logic->_scriptVars = _resman->openResource(1) + ResHeader::size(); _resman->openResource(CUR_PLAYER_ID); } Common::Error Sword2Engine::run() { // Get some falling RAM and put it in your pocket, never let it slip // away _debugger = NULL; _sound = NULL; _fontRenderer = NULL; _screen = NULL; _mouse = NULL; _logic = NULL; _resman = NULL; _memory = NULL; initGraphics(640, 480, true); _screen = new Screen(this, 640, 480); // Create the debugger as early as possible (but not before the // screen object!) so that errors can be displayed in it. In // particular, we want errors about missing files to be clearly // visible to the user. _debugger = new Debugger(this); _memory = new MemoryManager(this); _resman = new ResourceManager(this); if (!_resman->init()) return Common::kUnknownError; _logic = new Logic(this); _fontRenderer = new FontRenderer(this); _sound = new Sound(this); _mouse = new Mouse(this); registerDefaultSettings(); readSettings(); initStartMenu(); // During normal gameplay, we care neither about mouse button releases // nor the scroll wheel. setInputEventFilter(RD_LEFTBUTTONUP | RD_RIGHTBUTTONUP | RD_WHEELUP | RD_WHEELDOWN); setupPersistentResources(); initializeFontResourceFlags(); if (_features & GF_DEMO) _logic->writeVar(DEMO, 1); else _logic->writeVar(DEMO, 0); if (_saveSlot != -1) { if (saveExists(_saveSlot)) restoreGame(_saveSlot); else { RestoreDialog dialog(this); if (!dialog.runModal()) startGame(); } } else if (!_bootParam && saveExists() && !isPsx()) { // Initial load/restart panel disabled in PSX int32 pars[2] = { 221, FX_LOOP }; // version because of missing panel resources bool result; _mouse->setMouse(NORMAL_MOUSE_ID); _logic->fnPlayMusic(pars); StartDialog dialog(this); result = (dialog.runModal() != 0); // If the game is started from the beginning, the cutscene // player will kill the music for us. Otherwise, the restore // will either have killed the music, or done a crossfade. if (shouldQuit()) return Common::kNoError; if (result) startGame(); } else startGame(); _screen->initializeRenderCycle(); while (1) { _debugger->onFrame(); // Handle GMM Loading if (_gmmLoadSlot != -1) { // Hide mouse cursor and fade screen _mouse->hideMouse(); _screen->fadeDown(); // Clean up and load game _logic->_router->freeAllRouteMem(); // TODO: manage error handling restoreGame(_gmmLoadSlot); // Reset load slot _gmmLoadSlot = -1; // Show mouse _mouse->addHuman(); } KeyboardEvent *ke = keyboardEvent(); if (ke) { if ((ke->kbd.hasFlags(Common::KBD_CTRL) && ke->kbd.keycode == Common::KEYCODE_d) || ke->kbd.ascii == '#' || ke->kbd.ascii == '~') { _debugger->attach(); } else if (ke->kbd.hasFlags(0) || ke->kbd.hasFlags(Common::KBD_SHIFT)) { switch (ke->kbd.keycode) { case Common::KEYCODE_p: if (isPaused()) { _screen->dimPalette(false); pauseEngine(false); } else { pauseEngine(true); _screen->dimPalette(true); } break; #if 0 // Disabled because of strange rumors about the // credits running spontaneously every few // minutes. case Common::KEYCODE_c: if (!_logic->readVar(DEMO) && !_mouse->isChoosing()) { ScreenInfo *screenInfo = _screen->getScreenInfo(); _logic->fnPlayCredits(NULL); screenInfo->new_palette = 99; } break; #endif default: break; } } } // skip GameCycle if we're paused if (!isPaused()) { _gameCycle++; gameCycle(); } // We can't use this as termination condition for the loop, // because we want the break to happen before updating the // screen again. if (shouldQuit()) break; // creates the debug text blocks _debugger->buildDebugText(); _screen->buildDisplay(); } return Common::kNoError; } void Sword2Engine::restartGame() { ScreenInfo *screenInfo = _screen->getScreenInfo(); uint32 temp_demo_flag; _mouse->closeMenuImmediately(); // Restart the game. To do this, we must... // Stop music instantly! _sound->stopMusic(true); // In case we were dead - well we're not anymore! _logic->writeVar(DEAD, 0); // Restart the game. Clear all memory and reset the globals temp_demo_flag = _logic->readVar(DEMO); // Remove all resources from memory, including player object and // global variables _resman->removeAll(); // Reopen global variables resource and player object setupPersistentResources(); _logic->writeVar(DEMO, temp_demo_flag); // Free all the route memory blocks from previous game _logic->_router->freeAllRouteMem(); // Call the same function that first started us up startGame(); // Prime system with a game cycle // Reset the graphic 'BuildUnit' list before a new logic list // (see fnRegisterFrame) _screen->resetRenderLists(); // Reset the mouse hot-spot list (see fnRegisterMouse and // fnRegisterFrame) _mouse->resetMouseList(); _mouse->closeMenuImmediately(); // FOR THE DEMO - FORCE THE SCROLLING TO BE RESET! // - this is taken from fnInitBackground // switch on scrolling (2 means first time on screen) screenInfo->scroll_flag = 2; if (_logic->processSession()) error("restart 1st cycle failed??"); // So palette not restored immediately after control panel - we want // to fade up instead! screenInfo->new_palette = 99; } bool Sword2Engine::checkForMouseEvents() { return _mouseEvent.pending; } MouseEvent *Sword2Engine::mouseEvent() { if (!_mouseEvent.pending) return NULL; _mouseEvent.pending = false; return &_mouseEvent; } KeyboardEvent *Sword2Engine::keyboardEvent() { if (!_keyboardEvent.pending) return NULL; _keyboardEvent.pending = false; return &_keyboardEvent; } uint32 Sword2Engine::setInputEventFilter(uint32 filter) { uint32 oldFilter = _inputEventFilter; _inputEventFilter = filter; return oldFilter; } /** * OSystem Event Handler. Full of cross platform goodness and 99% fat free! */ void Sword2Engine::parseInputEvents() { Common::Event event; while (_eventMan->pollEvent(event)) { switch (event.type) { case Common::EVENT_KEYDOWN: if (event.kbd.hasFlags(Common::KBD_CTRL)) { if (event.kbd.keycode == Common::KEYCODE_f) { if (_gameSpeed == 1) _gameSpeed = 2; else _gameSpeed = 1; } } if (!(_inputEventFilter & RD_KEYDOWN)) { _keyboardEvent.pending = true; _keyboardEvent.kbd = event.kbd; } break; case Common::EVENT_LBUTTONDOWN: if (!(_inputEventFilter & RD_LEFTBUTTONDOWN)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_LEFTBUTTONDOWN; } break; case Common::EVENT_RBUTTONDOWN: if (!(_inputEventFilter & RD_RIGHTBUTTONDOWN)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_RIGHTBUTTONDOWN; } break; case Common::EVENT_LBUTTONUP: if (!(_inputEventFilter & RD_LEFTBUTTONUP)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_LEFTBUTTONUP; } break; case Common::EVENT_RBUTTONUP: if (!(_inputEventFilter & RD_RIGHTBUTTONUP)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_RIGHTBUTTONUP; } break; case Common::EVENT_WHEELUP: if (!(_inputEventFilter & RD_WHEELUP)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_WHEELUP; } break; case Common::EVENT_WHEELDOWN: if (!(_inputEventFilter & RD_WHEELDOWN)) { _mouseEvent.pending = true; _mouseEvent.buttons = RD_WHEELDOWN; } break; default: break; } } } void Sword2Engine::gameCycle() { // Do one game cycle, that is run the logic session until a full loop // has been performed. if (_logic->getRunList()) { do { // Reset the 'BuildUnit' and mouse hot-spot lists // before each new logic list. The service scripts // will fill thrm through fnRegisterFrame() and // fnRegisterMouse(). _screen->resetRenderLists(); _mouse->resetMouseList(); // Keep going as long as new lists keep getting put in // - i.e. screen changes. } while (_logic->processSession()); } else { // Start the console and print the start options perhaps? _debugger->attach("AWAITING START COMMAND: (Enter 's 1' then 'q' to start from beginning)"); } // If this screen is wide, recompute the scroll offsets every cycle ScreenInfo *screenInfo = _screen->getScreenInfo(); if (screenInfo->scroll_flag) _screen->setScrolling(); _mouse->mouseEngine(); _sound->processFxQueue(); } void Sword2Engine::startGame() { // Boot the game straight into a start script. It's always George's // script #1, but with different ScreenManager objects depending on // if it's the demo or the full game, or if we're using a boot param. int screen_manager_id = 0; debug(5, "startGame() STARTING:"); if (!_bootParam) { if (_logic->readVar(DEMO)) screen_manager_id = 19; // DOCKS SECTION START else screen_manager_id = 949; // INTRO & PARIS START } else { // FIXME this could be validated against startup.inf for valid // numbers to stop people shooting themselves in the foot if (_bootParam != 0) screen_manager_id = _bootParam; } _logic->runResObjScript(screen_manager_id, CUR_PLAYER_ID, 1); } // FIXME: Move this to some better place? void Sword2Engine::sleepUntil(uint32 time) { while (getMillis() < time) { // Make sure menu animations and fades don't suffer, but don't // redraw the entire scene. _mouse->processMenu(); _screen->updateDisplay(false); _system->delayMillis(10); } } void Sword2Engine::pauseEngineIntern(bool pause) { Engine::pauseEngineIntern(pause); if (pause) { _screen->pauseScreen(true); } else { _screen->pauseScreen(false); } } uint32 Sword2Engine::getMillis() { return _system->getMillis(); } Common::Error Sword2Engine::saveGameState(int slot, const Common::String &desc) { uint32 saveVal = saveGame(slot, (const byte *)desc.c_str()); if (saveVal == SR_OK) return Common::kNoError; else if (saveVal == SR_ERR_WRITEFAIL || saveVal == SR_ERR_FILEOPEN) return Common::kWritingFailed; else return Common::kUnknownError; } bool Sword2Engine::canSaveGameStateCurrently() { bool canSave = true; // No save if dead if (_logic->readVar(DEAD)) canSave = false; // No save if mouse not shown else if (_mouse->getMouseStatus()) canSave = false; // No save if inside a menu else if (_mouse->getMouseMode() == MOUSE_system_menu) canSave = false; // No save if fading else if (_screen->getFadeStatus()) canSave = false; return canSave; } Common::Error Sword2Engine::loadGameState(int slot) { // Prepare the game to load through GMM _gmmLoadSlot = slot; // TODO: error handling. return Common::kNoError; } bool Sword2Engine::canLoadGameStateCurrently() { bool canLoad = true; // No load if mouse is disabled if (_mouse->getMouseStatus()) canLoad = false; // No load if mouse is in system menu else if (_mouse->getMouseMode() == MOUSE_system_menu) canLoad = false; // No load if we are fading else if (_screen->getFadeStatus()) canLoad = false; // But if we are dead, ignore previous conditions if (_logic->readVar(DEAD)) canLoad = true; return canLoad; } } // End of namespace Sword2