/* 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) 1995-1997 Presto Studios, Inc. * * 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 "common/config-manager.h" #include "common/error.h" #include "common/events.h" #include "common/fs.h" #include "common/file.h" #include "common/memstream.h" #include "common/savefile.h" #include "common/textconsole.h" #include "common/translation.h" #include "common/random.h" #include "backends/keymapper/keymapper.h" #include "base/plugins.h" #include "base/version.h" #include "gui/message.h" #include "gui/saveload.h" #include "video/theora_decoder.h" #include "video/qt_decoder.h" #include "pegasus/console.h" #include "pegasus/cursor.h" #include "pegasus/energymonitor.h" #include "pegasus/gamestate.h" #include "pegasus/interface.h" #include "pegasus/menu.h" #include "pegasus/movie.h" #include "pegasus/pegasus.h" #include "pegasus/timers.h" #include "pegasus/ai/ai_area.h" #include "pegasus/items/itemlist.h" #include "pegasus/items/biochips/aichip.h" #include "pegasus/items/biochips/biochipitem.h" #include "pegasus/items/biochips/mapchip.h" #include "pegasus/items/biochips/opticalchip.h" #include "pegasus/items/biochips/pegasuschip.h" #include "pegasus/items/biochips/retscanchip.h" #include "pegasus/items/biochips/shieldchip.h" #include "pegasus/items/inventory/airmask.h" #include "pegasus/items/inventory/gascanister.h" #include "pegasus/items/inventory/inventoryitem.h" #include "pegasus/items/inventory/keycard.h" #include "pegasus/neighborhood/neighborhood.h" #include "pegasus/neighborhood/caldoria/caldoria.h" #include "pegasus/neighborhood/mars/mars.h" #include "pegasus/neighborhood/norad/constants.h" #include "pegasus/neighborhood/norad/alpha/noradalpha.h" #include "pegasus/neighborhood/norad/delta/noraddelta.h" #include "pegasus/neighborhood/prehistoric/prehistoric.h" #include "pegasus/neighborhood/tsa/fulltsa.h" #include "pegasus/neighborhood/tsa/tinytsa.h" #include "pegasus/neighborhood/wsc/wsc.h" namespace Pegasus { PegasusEngine::PegasusEngine(OSystem *syst, const PegasusGameDescription *gamedesc) : Engine(syst), InputHandler(0), _gameDescription(gamedesc), _shellNotification(kJMPDCShellNotificationID, this), _returnHotspot(kInfoReturnSpotID), _itemDragger(this), _bigInfoMovie(kNoDisplayElement), _smallInfoMovie(kNoDisplayElement) { _continuePoint = 0; _saveAllowed = _loadAllowed = true; _saveRequested = _loadRequested = false; _gameMenu = 0; _deathReason = kDeathStranded; _neighborhood = 0; _FXLevel = 0x80; _ambientLevel = 0x80; _gameMode = kNoMode; _switchModesSync = false; _draggingItem = 0; _dragType = kDragNoDrag; _idlerHead = 0; _currentCD = 1; _introTimer = 0; _aiSaveStream = 0; } PegasusEngine::~PegasusEngine() { throwAwayEverything(); delete _resFork; delete _console; delete _cursor; delete _continuePoint; delete _gameMenu; delete _neighborhood; delete _rnd; delete _introTimer; delete _aiSaveStream; for (ItemIterator it = _allItems.begin(); it != _allItems.end(); it++) delete *it; InputDeviceManager::destroy(); GameStateManager::destroy(); // NOTE: This must be deleted last! delete _gfx; } Common::Error PegasusEngine::run() { _console = new PegasusConsole(this); _gfx = new GraphicsManager(this); _resFork = new Common::MacResManager(); _cursor = new Cursor(); _rnd = new Common::RandomSource("Pegasus"); if (!_resFork->open("JMP PP Resources") || !_resFork->hasResFork()) error("Could not load JMP PP Resources"); // Initialize items createItems(); // Initialize cursors _cursor->addCursorFrames(0x80); // Main _cursor->addCursorFrames(900); // Mars Shuttle // Initialize the item dragger bounds _itemDragger.setHighlightBounds(); if (!isDemo() && !detectOpeningClosingDirectory()) { Common::String message = "Missing intro directory. "; // Give Mac OS X a more specific message because we can #ifdef MACOSX message += "Make sure \"Opening/Closing\" is present."; #else message += "Be sure to rename \"Opening/Closing\" to \"Opening_Closing\"."; #endif GUIErrorMessage(message); warning("%s", message.c_str()); return Common::kNoGameDataFoundError; } // Set up input initKeymap(); InputHandler::setInputHandler(this); allowInput(true); // Set up inventories _items.setWeightLimit(9); _items.setOwnerID(kPlayerID); _biochips.setWeightLimit(8); _biochips.setOwnerID(kPlayerID); _returnHotspot.setArea(Common::Rect(kNavAreaLeft, kNavAreaTop, 512 + kNavAreaLeft, 256 + kNavAreaTop)); _returnHotspot.setHotspotFlags(kInfoReturnSpotFlag); _allHotspots.push_back(&_returnHotspot); _screenDimmer.setBounds(Common::Rect(0, 0, 640, 480)); _screenDimmer.setDisplayOrder(kScreenDimmerOrder); // Load from the launcher/cli if requested (and don't show the intro in those cases) bool doIntro = true; if (ConfMan.hasKey("save_slot")) { uint32 gameToLoad = ConfMan.getInt("save_slot"); doIntro = (loadGameState(gameToLoad).getCode() != Common::kNoError); } _shellNotification.notifyMe(this, kJMPShellNotificationFlags, kJMPShellNotificationFlags); if (doIntro) // Start up the first notification _shellNotification.setNotificationFlags(kGameStartingFlag, kGameStartingFlag); if (!isDemo()) { _introTimer = new FuseFunction(); _introTimer->setFunctor(new Common::Functor0Mem(this, &PegasusEngine::introTimerExpired)); } while (!shouldQuit()) { processShell(); _system->delayMillis(10); // Ease off the CPU } return Common::kNoError; } bool PegasusEngine::canLoadGameStateCurrently() { return _loadAllowed && !isDemo(); } bool PegasusEngine::canSaveGameStateCurrently() { return _saveAllowed && !isDemo() && g_neighborhood; } bool PegasusEngine::detectOpeningClosingDirectory() { // We need to detect what our Opening/Closing directory is listed as // On the original disc, it was 'Opening/Closing' but only HFS(+) supports the slash // Mac OS X will display this as 'Opening:Closing' and we can use that directly // On other systems, users will need to rename to "Opening_Closing" Common::FSNode gameDataDir(ConfMan.get("path")); gameDataDir = gameDataDir.getChild("Images"); if (!gameDataDir.exists()) return false; Common::FSList fsList; if (!gameDataDir.getChildren(fsList, Common::FSNode::kListDirectoriesOnly, true)) return false; for (uint i = 0; i < fsList.size() && _introDirectory.empty(); i++) { Common::String name = fsList[i].getName(); if (name.equalsIgnoreCase("Opening:Closing")) _introDirectory = name; else if (name.equalsIgnoreCase("Opening_Closing")) _introDirectory = name; } if (_introDirectory.empty()) return false; debug(0, "Detected intro location as '%s'", _introDirectory.c_str()); _introDirectory = Common::String("Images/") + _introDirectory; return true; } void PegasusEngine::createItems() { Common::SeekableReadStream *res = _resFork->getResource(MKTAG('N', 'I', 't', 'm'), 0x80); if (!res) error("Couldn't find neighborhood items resource"); uint16 entryCount = res->readUint16BE(); for (uint16 i = 0; i < entryCount; i++) { ItemID itemID = res->readUint16BE(); NeighborhoodID neighborhoodID = res->readUint16BE(); RoomID roomID = res->readUint16BE(); DirectionConstant direction = res->readByte(); res->readByte(); // alignment createItem(itemID, neighborhoodID, roomID, direction); } delete res; } void PegasusEngine::createItem(ItemID itemID, NeighborhoodID neighborhoodID, RoomID roomID, DirectionConstant direction) { switch (itemID) { case kInterfaceBiochip: // Unused in game, but still in the data and we need to create // it because it's saved/loaded from save files. new BiochipItem(itemID, neighborhoodID, roomID, direction); break; case kAIBiochip: new AIChip(itemID, neighborhoodID, roomID, direction); break; case kPegasusBiochip: new PegasusChip(itemID, neighborhoodID, roomID, direction); break; case kOpticalBiochip: new OpticalChip(itemID, neighborhoodID, roomID, direction); break; case kMapBiochip: new MapChip(itemID, neighborhoodID, roomID, direction); break; case kRetinalScanBiochip: new RetScanChip(itemID, neighborhoodID, roomID, direction); break; case kShieldBiochip: new ShieldChip(itemID, neighborhoodID, roomID, direction); break; case kAirMask: new AirMask(itemID, neighborhoodID, roomID, direction); break; case kKeyCard: new KeyCard(itemID, neighborhoodID, roomID, direction); break; case kGasCanister: new GasCanister(itemID, neighborhoodID, roomID, direction); break; default: // Everything else is a normal inventory item new InventoryItem(itemID, neighborhoodID, roomID, direction); break; } } void PegasusEngine::runIntro() { stopIntroTimer(); bool skipped = false; Video::VideoDecoder *video = new Video::QuickTimeDecoder(); if (video->loadFile(_introDirectory + "/BandaiLogo.movie")) { video->setVolume(MIN(getAmbienceLevel(), 0xFF)); video->start(); while (!shouldQuit() && !video->endOfVideo() && !skipped) { if (video->needsUpdate()) { const Graphics::Surface *frame = video->decodeNextFrame(); if (frame) { _system->copyRectToScreen((const byte *)frame->getPixels(), frame->pitch, 0, 0, frame->w, frame->h); _system->updateScreen(); } } Input input; InputDevice.getInput(input, kFilterAllInput); if (input.anyInput()) skipped = true; _system->delayMillis(10); } } delete video; if (shouldQuit() || skipped) return; video = new Video::QuickTimeDecoder(); if (!video->loadFile(_introDirectory + "/Big Movie.movie")) error("Could not load intro movie"); video->setVolume(MIN(getAmbienceLevel(), 0xFF)); video->seek(Audio::Timestamp(0, 10 * 600, 600)); video->start(); playMovieScaled(video, 0, 0); delete video; } Common::Error PegasusEngine::showLoadDialog() { GUI::SaveLoadChooser slc(_("Load game:"), _("Load"), false); Common::String gameId = ConfMan.get("gameid"); const Plugin *plugin = nullptr; EngineMan.findGame(gameId, &plugin); int slot = slc.runModalWithPluginAndTarget(plugin, ConfMan.getActiveDomainName()); Common::Error result; if (slot >= 0) { if (loadGameState(slot).getCode() == Common::kNoError) result = Common::kNoError; else result = Common::kUnknownError; } else { result = Common::kUserCanceled; } return result; } Common::Error PegasusEngine::showSaveDialog() { GUI::SaveLoadChooser slc(_("Save game:"), _("Save"), true); Common::String gameId = ConfMan.get("gameid"); const Plugin *plugin = nullptr; EngineMan.findGame(gameId, &plugin); int slot = slc.runModalWithPluginAndTarget(plugin, ConfMan.getActiveDomainName()); if (slot >= 0) return saveGameState(slot, slc.getResultString()); return Common::kUserCanceled; } void PegasusEngine::showSaveFailedDialog(const Common::Error &status) { Common::String failMessage = Common::String::format(_("Failed to save game (%s)! " "Please consult the README for basic information, and for " "instructions on how to obtain further assistance."), status.getDesc().c_str()); GUI::MessageDialog dialog(failMessage); dialog.runModal(); } GUI::Debugger *PegasusEngine::getDebugger() { return _console; } void PegasusEngine::addIdler(Idler *idler) { idler->_nextIdler = _idlerHead; if (_idlerHead) _idlerHead->_prevIdler = idler; idler->_prevIdler = 0; _idlerHead = idler; } void PegasusEngine::removeIdler(Idler *idler) { if (idler->_prevIdler) idler->_prevIdler->_nextIdler = idler->_nextIdler; if (idler->_nextIdler) idler->_nextIdler->_prevIdler = idler->_prevIdler; if (idler == _idlerHead) _idlerHead = idler->_nextIdler; idler->_nextIdler = 0; idler->_prevIdler = 0; } void PegasusEngine::giveIdleTime() { for (Idler *idler = _idlerHead; idler != 0; idler = idler->_nextIdler) idler->useIdleTime(); } void PegasusEngine::addTimeBase(TimeBase *timeBase) { _timeBases.push_back(timeBase); } void PegasusEngine::removeTimeBase(TimeBase *timeBase) { _timeBases.remove(timeBase); } bool PegasusEngine::loadFromStream(Common::SeekableReadStream *stream) { // Dispose currently running stuff lowerInventoryDrawerSync(); lowerBiochipDrawerSync(); useMenu(0); useNeighborhood(0); removeAllItemsFromInventory(); removeAllItemsFromBiochips(); _currentItemID = kNoItemID; _currentBiochipID = kNoItemID; if (!g_interface) createInterface(); // Signature uint32 creator = stream->readUint32BE(); if (creator != kPegasusPrimeCreator) { warning("Bad save creator '%s'", tag2str(creator)); return false; } uint32 gameType = stream->readUint32BE(); int saveType; switch (gameType) { case kPegasusPrimeDisk1GameType: case kPegasusPrimeDisk2GameType: case kPegasusPrimeDisk3GameType: case kPegasusPrimeDisk4GameType: _currentCD = gameType - kPegasusPrimeDisk1GameType + 1; saveType = kNormalSave; break; case kPegasusPrimeContinueType: saveType = kContinueSave; break; default: // There are five other possible game types on the Pippin // version, but hopefully we don't see any of those here warning("Unhandled pegasus game type '%s'", tag2str(gameType)); return false; } uint32 version = stream->readUint32BE(); if (version != kPegasusPrimeVersion) { warning("Where did you get this save? It's a beta (v%04x)!", version & 0x7fff); return false; } // Game State GameState.readGameState(stream); // Energy setLastEnergyValue(stream->readUint32BE()); // Death reason setEnergyDeathReason(stream->readByte()); // Items _allItems.readFromStream(stream); // Inventory byte itemCount = stream->readByte(); if (itemCount > 0) { for (byte i = 0; i < itemCount; i++) { InventoryItem *inv = (InventoryItem *)_allItems.findItemByID((ItemID)stream->readUint16BE()); addItemToInventory(inv); } g_interface->setCurrentInventoryItemID((ItemID)stream->readUint16BE()); } // Biochips byte biochipCount = stream->readByte(); if (biochipCount > 0) { for (byte i = 0; i < biochipCount; i++) { BiochipItem *biochip = (BiochipItem *)_allItems.findItemByID((ItemID)stream->readUint16BE()); addItemToBiochips(biochip); } g_interface->setCurrentBiochipID((ItemID)stream->readUint16BE()); } // TODO: Disc check // Jump to environment jumpToNewEnvironment(GameState.getCurrentNeighborhood(), GameState.getCurrentRoom(), GameState.getCurrentDirection()); _shellNotification.setNotificationFlags(0, kNeedNewJumpFlag); performJump(GameState.getCurrentNeighborhood()); // AI rules if (g_AIArea) { // HACK: clone2727 accidentally changed some Prehistoric code to output some bad saves // at one point. That's fixed now, but I don't want to leave the other users high // and dry. if (GameState.getCurrentNeighborhood() == kPrehistoricID && !isDemo()) { uint32 pos = stream->pos(); stream->seek(0x208); uint32 roomView = stream->readUint32BE(); stream->seek(pos); if (roomView == 0x30019) { // This is a bad save -> Let's fix the data // One byte should be put at the end instead uint32 size = stream->size() - pos; byte *data = (byte *)malloc(size); data[0] = stream->readByte(); data[1] = stream->readByte(); data[2] = stream->readByte(); byte wrongData = stream->readByte(); stream->read(data + 3, size - 4); data[size - 1] = wrongData; Common::MemoryReadStream tempStream(data, size, DisposeAfterUse::YES); g_AIArea->readAIRules(&tempStream); } else { g_AIArea->readAIRules(stream); } } else { g_AIArea->readAIRules(stream); } } startNeighborhood(); // Make a new continue point if this isn't already one if (saveType == kNormalSave) makeContinuePoint(); return true; } bool PegasusEngine::writeToStream(Common::WriteStream *stream, int saveType) { // WORKAROUND: If we don't have the interface, we can't actually save. // However, we should still have a continue point, so we will just dump that // out. This is needed for saving a game while in the space chase. if (!g_interface) { // Saving a continue stream from a continue stream should // never happen. In addition, we do need to have a continue // stream for this to work. if (saveType != kNormalSave || !_continuePoint) return false; writeContinueStream(stream); return true; } if (g_neighborhood) g_neighborhood->flushGameState(); // Signature stream->writeUint32BE(kPegasusPrimeCreator); if (saveType == kNormalSave) stream->writeUint32BE(kPegasusPrimeDisk1GameType + _currentCD - 1); else // Continue stream->writeUint32BE(kPegasusPrimeContinueType); stream->writeUint32BE(kPegasusPrimeVersion); // Game State GameState.writeGameState(stream); // Energy stream->writeUint32BE(getSavedEnergyValue()); // Death reason stream->writeByte(getEnergyDeathReason()); // Items _allItems.writeToStream(stream); // Inventory byte itemCount = _items.getNumItems(); stream->writeByte(itemCount); if (itemCount > 0) { for (uint32 i = 0; i < itemCount; i++) stream->writeUint16BE(_items.getItemIDAt(i)); stream->writeUint16BE(g_interface->getCurrentInventoryItem()->getObjectID()); } // Biochips byte biochipCount = _biochips.getNumItems(); stream->writeByte(biochipCount); if (biochipCount > 0) { for (uint32 i = 0; i < biochipCount; i++) stream->writeUint16BE(_biochips.getItemIDAt(i)); stream->writeUint16BE(g_interface->getCurrentBiochip()->getObjectID()); } // AI rules if (g_AIArea) g_AIArea->writeAIRules(stream); return true; } void PegasusEngine::makeContinuePoint() { // WORKAROUND: Do not attempt to make a continue point if the interface is not set // up. The original did *not* do this and attempting to restore the game using the pause // menu during the canyon/space chase sequence would segfault the game and crash the // system. Nice! if (!g_interface) return; delete _continuePoint; Common::MemoryWriteStreamDynamic newPoint(DisposeAfterUse::NO); writeToStream(&newPoint, kContinueSave); _continuePoint = new Common::MemoryReadStream(newPoint.getData(), newPoint.size(), DisposeAfterUse::YES); } void PegasusEngine::loadFromContinuePoint() { // Failure to load a continue point is fatal if (!_continuePoint) error("Attempting to load from non-existent continue point"); _continuePoint->seek(0); if (!loadFromStream(_continuePoint)) error("Failed loading continue point"); } void PegasusEngine::writeContinueStream(Common::WriteStream *stream) { // We're going to pretty much copy the stream, except for the save type _continuePoint->seek(0); stream->writeUint32BE(_continuePoint->readUint32BE()); _continuePoint->readUint32BE(); // skip the continue type stream->writeUint32BE(kPegasusPrimeDisk1GameType + _currentCD - 1); // Now just copy over the rest uint32 size = _continuePoint->size() - _continuePoint->pos(); byte *data = new byte[size]; _continuePoint->read(data, size); stream->write(data, size); delete[] data; } Common::StringArray PegasusEngine::listSaveFiles() { Common::StringArray fileNames = g_system->getSavefileManager()->listSavefiles("pegasus-*.sav"); Common::sort(fileNames.begin(), fileNames.end()); return fileNames; } Common::Error PegasusEngine::loadGameState(int slot) { Common::StringArray fileNames = listSaveFiles(); Common::InSaveFile *loadFile = _saveFileMan->openForLoading(fileNames[slot]); if (!loadFile) return Common::kUnknownError; bool valid = loadFromStream(loadFile); delete loadFile; return valid ? Common::kNoError : Common::kUnknownError; } static bool isValidSaveFileChar(char c) { // Limit it to letters, digits, and a few other characters that should be safe return Common::isAlnum(c) || c == ' ' || c == '_' || c == '+' || c == '-' || c == '.'; } static bool isValidSaveFileName(const Common::String &desc) { for (uint32 i = 0; i < desc.size(); i++) if (!isValidSaveFileChar(desc[i])) return false; return true; } Common::Error PegasusEngine::saveGameState(int slot, const Common::String &desc) { if (!isValidSaveFileName(desc)) return Common::Error(Common::kCreatingFileFailed, _("Invalid file name for saving")); Common::String output = Common::String::format("pegasus-%s.sav", desc.c_str()); Common::OutSaveFile *saveFile = _saveFileMan->openForSaving(output, false); if (!saveFile) return Common::kUnknownError; bool valid = writeToStream(saveFile, kNormalSave); delete saveFile; return valid ? Common::kNoError : Common::kUnknownError; } void PegasusEngine::receiveNotification(Notification *notification, const NotificationFlags flags) { if (&_shellNotification == notification) { switch (flags) { case kGameStartingFlag: { useMenu(new MainMenu()); if (isDemo()) { // Start playing the music earlier here ((MainMenu *)_gameMenu)->startMainMenuLoop(); // Show the intro splash screen showTempScreen("Images/Demo/NGsplashScrn.pict"); if (shouldQuit()) { useMenu(0); return; } // Fade out and then back in with the main menu _gfx->doFadeOutSync(); _gfx->updateDisplay(); _gfx->doFadeInSync(); } else { // Display the intro runIntro(); resetIntroTimer(); if (shouldQuit()) return; // Now display the main menu _gfx->invalRect(Common::Rect(0, 0, 640, 480)); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); } break; } case kPlayerDiedFlag: doDeath(); break; case kNeedNewJumpFlag: performJump(GameState.getNextNeighborhood()); startNeighborhood(); break; default: break; } } } void PegasusEngine::checkCallBacks() { for (Common::List::iterator it = _timeBases.begin(); it != _timeBases.end(); it++) (*it)->checkCallBacks(); } void PegasusEngine::resetIntroTimer() { if (!isDemo() && _gameMenu && _gameMenu->getObjectID() == kMainMenuID) { _introTimer->stopFuse(); _introTimer->primeFuse(kIntroTimeOut); _introTimer->lightFuse(); } } void PegasusEngine::introTimerExpired() { if (_gameMenu && _gameMenu->getObjectID() == kMainMenuID) { ((MainMenu *)_gameMenu)->stopMainMenuLoop(); bool skipped = false; Video::VideoDecoder *video = new Video::QuickTimeDecoder(); if (!video->loadFile(_introDirectory + "/LilMovie.movie")) error("Failed to load little movie"); video->setVolume(MIN(getAmbienceLevel(), 0xFF)); bool saveAllowed = swapSaveAllowed(false); bool openAllowed = swapLoadAllowed(false); video->start(); skipped = playMovieScaled(video, 0, 0); delete video; if (shouldQuit()) return; if (!skipped) { runIntro(); if (shouldQuit()) return; } resetIntroTimer(); _gfx->invalRect(Common::Rect(0, 0, 640, 480)); swapSaveAllowed(saveAllowed); swapLoadAllowed(openAllowed); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); } } void PegasusEngine::stopIntroTimer() { if (_introTimer) _introTimer->stopFuse(); } void PegasusEngine::delayShell(TimeValue time, TimeScale scale) { if (time == 0 || scale == 0) return; uint32 startTime = g_system->getMillis(); uint32 timeInMillis = time * 1000 / scale; while (g_system->getMillis() < startTime + timeInMillis) { InputDevice.pumpEvents(); checkCallBacks(); _gfx->updateDisplay(); } } void PegasusEngine::useMenu(GameMenu *newMenu) { if (_gameMenu) { _gameMenu->restorePreviousHandler(); delete _gameMenu; } _gameMenu = newMenu; if (_gameMenu) _gameMenu->becomeCurrentHandler(); } bool PegasusEngine::checkGameMenu() { GameMenuCommand command = kMenuCmdNoCommand; if (_gameMenu) { command = _gameMenu->getLastCommand(); if (command != kMenuCmdNoCommand) { _gameMenu->clearLastCommand(); doGameMenuCommand(command); } } return command != kMenuCmdNoCommand; } void PegasusEngine::doGameMenuCommand(const GameMenuCommand command) { Common::Error result; switch (command) { case kMenuCmdStartAdventure: stopIntroTimer(); GameState.setWalkthroughMode(false); startNewGame(); break; case kMenuCmdCredits: if (isDemo()) { showTempScreen("Images/Demo/DemoCredits.pict"); _gfx->doFadeOutSync(); _gfx->updateDisplay(); _gfx->doFadeInSync(); } else { stopIntroTimer(); _gfx->doFadeOutSync(); useMenu(new CreditsMenu()); _gfx->updateDisplay(); _gfx->doFadeInSync(); } break; case kMenuCmdQuit: case kMenuCmdDeathQuitDemo: if (isDemo()) showTempScreen("Images/Demo/NGquitScrn.pict"); _gfx->doFadeOutSync(); quitGame(); break; case kMenuCmdOverview: stopIntroTimer(); doInterfaceOverview(); resetIntroTimer(); break; case kMenuCmdStartWalkthrough: stopIntroTimer(); GameState.setWalkthroughMode(true); startNewGame(); break; case kMenuCmdRestore: stopIntroTimer(); // fall through case kMenuCmdDeathRestore: result = showLoadDialog(); if (command == kMenuCmdRestore && result.getCode() != Common::kNoError) resetIntroTimer(); break; case kMenuCmdCreditsMainMenu: _gfx->doFadeOutSync(); useMenu(new MainMenu()); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); _gfx->doFadeInSync(); resetIntroTimer(); break; case kMenuCmdDeathContinue: if (((DeathMenu *)_gameMenu)->playerWon()) { if (isDemo()) { showTempScreen("Images/Demo/DemoCredits.pict"); _gfx->doFadeOutSync(); _gfx->updateDisplay(); _gfx->doFadeInSync(); } else { _gfx->doFadeOutSync(); useMenu(0); _gfx->enableErase(); _gfx->updateDisplay(); _gfx->disableErase(); Video::VideoDecoder *video = new Video::QuickTimeDecoder(); if (!video->loadFile(_introDirectory + "/Closing.movie")) error("Could not load closing movie"); video->setVolume(MIN(getSoundFXLevel(), 0xFF)); uint16 x = (640 - video->getWidth() * 2) / 2; uint16 y = (480 - video->getHeight() * 2) / 2; video->start(); playMovieScaled(video, x, y); delete video; if (shouldQuit()) return; useMenu(new MainMenu()); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); _gfx->doFadeInSync(); resetIntroTimer(); } } else { loadFromContinuePoint(); } break; case kMenuCmdDeathMainMenuDemo: case kMenuCmdDeathMainMenu: _gfx->doFadeOutSync(); useMenu(new MainMenu()); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); _gfx->doFadeInSync(); resetIntroTimer(); break; case kMenuCmdPauseSave: result = showSaveDialog(); if (result.getCode() != Common::kUserCanceled) { if (result.getCode() != Common::kNoError) showSaveFailedDialog(result); pauseMenu(false); } break; case kMenuCmdPauseContinue: pauseMenu(false); break; case kMenuCmdPauseRestore: makeContinuePoint(); result = showLoadDialog(); if (result.getCode() == Common::kNoError) { // Successfully loaded, unpause the game pauseMenu(false); } else if (result.getCode() != Common::kUserCanceled) { // Try to get us back to a sane state loadFromContinuePoint(); } break; case kMenuCmdPauseQuit: _gfx->doFadeOutSync(); throwAwayEverything(); pauseMenu(false); useMenu(new MainMenu()); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); _gfx->doFadeInSync(); resetIntroTimer(); break; case kMenuCmdNoCommand: break; default: error("Unknown menu command %d", command); } } void PegasusEngine::handleInput(const Input &input, const Hotspot *cursorSpot) { if (!checkGameMenu()) shellGameInput(input, cursorSpot); // Handle the console here if (input.isConsoleRequested()) { _console->attach(); _console->onFrame(); } // Handle save requests here if (_saveRequested && _saveAllowed) { _saveRequested = false; // Can only save during a game and not in the demo if (g_neighborhood && !isDemo()) { pauseEngine(true); Common::Error result = showSaveDialog(); if (result.getCode() != Common::kNoError && result.getCode() != Common::kUserCanceled) showSaveFailedDialog(result); pauseEngine(false); } } // Handle load requests here if (_loadRequested && _loadAllowed) { _loadRequested = false; // WORKAROUND: Do not entertain load requests when the pause menu is up // The original did and the game entered a bad state after loading. // It's theoretically possible to make it so it does work while the // pause menu is up, but the pause state of the engine is just too weird. // Just use the pause menu's restore button since it's there for that // for you to load anyway. if (!isDemo() && !(_gameMenu && _gameMenu->getObjectID() == kPauseMenuID)) { pauseEngine(true); if (g_neighborhood) { makeContinuePoint(); Common::Error result = showLoadDialog(); if (result.getCode() != Common::kNoError && result.getCode() != Common::kUserCanceled) loadFromContinuePoint(); } else { if (_introTimer) _introTimer->stopFuse(); Common::Error result = showLoadDialog(); if (result.getCode() != Common::kNoError) { if (!_gameMenu) { useMenu(new MainMenu()); ((MainMenu *)_gameMenu)->startMainMenuLoop(); } resetIntroTimer(); } } pauseEngine(false); } } } void PegasusEngine::doInterfaceOverview() { static const short kNumOverviewSpots = 11; static const Common::Rect overviewSpots[kNumOverviewSpots] = { Common::Rect(354, 318, 354 + 204, 318 + 12), Common::Rect(211, 34, 211 + 114, 34 + 28), Common::Rect(502, 344, 502 + 138, 344 + 120), Common::Rect(132, 40, 132 + 79, 40 + 22), Common::Rect(325, 40, 332 + 115, 40 + 22), Common::Rect(70, 318, 70 + 284, 318 + 12), Common::Rect(76, 334, 76 + 96, 334 + 96), Common::Rect(64, 64, 64 + 512, 64 + 256), Common::Rect(364, 334, 364 + 96, 334 + 96), Common::Rect(172, 334, 172 + 192, 334 + 96), Common::Rect(542, 36, 542 + 58, 36 + 20) }; _gfx->doFadeOutSync(); useMenu(0); Picture leftBackground(kNoDisplayElement); leftBackground.initFromPICTFile("Images/Interface/OVLeft.mac"); leftBackground.setDisplayOrder(0); leftBackground.moveElementTo(kBackground1Left, kBackground1Top); leftBackground.startDisplaying(); leftBackground.show(); Picture topBackground(kNoDisplayElement); topBackground.initFromPICTFile("Images/Interface/OVTop.mac"); topBackground.setDisplayOrder(0); topBackground.moveElementTo(kBackground2Left, kBackground2Top); topBackground.startDisplaying(); topBackground.show(); Picture rightBackground(kNoDisplayElement); rightBackground.initFromPICTFile("Images/Interface/OVRight.mac"); rightBackground.setDisplayOrder(0); rightBackground.moveElementTo(kBackground3Left, kBackground3Top); rightBackground.startDisplaying(); rightBackground.show(); Picture bottomBackground(kNoDisplayElement); bottomBackground.initFromPICTFile("Images/Interface/OVBottom.mac"); bottomBackground.setDisplayOrder(0); bottomBackground.moveElementTo(kBackground4Left, kBackground4Top); bottomBackground.startDisplaying(); bottomBackground.show(); Picture controllerHighlight(kNoDisplayElement); controllerHighlight.initFromPICTFile("Images/Interface/OVcontrollerHilite.mac"); controllerHighlight.setDisplayOrder(0); controllerHighlight.moveElementTo(kOverviewControllerLeft, kOverviewControllerTop); controllerHighlight.startDisplaying(); Movie overviewText(kNoDisplayElement); overviewText.initFromMovieFile("Images/Interface/Overview Mac.movie"); overviewText.setDisplayOrder(0); overviewText.moveElementTo(kNavAreaLeft, kNavAreaTop); overviewText.startDisplaying(); overviewText.show(); overviewText.redrawMovieWorld(); DropHighlight highlight(kNoDisplayElement); highlight.setDisplayOrder(1); highlight.startDisplaying(); highlight.setHighlightThickness(4); highlight.setHighlightColor(g_system->getScreenFormat().RGBToColor(239, 239, 0)); highlight.setHighlightCornerDiameter(8); Input input; InputDevice.getInput(input, kFilterAllInput); Common::Point cursorLoc; input.getInputLocation(cursorLoc); uint16 i; for (i = 0; i < kNumOverviewSpots; ++i) if (overviewSpots[i].contains(cursorLoc)) break; TimeValue time; if (i == kNumOverviewSpots) time = 5; else if (i > 4) time = i + 1; else time = i; if (time == 2) { highlight.hide(); controllerHighlight.show(); } else if (i != kNumOverviewSpots) { controllerHighlight.hide(); Common::Rect r = overviewSpots[i]; r.grow(5); highlight.setBounds(r); highlight.show(); } else { highlight.hide(); controllerHighlight.hide(); } overviewText.setTime(time * 3 + 2, 15); overviewText.redrawMovieWorld(); _cursor->setCurrentFrameIndex(3); _cursor->show(); _gfx->updateDisplay(); _gfx->doFadeInSync(); for (;;) { InputDevice.getInput(input, kFilterAllInput); if (input.anyInput() || shouldQuit() || _loadRequested || _saveRequested) break; input.getInputLocation(cursorLoc); for (i = 0; i < kNumOverviewSpots; ++i) if (overviewSpots[i].contains(cursorLoc)) break; if (i == kNumOverviewSpots) time = 5; else if (i > 4) time = i + 1; else time = i; if (time == 2) { highlight.hide(); controllerHighlight.show(); } else if (i != kNumOverviewSpots) { controllerHighlight.hide(); Common::Rect r = overviewSpots[i]; r.grow(5); highlight.setBounds(r); highlight.show(); } else { highlight.hide(); controllerHighlight.hide(); } // The original just constantly redraws the frame, but that // doesn't actually need to be done. if ((time * 3 + 2) * 40 != overviewText.getTime()) { overviewText.setTime(time * 3 + 2, 15); overviewText.redrawMovieWorld(); } refreshDisplay(); _system->delayMillis(10); } if (shouldQuit()) return; highlight.hide(); _cursor->hide(); _gfx->doFadeOutSync(); useMenu(new MainMenu()); _gfx->updateDisplay(); ((MainMenu *)_gameMenu)->startMainMenuLoop(); _gfx->doFadeInSync(); _saveRequested = false; _loadRequested = false; } void PegasusEngine::showTempScreen(const Common::String &fileName) { _gfx->doFadeOutSync(); Picture picture(0); picture.initFromPICTFile(fileName); picture.setDisplayOrder(kMaxAvailableOrder); picture.startDisplaying(); picture.show(); _gfx->updateDisplay(); _gfx->doFadeInSync(); // Wait for the next event bool done = false; while (!shouldQuit() && !done) { Common::Event event; while (_eventMan->pollEvent(event)) { switch (event.type) { case Common::EVENT_LBUTTONUP: case Common::EVENT_RBUTTONUP: case Common::EVENT_KEYDOWN: done = true; break; default: break; } } _system->delayMillis(10); } } void PegasusEngine::refreshDisplay() { giveIdleTime(); _gfx->updateDisplay(); } void PegasusEngine::resetEnergyDeathReason() { switch (getCurrentNeighborhoodID()) { case kMarsID: _deathReason = kDeathArrestedInMars; break; case kNoradAlphaID: case kNoradDeltaID: _deathReason = kDeathArrestedInNorad; break; case kWSCID: _deathReason = kDeathArrestedInWSC; break; default: _deathReason = kDeathStranded; break; } } bool PegasusEngine::playerHasItem(const Item *item) { return playerHasItemID(item->getObjectID()); } bool PegasusEngine::playerHasItemID(const ItemID itemID) { return itemInInventory(itemID) || itemInBiochips(itemID); } InventoryItem *PegasusEngine::getCurrentInventoryItem() { if (g_interface) return g_interface->getCurrentInventoryItem(); return 0; } bool PegasusEngine::itemInInventory(InventoryItem *item) { return _items.itemInInventory(item); } bool PegasusEngine::itemInInventory(ItemID id) { return _items.itemInInventory(id); } BiochipItem *PegasusEngine::getCurrentBiochip() { if (g_interface) return g_interface->getCurrentBiochip(); return 0; } bool PegasusEngine::itemInBiochips(BiochipItem *item) { return _biochips.itemInInventory(item); } bool PegasusEngine::itemInBiochips(ItemID id) { return _biochips.itemInInventory(id); } bool PegasusEngine::playerAlive() { return (_shellNotification.getNotificationFlags() & kPlayerDiedFlag) == 0; } Common::String PegasusEngine::getBriefingMovie() { if (_neighborhood) return _neighborhood->getBriefingMovie(); return Common::String(); } Common::String PegasusEngine::getEnvScanMovie() { if (_neighborhood) return _neighborhood->getEnvScanMovie(); return Common::String(); } uint PegasusEngine::getNumHints() { if (_neighborhood) return _neighborhood->getNumHints(); return 0; } Common::String PegasusEngine::getHintMovie(uint hintNum) { if (_neighborhood) return _neighborhood->getHintMovie(hintNum); return Common::String(); } bool PegasusEngine::canSolve() { if (_neighborhood) return _neighborhood->canSolve(); return false; } void PegasusEngine::prepareForAIHint(const Common::String &movieName) { if (g_neighborhood) g_neighborhood->prepareForAIHint(movieName); } void PegasusEngine::cleanUpAfterAIHint(const Common::String &movieName) { if (g_neighborhood) g_neighborhood->cleanUpAfterAIHint(movieName); } void PegasusEngine::jumpToNewEnvironment(const NeighborhoodID neighborhoodID, const RoomID roomID, const DirectionConstant direction) { GameState.setNextLocation(neighborhoodID, roomID, direction); _shellNotification.setNotificationFlags(kNeedNewJumpFlag, kNeedNewJumpFlag); } void PegasusEngine::checkFlashlight() { if (_neighborhood) _neighborhood->checkFlashlight(); } bool PegasusEngine::playMovieScaled(Video::VideoDecoder *video, uint16 x, uint16 y) { bool skipped = false; while (!shouldQuit() && !video->endOfVideo() && !skipped) { if (video->needsUpdate()) { const Graphics::Surface *frame = video->decodeNextFrame(); if (frame) { if (frame->w <= 320 && frame->h <= 240) { drawScaledFrame(frame, x, y); } else { _system->copyRectToScreen((const byte *)frame->getPixels(), frame->pitch, x, y, frame->w, frame->h); _system->updateScreen(); } } } Input input; InputDevice.getInput(input, kFilterAllInput); if (input.anyInput() || _saveRequested || _loadRequested) skipped = true; _system->delayMillis(10); } return skipped; } void PegasusEngine::die(const DeathReason reason) { Input dummy; if (isDragging()) _itemDragger.stopTracking(dummy); _deathReason = reason; _shellNotification.setNotificationFlags(kPlayerDiedFlag, kPlayerDiedFlag); } void PegasusEngine::doDeath() { #ifdef USE_THEORADEC // The updated demo has a new Theora video for the closing if (isDVDDemo() && _deathReason == kPlayerWonGame) { Video::TheoraDecoder decoder; if (decoder.loadFile("Images/Demo TSA/DemoClosing.ogg")) { throwAwayEverything(); decoder.start(); playMovieScaled(&decoder, 0, 0); } } #endif _gfx->doFadeOutSync(); throwAwayEverything(); useMenu(new DeathMenu(_deathReason)); _gfx->updateDisplay(); _gfx->doFadeInSync(); } void PegasusEngine::throwAwayEverything() { if (_items.getNumItems() != 0 && g_interface) _currentItemID = g_interface->getCurrentInventoryItem()->getObjectID(); else _currentItemID = kNoItemID; if (_biochips.getNumItems() != 0 && g_interface) _currentBiochipID = g_interface->getCurrentBiochip()->getObjectID(); else _currentBiochipID = kNoItemID; useMenu(0); useNeighborhood(0); delete g_interface; g_interface = 0; } InputBits PegasusEngine::getInputFilter() { InputBits filter = InputHandler::getInputFilter(); if (isPaused()) return filter & ~JMPPPInput::getItemPanelsInputFilter(); return filter; } void PegasusEngine::processShell() { checkCallBacks(); checkNotifications(); InputHandler::pollForInput(); refreshDisplay(); } void PegasusEngine::createInterface() { if (!g_interface) new Interface(); g_interface->createInterface(); } void PegasusEngine::setGameMode(const GameMode newMode) { if (newMode != _gameMode && canSwitchGameMode(newMode, _gameMode)) { switchGameMode(newMode, _gameMode); _gameMode = newMode; } } void PegasusEngine::switchGameMode(const GameMode newMode, const GameMode oldMode) { // Start raising panels before lowering panels, to give the activating panel time // to set itself up without cutting into the lowering panel's animation time. if (_switchModesSync) { if (newMode == kModeInventoryPick) raiseInventoryDrawerSync(); else if (newMode == kModeBiochipPick) raiseBiochipDrawerSync(); else if (newMode == kModeInfoScreen) showInfoScreen(); if (oldMode == kModeInventoryPick) lowerInventoryDrawerSync(); else if (oldMode == kModeBiochipPick) lowerBiochipDrawerSync(); else if (oldMode == kModeInfoScreen) hideInfoScreen(); } else { if (newMode == kModeInventoryPick) raiseInventoryDrawer(); else if (newMode == kModeBiochipPick) raiseBiochipDrawer(); else if (newMode == kModeInfoScreen) showInfoScreen(); if (oldMode == kModeInventoryPick) lowerInventoryDrawer(); else if (oldMode == kModeBiochipPick) lowerBiochipDrawer(); else if (oldMode == kModeInfoScreen) hideInfoScreen(); } } bool PegasusEngine::canSwitchGameMode(const GameMode newMode, const GameMode oldMode) { // WORKAROUND: Don't allow game mode switches when the interface is not set up. // Prevents segfaults when pressing 'i' when in the space chase. if (!g_interface) return false; if (newMode == kModeInventoryPick && oldMode == kModeBiochipPick) return false; if (newMode == kModeBiochipPick && oldMode == kModeInventoryPick) return false; return true; } bool PegasusEngine::itemInLocation(const ItemID itemID, const NeighborhoodID neighborhood, const RoomID room, const DirectionConstant direction) { NeighborhoodID itemNeighborhood; RoomID itemRoom; DirectionConstant itemDirection; Item *item = _allItems.findItemByID(itemID); item->getItemRoom(itemNeighborhood, itemRoom, itemDirection); return itemNeighborhood == neighborhood && itemRoom == room && itemDirection == direction; } InventoryResult PegasusEngine::addItemToInventory(InventoryItem *item) { InventoryResult result; do { if (g_interface) result = g_interface->addInventoryItem(item); else result = _items.addItem(item); if (result == kTooMuchWeight) destroyInventoryItem(pickItemToDestroy()); } while (result != kInventoryOK); GameState.setTakenItem(item, true); if (g_neighborhood) g_neighborhood->pickedUpItem(item); g_AIArea->checkMiddleArea(); return result; } void PegasusEngine::useNeighborhood(Neighborhood *neighborhood) { delete _neighborhood; _neighborhood = neighborhood; if (_neighborhood) { InputHandler::setInputHandler(_neighborhood); _neighborhood->init(); _neighborhood->moveNavTo(kNavAreaLeft, kNavAreaTop); g_interface->setDate(_neighborhood->getDateResID()); } else { InputHandler::setInputHandler(this); } } void PegasusEngine::performJump(NeighborhoodID neighborhoodID) { if (_neighborhood) useNeighborhood(0); // Sub chase is special if (neighborhoodID == kNoradSubChaseID) { throwAwayEverything(); _loadAllowed = false; doSubChase(); if (shouldQuit()) return; neighborhoodID = kNoradDeltaID; GameState.setNextRoom(kNorad41); GameState.setNextDirection(kEast); _loadAllowed = true; } Neighborhood *neighborhood; makeNeighborhood(neighborhoodID, neighborhood); useNeighborhood(neighborhood); // Update the CD variable (used just for saves currently) _currentCD = getNeighborhoodCD(neighborhoodID); } void PegasusEngine::startNeighborhood() { if (g_interface && _currentItemID != kNoItemID) g_interface->setCurrentInventoryItemID(_currentItemID); if (g_interface && _currentBiochipID != kNoItemID) g_interface->setCurrentBiochipID(_currentBiochipID); setGameMode(kModeNavigation); if (_neighborhood) _neighborhood->start(); } void PegasusEngine::startNewGame() { // WORKAROUND: The original game ignored the menu difficulty // setting. We're going to pass it through here so that // the menu actually makes sense now. bool isWalkthrough = GameState.getWalkthroughMode(); GameState.resetGameState(); GameState.setWalkthroughMode(isWalkthrough); _gfx->doFadeOutSync(); useMenu(0); _gfx->enableErase(); _gfx->updateDisplay(); _gfx->disableErase(); _gfx->enableUpdates(); createInterface(); if (isDemo()) { setLastEnergyValue(kFullEnergy); jumpToNewEnvironment(kPrehistoricID, kPrehistoric02, kSouth); GameState.setPrehistoricSeenTimeStream(false); GameState.setPrehistoricSeenFlyer1(false); GameState.setPrehistoricSeenFlyer2(false); GameState.setPrehistoricSeenBridgeZoom(false); GameState.setPrehistoricBreakerThrown(false); #ifdef USE_THEORADEC if (isDVD()) { // The updated demo has a new Theora video for the closing Video::TheoraDecoder decoder; if (decoder.loadFile("Images/Demo TSA/DemoOpening.ogg")) { decoder.start(); playMovieScaled(&decoder, 0, 0); } } #endif } else { jumpToNewEnvironment(kCaldoriaID, kCaldoria00, kEast); } removeAllItemsFromInventory(); removeAllItemsFromBiochips(); // Properly reset all items to their original state g_allItems.resetAllItems(); BiochipItem *biochip = (BiochipItem *)_allItems.findItemByID(kAIBiochip); addItemToBiochips(biochip); if (isDemo()) { biochip = (BiochipItem *)_allItems.findItemByID(kPegasusBiochip); addItemToBiochips(biochip); biochip = (BiochipItem *)_allItems.findItemByID(kMapBiochip); addItemToBiochips(biochip); InventoryItem *item = (InventoryItem *)_allItems.findItemByID(kKeyCard); addItemToInventory(item); item = (InventoryItem *)_allItems.findItemByID(kJourneymanKey); addItemToInventory(item); _currentItemID = kJourneymanKey; } else { _currentItemID = kNoItemID; } _currentBiochipID = kAIBiochip; // Clear jump notification flags and just perform the jump... _shellNotification.setNotificationFlags(0, kNeedNewJumpFlag); performJump(GameState.getNextNeighborhood()); startNeighborhood(); } void PegasusEngine::makeNeighborhood(NeighborhoodID neighborhoodID, Neighborhood *&neighborhood) { // TODO: CD check switch (neighborhoodID) { case kCaldoriaID: neighborhood = new Caldoria(g_AIArea, this); break; case kMarsID: neighborhood = new Mars(g_AIArea, this); break; case kPrehistoricID: neighborhood = new Prehistoric(g_AIArea, this); break; case kFullTSAID: neighborhood = new FullTSA(g_AIArea, this); break; case kTinyTSAID: neighborhood = new TinyTSA(g_AIArea, this); break; case kWSCID: neighborhood = new WSC(g_AIArea, this); break; case kNoradAlphaID: neighborhood = new NoradAlpha(g_AIArea, this); break; case kNoradDeltaID: createInterface(); neighborhood = new NoradDelta(g_AIArea, this); break; default: error("Unknown neighborhood %d", neighborhoodID); } } bool PegasusEngine::wantsCursor() { return _gameMenu == 0; } void PegasusEngine::updateCursor(const Common::Point, const Hotspot *cursorSpot) { if (_itemDragger.isTracking()) { _cursor->setCurrentFrameIndex(5); } else { if (!cursorSpot) { _cursor->setCurrentFrameIndex(0); } else { uint32 id = cursorSpot->getObjectID(); switch (id) { case kCurrentItemSpotID: if (countInventoryItems() != 0) _cursor->setCurrentFrameIndex(4); else _cursor->setCurrentFrameIndex(0); break; default: HotSpotFlags flags = cursorSpot->getHotspotFlags(); if (flags & kZoomInSpotFlag) _cursor->setCurrentFrameIndex(1); else if (flags & kZoomOutSpotFlag) _cursor->setCurrentFrameIndex(2); else if (flags & (kPickUpItemSpotFlag | kPickUpBiochipSpotFlag)) _cursor->setCurrentFrameIndex(4); else if (flags & kJMPClickingSpotFlags) _cursor->setCurrentFrameIndex(3); else _cursor->setCurrentFrameIndex(0); } } } } void PegasusEngine::toggleInventoryDisplay() { if (_gameMode == kModeInventoryPick) setGameMode(kModeNavigation); else setGameMode(kModeInventoryPick); } void PegasusEngine::toggleBiochipDisplay() { if (_gameMode == kModeBiochipPick) setGameMode(kModeNavigation); else setGameMode(kModeBiochipPick); } void PegasusEngine::showInfoScreen() { if (g_neighborhood) { // Break the input handler chain... _savedHandler = InputHandler::getCurrentHandler(); InputHandler::setInputHandler(this); Picture *pushPicture = ((Neighborhood *)g_neighborhood)->getTurnPushPicture(); _bigInfoMovie.shareSurface(pushPicture); _smallInfoMovie.shareSurface(pushPicture); g_neighborhood->hideNav(); _smallInfoMovie.initFromMovieFile("Images/Items/Info Right Movie"); _smallInfoMovie.setDisplayOrder(kInfoSpinOrder); _smallInfoMovie.moveElementTo(kNavAreaLeft + 304, kNavAreaTop + 8); _smallInfoMovie.moveMovieBoxTo(304, 8); _smallInfoMovie.startDisplaying(); _smallInfoMovie.show(); TimeValue startTime, stopTime; g_AIArea->getSmallInfoSegment(startTime, stopTime); _smallInfoMovie.setSegment(startTime, stopTime); _smallInfoMovie.setTime(startTime); _smallInfoMovie.setFlags(kLoopTimeBase); _bigInfoMovie.initFromMovieFile("Images/Items/Info Left Movie"); _bigInfoMovie.setDisplayOrder(kInfoBackgroundOrder); _bigInfoMovie.moveElementTo(kNavAreaLeft, kNavAreaTop); _bigInfoMovie.startDisplaying(); _bigInfoMovie.show(); _bigInfoMovie.setTime(g_AIArea->getBigInfoTime()); _bigInfoMovie.redrawMovieWorld(); _smallInfoMovie.redrawMovieWorld(); _smallInfoMovie.start(); } } void PegasusEngine::hideInfoScreen() { if (g_neighborhood) { InputHandler::setInputHandler(_savedHandler); _bigInfoMovie.hide(); _bigInfoMovie.stopDisplaying(); _bigInfoMovie.releaseMovie(); _smallInfoMovie.hide(); _smallInfoMovie.stopDisplaying(); _smallInfoMovie.stop(); _smallInfoMovie.releaseMovie(); g_neighborhood->showNav(); } } void PegasusEngine::raiseInventoryDrawer() { if (g_interface) g_interface->raiseInventoryDrawer(); } void PegasusEngine::raiseBiochipDrawer() { if (g_interface) g_interface->raiseBiochipDrawer(); } void PegasusEngine::lowerInventoryDrawer() { if (g_interface) g_interface->lowerInventoryDrawer(); } void PegasusEngine::lowerBiochipDrawer() { if (g_interface) g_interface->lowerBiochipDrawer(); } void PegasusEngine::raiseInventoryDrawerSync() { if (g_interface) g_interface->raiseInventoryDrawerSync(); } void PegasusEngine::raiseBiochipDrawerSync() { if (g_interface) g_interface->raiseBiochipDrawerSync(); } void PegasusEngine::lowerInventoryDrawerSync() { if (g_interface) g_interface->lowerInventoryDrawerSync(); } void PegasusEngine::lowerBiochipDrawerSync() { if (g_interface) g_interface->lowerBiochipDrawerSync(); } void PegasusEngine::toggleInfo() { if (_gameMode == kModeInfoScreen) setGameMode(kModeNavigation); else if (_gameMode == kModeNavigation) setGameMode(kModeInfoScreen); } void PegasusEngine::dragTerminated(const Input &) { Hotspot *finalSpot = _itemDragger.getLastHotspot(); InventoryResult result; if (_dragType == kDragInventoryPickup) { if (finalSpot && finalSpot->getObjectID() == kInventoryDropSpotID) result = addItemToInventory((InventoryItem *)_draggingItem); else result = kTooMuchWeight; if (result != kInventoryOK) autoDragItemIntoRoom(_draggingItem, _draggingSprite); else delete _draggingSprite; } else if (_dragType == kDragBiochipPickup) { if (finalSpot && finalSpot->getObjectID() == kBiochipDropSpotID) result = addItemToBiochips((BiochipItem *)_draggingItem); else result = kTooMuchWeight; if (result != kInventoryOK) autoDragItemIntoRoom(_draggingItem, _draggingSprite); else delete _draggingSprite; } else if (_dragType == kDragInventoryUse) { if (finalSpot && (finalSpot->getHotspotFlags() & kDropItemSpotFlag) != 0) { // *** Need to decide on a case by case basis what to do here. // the crowbar should break the cover off the Mars reactor if its frozen, the // global transport card should slide through the slot, the oxygen mask should // attach to the filling station, and so on... _neighborhood->dropItemIntoRoom(_draggingItem, finalSpot); delete _draggingSprite; } else { autoDragItemIntoInventory(_draggingItem, _draggingSprite); } } _dragType = kDragNoDrag; if (g_AIArea) g_AIArea->unlockAI(); } void PegasusEngine::dragItem(const Input &input, Item *item, DragType type) { _draggingItem = item; _dragType = type; // Create the sprite. _draggingSprite = _draggingItem->getDragSprite(kDraggingSpriteID); Common::Point where; input.getInputLocation(where); Common::Rect r1; _draggingSprite->getBounds(r1); r1 = Common::Rect::center(where.x, where.y, r1.width(), r1.height()); _draggingSprite->setBounds(r1); // Set up drag constraints. DisplayElement *navMovie = _gfx->findDisplayElement(kNavMovieID); Common::Rect r2; navMovie->getBounds(r2); r2.left -= r1.width() / 3; r2.right += r1.width() / 3; r2.top -= r1.height() / 3; r2.bottom += r2.height() / 3; r1 = Common::Rect(-30000, -30000, 30000, 30000); _itemDragger.setDragConstraints(r2, r1); // Start dragging. _draggingSprite->setDisplayOrder(kDragSpriteOrder); _draggingSprite->startDisplaying(); _draggingSprite->show(); _itemDragger.setDragSprite(_draggingSprite); _itemDragger.setNextHandler(_neighborhood); _itemDragger.startTracking(input); if (g_AIArea) g_AIArea->lockAIOut(); } void PegasusEngine::shellGameInput(const Input &input, const Hotspot *cursorSpot) { if (_gameMode == kModeInfoScreen) { if (JMPPPInput::isToggleAIMiddleInput(input)) { LowerClientSignature middleOwner = g_AIArea->getMiddleAreaOwner(); g_AIArea->toggleMiddleAreaOwner(); if (middleOwner != g_AIArea->getMiddleAreaOwner()) { _bigInfoMovie.setTime(g_AIArea->getBigInfoTime()); _smallInfoMovie.stop(); _smallInfoMovie.setFlags(0); TimeValue startTime, stopTime; g_AIArea->getSmallInfoSegment(startTime, stopTime); _smallInfoMovie.setSegment(startTime, stopTime); _smallInfoMovie.setTime(startTime); _smallInfoMovie.setFlags(kLoopTimeBase); _bigInfoMovie.redrawMovieWorld(); _smallInfoMovie.redrawMovieWorld(); _smallInfoMovie.start(); } } } else { if (JMPPPInput::isRaiseInventoryInput(input)) toggleInventoryDisplay(); if (JMPPPInput::isRaiseBiochipsInput(input)) toggleBiochipDisplay(); if (JMPPPInput::isTogglePauseInput(input) && _neighborhood) pauseMenu(!isPaused()); } if (JMPPPInput::isToggleInfoInput(input)) toggleInfo(); } void PegasusEngine::activateHotspots() { if (_gameMode == kModeInfoScreen) { _allHotspots.activateOneHotspot(kInfoReturnSpotID); } else { // Set up hot spots. if (_dragType == kDragInventoryPickup) _allHotspots.activateOneHotspot(kInventoryDropSpotID); else if (_dragType == kDragBiochipPickup) _allHotspots.activateOneHotspot(kBiochipDropSpotID); else if (_dragType == kDragNoDrag) _allHotspots.activateMaskedHotspots(kShellSpotFlag); } } bool PegasusEngine::isClickInput(const Input &input, const Hotspot *cursorSpot) { if (_cursor->isVisible() && cursorSpot) return JMPPPInput::isClickInput(input); else return false; } InputBits PegasusEngine::getClickFilter() { return JMPPPInput::getClickInputFilter(); } void PegasusEngine::clickInHotspot(const Input &input, const Hotspot *clickedSpot) { if (clickedSpot->getObjectID() == kCurrentItemSpotID) { InventoryItem *currentItem = getCurrentInventoryItem(); if (currentItem) { removeItemFromInventory(currentItem); dragItem(input, currentItem, kDragInventoryUse); } } else if (clickedSpot->getObjectID() == kInfoReturnSpotID) { toggleInfo(); } } InventoryResult PegasusEngine::removeItemFromInventory(InventoryItem *item) { InventoryResult result; if (g_interface) result = g_interface->removeInventoryItem(item); else result = _items.removeItem(item); // This should never happen assert(result == kInventoryOK); return result; } void PegasusEngine::removeAllItemsFromInventory() { if (g_interface) g_interface->removeAllItemsFromInventory(); else _items.removeAllItems(); } ///////////////////////////////////////////// // // Biochip handling. // Adding biochips to the biochip drawer is a little funny, because of two things: // -- We get the map biochip and pegasus biochip at the same time by dragging // one sprite in TSA // -- We can drag in more than one copy of the various biochips. // Because of this we need to make sure that no more than one copy of each biochip // is ever added. InventoryResult PegasusEngine::addItemToBiochips(BiochipItem *biochip) { InventoryResult result; if (g_interface) result = g_interface->addBiochip(biochip); else result = _biochips.addItem(biochip); // This can never happen assert(result == kInventoryOK); GameState.setTakenItem(biochip, true); if (g_neighborhood) g_neighborhood->pickedUpItem(biochip); g_AIArea->checkMiddleArea(); return result; } void PegasusEngine::removeAllItemsFromBiochips() { if (g_interface) g_interface->removeAllItemsFromBiochips(); else _biochips.removeAllItems(); } void PegasusEngine::setSoundFXLevel(uint16 fxLevel) { _FXLevel = fxLevel; if (_neighborhood) _neighborhood->setSoundFXLevel(fxLevel); if (g_AIArea) g_AIArea->setAIVolume(fxLevel); } void PegasusEngine::setAmbienceLevel(uint16 ambientLevel) { _ambientLevel = ambientLevel; if (_neighborhood) _neighborhood->setAmbienceLevel(ambientLevel); } void PegasusEngine::pauseMenu(bool menuUp) { if (menuUp) { pauseEngine(true); _screenDimmer.startDisplaying(); _screenDimmer.show(); _gfx->updateDisplay(); useMenu(new PauseMenu()); } else { pauseEngine(false); _screenDimmer.hide(); _screenDimmer.stopDisplaying(); useMenu(0); g_AIArea->checkMiddleArea(); } } void PegasusEngine::autoDragItemIntoRoom(Item *item, Sprite *draggingSprite) { if (g_AIArea) g_AIArea->lockAIOut(); Common::Point start, stop; draggingSprite->getLocation(start.x, start.y); Hotspot *dropSpot = _neighborhood->getItemScreenSpot(item, draggingSprite); if (dropSpot) { dropSpot->getCenter(stop.x, stop.y); } else { stop.x = kNavAreaLeft + 256; stop.y = kNavAreaTop + 128; } Common::Rect bounds; draggingSprite->getBounds(bounds); stop.x -= bounds.width() >> 1; stop.y -= bounds.height() >> 1; int dx = ABS(stop.x - start.x); int dy = ABS(stop.y - start.y); TimeValue time = MAX(dx, dy); allowInput(false); _autoDragger.autoDrag(draggingSprite, start, stop, time, kDefaultTimeScale); while (_autoDragger.isDragging()) { InputDevice.pumpEvents(); checkCallBacks(); refreshDisplay(); _system->delayMillis(10); } _neighborhood->dropItemIntoRoom(_draggingItem, dropSpot); allowInput(true); delete _draggingSprite; if (g_AIArea) g_AIArea->unlockAI(); } void PegasusEngine::autoDragItemIntoInventory(Item *, Sprite *draggingSprite) { if (g_AIArea) g_AIArea->lockAIOut(); Common::Point start; draggingSprite->getLocation(start.x, start.y); Common::Rect r; draggingSprite->getBounds(r); Common::Point stop((76 + 172 - r.width()) / 2, 334 - (2 * r.height() / 3)); int dx = ABS(stop.x - start.x); int dy = ABS(stop.y - start.y); TimeValue time = MAX(dx, dy); allowInput(false); _autoDragger.autoDrag(draggingSprite, start, stop, time, kDefaultTimeScale); while (_autoDragger.isDragging()) { InputDevice.pumpEvents(); checkCallBacks(); refreshDisplay(); _system->delayMillis(10); } addItemToInventory((InventoryItem *)_draggingItem); allowInput(true); delete _draggingSprite; if (g_AIArea) g_AIArea->unlockAI(); } NeighborhoodID PegasusEngine::getCurrentNeighborhoodID() const { if (_neighborhood) return _neighborhood->getObjectID(); return kNoNeighborhoodID; } void PegasusEngine::pauseEngineIntern(bool pause) { Engine::pauseEngineIntern(pause); if (pause) { for (Common::List::iterator it = _timeBases.begin(); it != _timeBases.end(); it++) (*it)->pause(); } else { for (Common::List::iterator it = _timeBases.begin(); it != _timeBases.end(); it++) (*it)->resume(); } } uint PegasusEngine::getRandomBit() { return _rnd->getRandomBit(); } uint PegasusEngine::getRandomNumber(uint max) { return _rnd->getRandomNumber(max); } void PegasusEngine::shuffleArray(int32 *arr, int32 count) { if (count > 1) { for (int32 i = 1; i < count; ++i) { int32 j = _rnd->getRandomNumber(i); if (j != i) SWAP(arr[i], arr[j]); } } } void PegasusEngine::playEndMessage() { if (g_interface) { allowInput(false); g_interface->playEndMessage(); allowInput(true); } die(kPlayerWonGame); } void PegasusEngine::doSubChase() { Video::VideoDecoder *video = new Video::QuickTimeDecoder(); if (!video->loadFile("Images/Norad Alpha/Sub Chase Movie")) error("Failed to load sub chase"); video->setEndTime(Audio::Timestamp(0, 133200, 600)); video->start(); while (!shouldQuit() && !video->endOfVideo()) { if (video->needsUpdate()) { const Graphics::Surface *frame = video->decodeNextFrame(); if (frame) drawScaledFrame(frame, 0, 0); } InputDevice.pumpEvents(); _system->delayMillis(10); } delete video; } template static void scaleFrame(const PixelInt *src, PixelInt *dst, int w, int h, int srcPitch) { assert((srcPitch % sizeof(PixelInt)) == 0); // sanity check; allows some simpler code PixelInt *dst1 = dst; PixelInt *dst2 = dst + w * 2; int srcInc = (srcPitch / sizeof(PixelInt)) - w; int dstInc = w * 2; while (h--) { for (int x = 0; x < w; x++) { PixelInt pixel = *src++; *dst1++ = pixel; *dst1++ = pixel; *dst2++ = pixel; *dst2++ = pixel; } src += srcInc; dst1 += dstInc; dst2 += dstInc; } } void PegasusEngine::drawScaledFrame(const Graphics::Surface *frame, uint16 x, uint16 y) { // Scale up the frame doing some simple scaling Graphics::Surface scaledFrame; scaledFrame.create(frame->w * 2, frame->h * 2, frame->format); if (frame->format.bytesPerPixel == 2) scaleFrame((const uint16 *)frame->getPixels(), (uint16 *)scaledFrame.getPixels(), frame->w, frame->h, frame->pitch); else scaleFrame((const uint32 *)frame->getPixels(), (uint32 *)scaledFrame.getPixels(), frame->w, frame->h, frame->pitch); _system->copyRectToScreen((byte *)scaledFrame.getPixels(), scaledFrame.pitch, x, y, scaledFrame.w, scaledFrame.h); _system->updateScreen(); scaledFrame.free(); } void PegasusEngine::destroyInventoryItem(const ItemID itemID) { InventoryItem *item = (InventoryItem *)_allItems.findItemByID(itemID); ItemExtraEntry entry; switch (itemID) { case kAirMask: item->findItemExtra(kRemoveAirMask, entry); item->setItemRoom(kMarsID, kMars49, kSouth); break; case kArgonCanister: item->findItemExtra(kRemoveArgon, entry); item->setItemRoom(kWSCID, kWSC02Morph, kSouth); break; case kCrowbar: item->findItemExtra(kRemoveCrowbar, entry); item->setItemRoom(kMarsID, kMars34, kSouth); break; case kJourneymanKey: item->findItemExtra(kRemoveJourneymanKey, entry); item->setItemRoom(kFullTSAID, kTSA22Red, kEast); break; case kMarsCard: item->findItemExtra(kRemoveMarsCard, entry); item->setItemRoom(kMarsID, kMars31South, kSouth); break; case kNitrogenCanister: item->findItemExtra(kRemoveNitrogen, entry); item->setItemRoom(kWSCID, kWSC02Messages, kSouth); break; case kOrangeJuiceGlassEmpty: item->findItemExtra(kRemoveGlass, entry); item->setItemRoom(kCaldoriaID, kCaldoriaReplicator, kNorth); break; case kPoisonDart: item->findItemExtra(kRemoveDart, entry); item->setItemRoom(kWSCID, kWSC01, kWest); break; case kSinclairKey: item->findItemExtra(kRemoveSinclairKey, entry); item->setItemRoom(kWSCID, kWSC02Morph, kSouth); break; default: return; } g_interface->setCurrentInventoryItemID(itemID); g_AIArea->playAIAreaSequence(kInventorySignature, kMiddleAreaSignature, entry.extraStart, entry.extraStop); removeItemFromInventory(item); } ItemID PegasusEngine::pickItemToDestroy() { /* Must pick an item to destroy Part I: Polite -- try to find an item that's been used Part II: Desperate -- return the first available item. */ // Polite: if (playerHasItemID(kOrangeJuiceGlassEmpty)) return kOrangeJuiceGlassEmpty; if (playerHasItemID(kPoisonDart)) { if (GameState.getCurrentNeighborhood() != kWSCID || GameState.getWSCAnalyzedDart()) return kPoisonDart; } if (playerHasItemID(kJourneymanKey)) { if (GameState.getTSAState() >= kTSAPlayerGotHistoricalLog && GameState.getTSAState() != kPlayerOnWayToPrehistoric && GameState.getTSAState() != kPlayerWentToPrehistoric) return kJourneymanKey; } if (playerHasItemID(kMarsCard)) { if (GameState.getCurrentNeighborhood() != kMarsID || GameState.getMarsArrivedBelow()) return kMarsCard; } // Don't want to deal with deleting the sinclair key and argon canister, since it's // impossible to pick them up one at a time. if (playerHasItemID(kNitrogenCanister)) { if (GameState.getScoringGotCardBomb() && GameState.getCurrentNeighborhood() != kMarsID) return kNitrogenCanister; } if (playerHasItemID(kCrowbar)) { if (GameState.getCurrentNeighborhood() == kWSCID) { if (GameState.getCurrentRoom() >= kWSC62) return kCrowbar; } else if (GameState.getCurrentNeighborhood() == kMarsID) { if (GameState.getScoringGotCardBomb()) return kCrowbar; } else return kCrowbar; } if (playerHasItemID(kAirMask)) { if (GameState.getCurrentNeighborhood() == kMarsID) { if (g_neighborhood->getAirQuality(GameState.getCurrentRoom()) == kAirQualityGood) return kAirMask; } else if (GameState.getCurrentNeighborhood() != kNoradAlphaID && GameState.getCurrentNeighborhood() != kNoradDeltaID) { return kAirMask; } } // Desperate: if (playerHasItemID(kPoisonDart)) return kPoisonDart; if (playerHasItemID(kJourneymanKey)) return kJourneymanKey; if (playerHasItemID(kMarsCard)) return kMarsCard; if (playerHasItemID(kNitrogenCanister)) return kNitrogenCanister; if (playerHasItemID(kCrowbar)) return kCrowbar; if (playerHasItemID(kAirMask)) return kAirMask; // Should never get this far... error("Could not find item to delete"); return kNoItemID; } uint PegasusEngine::getNeighborhoodCD(const NeighborhoodID neighborhood) const { switch (neighborhood) { case kCaldoriaID: case kNoradAlphaID: case kNoradSubChaseID: return 1; case kFullTSAID: case kPrehistoricID: return 2; case kMarsID: return 3; case kWSCID: case kNoradDeltaID: return 4; case kTinyTSAID: // Tiny TSA exists on three of the CD's, so just continue // with the CD we're on return _currentCD; } // Can't really happen, but it's a good fallback anyway :P return 1; } void PegasusEngine::initKeymap() { #ifdef ENABLE_KEYMAPPER static const char *const kKeymapName = "pegasus"; Common::Keymapper *const mapper = _eventMan->getKeymapper(); // Do not try to recreate same keymap over again if (mapper->getKeymap(kKeymapName) != 0) return; Common::Keymap *const engineKeyMap = new Common::Keymap(kKeymapName); // Since the game has multiple built-in keys for each of these anyway, // this just attempts to remap one of them. const Common::KeyActionEntry keyActionEntries[] = { { Common::KEYCODE_UP, "UP", _("Up/Zoom In/Move Forward/Open Doors") }, { Common::KEYCODE_DOWN, "DWN", _("Down/Zoom Out") }, { Common::KEYCODE_LEFT, "TL", _("Turn Left") }, { Common::KEYCODE_RIGHT, "TR", _("Turn Right") }, { Common::KEYCODE_BACKQUOTE, "TIV", _("Display/Hide Inventory Tray") }, { Common::KEYCODE_BACKSPACE, "TBI", _("Display/Hide Biochip Tray") }, { Common::KEYCODE_RETURN, "ENT", _("Action/Select") }, { Common::KEYCODE_t, "TMA", _("Toggle Center Data Display") }, { Common::KEYCODE_i, "TIN", _("Display/Hide Info Screen") }, { Common::KEYCODE_ESCAPE, "PM", _("Display/Hide Pause Menu") }, { Common::KEYCODE_e, "WTF", "???" } // easter egg key (without being completely upfront about it) }; for (uint i = 0; i < ARRAYSIZE(keyActionEntries); i++) { Common::Action *const act = new Common::Action(engineKeyMap, keyActionEntries[i].id, keyActionEntries[i].description); act->addKeyEvent(keyActionEntries[i].ks); } mapper->addGameKeymap(engineKeyMap); mapper->pushKeymap(kKeymapName, true); #endif } } // End of namespace Pegasus