/* ResidualVM - A 3D game interpreter * * ResidualVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the AUTHORS * 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 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/debug-channels.h" #include "common/events.h" #include "common/error.h" #include "common/config-manager.h" #include "common/file.h" #include "common/util.h" #include "common/textconsole.h" #include "common/translation.h" #include "gui/debugger.h" #include "gui/error.h" #include "engines/engine.h" #include "engines/myst3/archive.h" #include "engines/myst3/console.h" #include "engines/myst3/database.h" #include "engines/myst3/effects.h" #include "engines/myst3/myst3.h" #include "engines/myst3/nodecube.h" #include "engines/myst3/nodeframe.h" #include "engines/myst3/scene.h" #include "engines/myst3/state.h" #include "engines/myst3/cursor.h" #include "engines/myst3/inventory.h" #include "engines/myst3/script.h" #include "engines/myst3/menu.h" #include "engines/myst3/movie.h" #include "engines/myst3/sound.h" #include "engines/myst3/ambient.h" #include "engines/myst3/transition.h" #include "image/jpeg.h" #include "graphics/conversion.h" #include "graphics/renderer.h" #include "graphics/yuv_to_rgb.h" #include "graphics/framelimiter.h" #include "math/vector2d.h" namespace Myst3 { Myst3Engine::Myst3Engine(OSystem *syst, const Myst3GameDescription *version) : Engine(syst), _system(syst), _gameDescription(version), _db(nullptr), _scriptEngine(nullptr), _state(nullptr), _node(nullptr), _scene(nullptr), _archiveNode(nullptr), _cursor(nullptr), _inventory(nullptr), _gfx(nullptr), _menu(nullptr), _rnd(nullptr), _sound(nullptr), _ambient(nullptr), _inputSpacePressed(false), _inputEnterPressed(false), _inputEscapePressed(false), _inputTildePressed(false), _inputEscapePressedNotConsumed(false), _interactive(false), _menuAction(0), _projectorBackground(nullptr), _shakeEffect(nullptr), _rotationEffect(nullptr), _backgroundSoundScriptLastRoomId(0), _backgroundSoundScriptLastAgeId(0), _transition(nullptr), _frameLimiter(nullptr), _inventoryManualHide(false) { // Add subdirectories to the search path to allow running from a full HDD install const Common::FSNode gameDataDir(ConfMan.get("path")); SearchMan.addSubDirectoryMatching(gameDataDir, "bin"); SearchMan.addSubDirectoryMatching(gameDataDir, "M3Data"); SearchMan.addSubDirectoryMatching(gameDataDir, "M3Data/TEXT"); SearchMan.addSubDirectoriesMatching(gameDataDir, "EXILE Disc ?/Data", true); // Win DVD version directories SearchMan.addSubDirectoryMatching(gameDataDir, "English"); SearchMan.addSubDirectoryMatching(gameDataDir, "Data"); // Mac DVD version directories SearchMan.addSubDirectoryMatching(gameDataDir, "Exile DVD"); SearchMan.addSubDirectoryMatching(gameDataDir, "Exile DVD/data"); // PS2 version directories SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA"); SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA/WORLD"); SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA/WORLD/SOUND"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/DISCS"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/DISCS/DATA"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT/NTSC"); SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT/PAL"); } Myst3Engine::~Myst3Engine() { closeArchives(); delete _menu; delete _inventory; delete _cursor; delete _scene; delete _archiveNode; delete _db; delete _scriptEngine; delete _state; delete _rnd; delete _sound; delete _ambient; delete _frameLimiter; delete _gfx; } bool Myst3Engine::hasFeature(EngineFeature f) const { // The TinyGL renderer does not support arbitrary resolutions for now Common::String rendererConfig = ConfMan.get("renderer"); Graphics::RendererType desiredRendererType = Graphics::parseRendererTypeCode(rendererConfig); Graphics::RendererType matchingRendererType = Graphics::getBestMatchingAvailableRendererType(desiredRendererType); bool softRenderer = matchingRendererType == Graphics::kRendererTypeTinyGL; return (f == kSupportsReturnToLauncher) || (f == kSupportsLoadingDuringRuntime) || (f == kSupportsSavingDuringRuntime) || (f == kSupportsArbitraryResolutions && !softRenderer); } Common::Error Myst3Engine::run() { if (!checkDatafiles()) { // An error message has already been displayed return Common::kUserCanceled; } _gfx = createRenderer(_system); _gfx->init(); _gfx->clear(); _frameLimiter = new Graphics::FrameLimiter(_system, ConfMan.getInt("engine_speed")); _sound = new Sound(this); _ambient = new Ambient(this); _rnd = new Common::RandomSource("sprint"); setDebugger(new Console(this)); _scriptEngine = new Script(this); _db = new Database(getPlatform(), getGameLanguage(), getGameLocalizationType()); _state = new GameState(getPlatform(), _db); _scene = new Scene(this); if (getPlatform() == Common::kPlatformXbox) { _menu = new AlbumMenu(this); } else { _menu = new PagingMenu(this); } _archiveNode = new Archive(); _system->showMouse(false); settingsInitDefaults(); syncSoundSettings(); openArchives(); _cursor = new Cursor(this); _inventory = new Inventory(this); // Init the font Graphics::Surface *font = loadTexture(1206); _gfx->initFont(font); font->free(); delete font; if (ConfMan.hasKey("save_slot")) { // Load game from specified slot, if any Common::Error loadError = loadGameState(ConfMan.getInt("save_slot")); if (loadError.getCode() != Common::kNoError) { return loadError; } } else { if (getPlatform() == Common::kPlatformXbox) { // Play the logo videos loadNode(kNodeLogoPlay, kLogo, 11); } // Game init script, loads the menu loadNode(kNodeSharedInit, kRoomShared, 1); } while (!shouldQuit()) { runNodeBackgroundScripts(); processInput(true); updateCursor(); if (_menuAction) { _menu->updateMainMenu(_menuAction); _menuAction = 0; } drawFrame(); } unloadNode(); _archiveNode->close(); _gfx->freeFont(); // Make sure the mouse is unlocked _system->lockMouse(false); return Common::kNoError; } bool Myst3Engine::addArchive(const Common::String &file, bool mandatory) { Archive *archive = new Archive(); bool opened = archive->open(file.c_str(), nullptr); if (opened) { _archivesCommon.push_back(archive); } else { delete archive; if (mandatory) error("Unable to open archive %s", file.c_str()); } return opened; } void Myst3Engine::openArchives() { // The language of the menus is always the same as the executable // The English CD version can only display English text // The non English CD versions can display their localized language and English // The DVD version can display 6 different languages Common::String menuLanguage; Common::String textLanguage; switch (getGameLanguage()) { case Common::NL_NLD: menuLanguage = "DUTCH"; break; case Common::FR_FRA: menuLanguage = "FRENCH"; break; case Common::DE_DEU: menuLanguage = "GERMAN"; break; case Common::HE_ISR: menuLanguage = "HEBREW"; break; case Common::IT_ITA: menuLanguage = "ITALIAN"; break; case Common::ES_ESP: menuLanguage = "SPANISH"; break; case Common::JA_JPN: menuLanguage = "JAPANESE"; break; case Common::PL_POL: menuLanguage = "POLISH"; break; case Common::EN_ANY: case Common::RU_RUS: default: menuLanguage = "ENGLISH"; break; } if (getGameLocalizationType() == kLocMulti6) { switch (ConfMan.getInt("text_language")) { case kDutch: textLanguage = "DUTCH"; break; case kFrench: textLanguage = "FRENCH"; break; case kGerman: textLanguage = "GERMAN"; break; case kItalian: textLanguage = "ITALIAN"; break; case kSpanish: textLanguage = "SPANISH"; break; case kEnglish: default: textLanguage = "ENGLISH"; break; } } else if (getGameLanguage() == Common::HE_ISR) { textLanguage = "ENGLISH"; // The Hebrew version does not have a "HEBREW.m3t" file } else { if (getGameLocalizationType() == kLocMonolingual || ConfMan.getInt("text_language")) { textLanguage = menuLanguage; } else { textLanguage = "ENGLISH"; } } if (getGameLocalizationType() != kLocMonolingual && getPlatform() != Common::kPlatformXbox && textLanguage == "ENGLISH") { textLanguage = "ENGLISHjp"; } if (getPlatform() == Common::kPlatformXbox) { menuLanguage += "X"; textLanguage += "X"; } // Load all the override files in the search path Common::ArchiveMemberList overrides; SearchMan.listMatchingMembers(overrides, "*.m3o"); for (Common::ArchiveMemberList::const_iterator it = overrides.begin(); it != overrides.end(); it++) { addArchive(it->get()->getName(), false); } addArchive(textLanguage + ".m3t", true); if (getGameLocalizationType() != kLocMonolingual || getPlatform() == Common::kPlatformXbox || getGameLanguage() == Common::HE_ISR) { addArchive(menuLanguage + ".m3u", true); } addArchive("RSRC.m3r", true); } bool Myst3Engine::isTextLanguageEnglish() const { if (getGameLocalizationType() == kLocMonolingual && getGameLanguage() == Common::EN_ANY) { return true; } return getGameLocalizationType() != kLocMonolingual && ConfMan.getInt("text_language") == kEnglish; } void Myst3Engine::closeArchives() { for (uint i = 0; i < _archivesCommon.size(); i++) delete _archivesCommon[i]; _archivesCommon.clear(); } bool Myst3Engine::checkDatafiles() { if (!SearchMan.hasFile("OVER101.m3o")) { warning("Unable to open the update game archive 'OVER101.m3o'"); Common::U32String updateMessage = _("This version of Myst III has not been updated with the latest official patch.\n" "Please install the official update corresponding to your game's language.\n" "The updates can be downloaded from:\n" "https://www.scummvm.org/frs/extras/patches/"); warning("%s", updateMessage.encode().c_str()); GUI::displayErrorDialog(updateMessage); return false; } return true; } HotSpot *Myst3Engine::getHoveredHotspot(NodePtr nodeData, uint16 var) { _state->setHotspotHovered(false); _state->setHotspotActiveRect(0); if (_state->getViewType() == kCube) { float pitch, heading; _cursor->getDirection(pitch, heading); for (uint j = 0; j < nodeData->hotspots.size(); j++) { int32 hitRect = nodeData->hotspots[j].isPointInRectsCube(pitch, heading); if (hitRect >= 0 && nodeData->hotspots[j].isEnabled(_state, var)) { if (nodeData->hotspots[j].rects.size() > 1) { _state->setHotspotHovered(true); _state->setHotspotActiveRect(hitRect); } return &nodeData->hotspots[j]; } } } else { // get the mouse position in original game window coordinates Common::Point mouse = _cursor->getPosition(false); mouse = _scene->scalePoint(mouse); for (uint j = 0; j < nodeData->hotspots.size(); j++) { int32 hitRect = nodeData->hotspots[j].isPointInRectsFrame(_state, mouse); if (hitRect >= 0 && nodeData->hotspots[j].isEnabled(_state, var)) { if (nodeData->hotspots[j].rects.size() > 1) { _state->setHotspotHovered(true); _state->setHotspotActiveRect(hitRect); } return &nodeData->hotspots[j]; } } } return nullptr; } void Myst3Engine::updateCursor() { if (!_inventory->isMouseInside()) { _inventoryManualHide = false; } if (isInventoryVisible() && _inventory->isMouseInside()) { _inventory->updateCursor(); return; } NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); _state->setHotspotIgnoreClick(true); HotSpot *hovered = getHoveredHotspot(nodeData); _state->setHotspotIgnoreClick(false); if (hovered) { _cursor->changeCursor(hovered->cursor); } else { _cursor->changeCursor(8); } } void Myst3Engine::processInput(bool interactive) { _interactive = interactive; if (_state->hasVarGamePadUpPressed()) { // Reset the gamepad directions once they had a chance to be read by the scripts // This combined with keyboard repeat ensures the menu does not scroll too fast _state->setGamePadUpPressed(false); _state->setGamePadDownPressed(false); _state->setGamePadLeftPressed(false); _state->setGamePadRightPressed(false); } bool shouldInteractWithHoveredElement = false; // Process events Common::Event event; while (getEventManager()->pollEvent(event)) { if (_state->hasVarGamePadUpPressed()) { processEventForGamepad(event); } processEventForKeyboardState(event); if (event.type == Common::EVENT_MOUSEMOVE) { if (_state->getViewType() == kCube && _cursor->isPositionLocked()) { _scene->updateCamera(event.relMouse); } _cursor->updatePosition(event.mouse); } else if (event.type == Common::EVENT_LBUTTONDOWN) { shouldInteractWithHoveredElement = true; } else if (event.type == Common::EVENT_RBUTTONDOWN) { // Skip the event when in non-interactive mode if (!interactive) continue; // Nothing to do if not in cube view if (_state->getViewType() != kCube) continue; // Don't unlock if the cursor is transparent if (!_state->getCursorTransparency()) continue; bool cursorLocked = _cursor->isPositionLocked(); _cursor->lockPosition(!cursorLocked); } else if (event.type == Common::EVENT_KEYDOWN) { // Save file name input if (_menu->handleInput(event.kbd)) { continue; } if (event.kbdRepeat) { // Ignore keyboard repeat except when entering save names continue; } switch (event.kbd.keycode) { case Common::KEYCODE_ESCAPE: _inputEscapePressedNotConsumed = true; break; case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: shouldInteractWithHoveredElement = true; break; case Common::KEYCODE_F5: // Open main menu if (_cursor->isVisible() && interactive) { if (_state->getLocationRoom() != kRoomMenu) _menu->goToNode(kNodeMenuMain); } break; case Common::KEYCODE_i: if (event.kbd.flags & Common::KBD_CTRL) { bool mouseInverted = ConfMan.getBool("mouse_inverted"); mouseInverted = !mouseInverted; ConfMan.setBool("mouse_inverted", mouseInverted); } break; default: break; } } else if (event.type == Common::EVENT_SCREEN_CHANGED) { _gfx->computeScreenViewport(); _cursor->updatePosition(_eventMan->getMousePos()); _inventory->reflow(); } } // The input state variables need to be set before calling the scripts updateInputState(); if (shouldInteractWithHoveredElement && interactive) { interactWithHoveredElement(); } // Open main menu // This is not checked directly in the event handling code // because menu open requests done while in lookOnly mode // need to be honored after leaving the inner script loop, // especially when the script loop was cancelled due to pressing // escape. if (_inputEscapePressedNotConsumed && interactive) { _inputEscapePressedNotConsumed = false; if (_cursor->isVisible() && _state->hasVarMenuEscapePressed()) { if (_state->getLocationRoom() != kRoomMenu) _menu->goToNode(kNodeMenuMain); else _state->setMenuEscapePressed(1); } } } void Myst3Engine::processEventForKeyboardState(const Common::Event &event) { if (event.type == Common::EVENT_KEYDOWN) { if (event.kbdRepeat) { // Ignore keyboard repeat except when entering save names return; } switch (event.kbd.keycode) { case Common::KEYCODE_ESCAPE: _inputEscapePressed = true; break; case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: if (!event.kbd.hasFlags(Common::KBD_ALT)) { _inputEnterPressed = true; } break; case Common::KEYCODE_SPACE: _inputSpacePressed = true; break; case Common::KEYCODE_BACKQUOTE: // tilde, used to trigger the easter eggs _inputTildePressed = true; break; default: break; } } else if (event.type == Common::EVENT_KEYUP) { switch (event.kbd.keycode) { case Common::KEYCODE_ESCAPE: _inputEscapePressed = false; _inputEscapePressedNotConsumed = false; break; case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: _inputEnterPressed = false; break; case Common::KEYCODE_SPACE: _inputSpacePressed = false; break; case Common::KEYCODE_BACKQUOTE: _inputTildePressed = false; break; default: break; } } } void Myst3Engine::processEventForGamepad(const Common::Event &event) { if (event.type == Common::EVENT_LBUTTONDOWN) { _state->setGamePadActionPressed(true); } else if (event.type == Common::EVENT_LBUTTONUP) { _state->setGamePadActionPressed(false); } else if (event.type == Common::EVENT_KEYDOWN) { if (event.kbdRepeat) return; switch (event.kbd.keycode) { case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: _state->setGamePadActionPressed(true); break; case Common::KEYCODE_UP: _state->setGamePadUpPressed(true); break; case Common::KEYCODE_DOWN: _state->setGamePadDownPressed(true); break; case Common::KEYCODE_LEFT: _state->setGamePadLeftPressed(true); break; case Common::KEYCODE_RIGHT: _state->setGamePadRightPressed(true); break; case Common::KEYCODE_ESCAPE: _state->setGamePadCancelPressed(true); break; default: break; } } else if (event.type == Common::EVENT_KEYUP) { switch (event.kbd.keycode) { case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: _state->setGamePadActionPressed(false); break; case Common::KEYCODE_ESCAPE: _state->setGamePadCancelPressed(false); break; default: break; } } } void Myst3Engine::updateInputState() { _state->setInputMousePressed(inputValidatePressed()); _state->setInputTildePressed(_inputTildePressed); _state->setInputSpacePressed(_inputSpacePressed); _state->setInputEscapePressed(_inputEscapePressed); } void Myst3Engine::interactWithHoveredElement() { if (isInventoryVisible() && _inventory->isMouseInside()) { uint16 hoveredInventory = _inventory->hoveredItem(); if (hoveredInventory > 0) { _inventory->useItem(hoveredInventory); } else { if (isWideScreenModEnabled()) { _inventoryManualHide = true; } } return; } NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); HotSpot *hovered = getHoveredHotspot(nodeData); if (hovered) { _scriptEngine->run(&hovered->script); return; } // Bad click _sound->playEffect(697, 5); } void Myst3Engine::drawFrame(bool noSwap) { _sound->update(); _gfx->clear(); if (_state->getViewType() == kCube) { float pitch = _state->getLookAtPitch(); float heading = _state->getLookAtHeading(); float fov = _state->getLookAtFOV(); // Apply the rotation effect if (_rotationEffect) { _rotationEffect->update(); heading += _rotationEffect->getHeadingOffset(); _state->lookAt(pitch, heading); } // Apply the shake effect if (_shakeEffect) { _shakeEffect->update(); pitch += _shakeEffect->getPitchOffset(); heading += _shakeEffect->getHeadingOffset(); } _gfx->setupCameraPerspective(pitch, heading, fov); } if (_node) { _node->update(); _gfx->renderDrawable(_node, _scene); } for (int i = _movies.size() - 1; i >= 0 ; i--) { _movies[i]->update(); _gfx->renderDrawable(_movies[i], _scene); } if (_state->getViewType() == kMenu) { _gfx->renderDrawable(_menu, _scene); } for (uint i = 0; i < _drawables.size(); i++) { _gfx->renderDrawable(_drawables[i], _scene); } if (_state->getViewType() != kMenu) { float pitch = _state->getLookAtPitch(); float heading = _state->getLookAtHeading(); SunSpot flare = computeSunspotsIntensity(pitch, heading); if (flare.intensity >= 0) _scene->drawSunspotFlare(flare); } if (isInventoryVisible()) { _gfx->renderWindow(_inventory); } // Draw overlay 2D movies for (int i = _movies.size() - 1; i >= 0 ; i--) { _gfx->renderDrawableOverlay(_movies[i], _scene); } for (uint i = 0; i < _drawables.size(); i++) { _gfx->renderDrawableOverlay(_drawables[i], _scene); } // Draw spot subtitles if (_node) { _gfx->renderDrawableOverlay(_node, _scene); } bool cursorVisible = _cursor->isVisible(); if (getPlatform() == Common::kPlatformXbox) { // The cursor is not drawn in the Xbox version menus and journals cursorVisible &= !(_state->getLocationRoom() == kRoomMenu || _state->getLocationRoom() == kRoomJournals); } if (cursorVisible) _gfx->renderDrawable(_cursor, _scene); _gfx->flipBuffer(); if (!noSwap) { _frameLimiter->delayBeforeSwap(); _system->updateScreen(); _state->updateFrameCounters(); _frameLimiter->startFrame(); } } bool Myst3Engine::isInventoryVisible() { if (_state->getViewType() == kMenu) return false; if (_node && _node->hasSubtitlesToDraw()) return false; if (_inventoryManualHide) { return false; } // Only draw the inventory when the mouse is inside its area if (isWideScreenModEnabled() && !_inventory->isMouseInside()) { return false; } return true; } void Myst3Engine::setupTransition() { delete _transition; _transition = new Transition(this); } void Myst3Engine::drawTransition(TransitionType transitionType) { if (_transition) { _interactive = false; // Don't allow loading while drawing transitions _transition->draw(transitionType); delete _transition; _transition = nullptr; } } void Myst3Engine::goToNode(uint16 nodeID, TransitionType transitionType) { uint16 node = _state->getLocationNextNode(); if (!node) node = nodeID; uint16 room = _state->getLocationNextRoom(); uint16 age = _state->getLocationNextAge(); setupTransition(); ViewType sourceViewType = _state->getViewType(); if (sourceViewType == kCube) { // The lookat direction in the next node should be // the direction of the mouse cursor float pitch, heading; _cursor->getDirection(pitch, heading); _state->lookAt(pitch, heading); } loadNode(node, room, age); _state->setLocationNextNode(0); _state->setLocationNextRoom(0); _state->setLocationNextAge(0); if (_state->getAmbiantPreviousFadeOutDelay() > 0) { _ambient->playCurrentNode(100, _state->getAmbiantPreviousFadeOutDelay()); } drawTransition(transitionType); } void Myst3Engine::loadNode(uint16 nodeID, uint32 roomID, uint32 ageID) { unloadNode(); _scriptEngine->run(&_db->getNodeInitScript()); if (nodeID) _state->setLocationNode(_state->valueOrVarValue(nodeID)); if (roomID) _state->setLocationRoom(_state->valueOrVarValue(roomID)); else roomID = _state->getLocationRoom(); if (ageID) _state->setLocationAge(_state->valueOrVarValue(ageID)); else ageID = _state->getLocationAge(); _db->cacheRoom(roomID, ageID); Common::String newRoomName = _db->getRoomName(roomID, ageID); if ((!_archiveNode || _archiveNode->getRoomName() != newRoomName) && !_db->isCommonRoom(roomID, ageID)) { Common::String nodeFile = Common::String::format("%snodes.m3a", newRoomName.c_str()); _archiveNode->close(); if (!_archiveNode->open(nodeFile.c_str(), newRoomName.c_str())) { error("Unable to open archive %s", nodeFile.c_str()); } } runNodeInitScripts(); if (!_node) { return; // The main init script does not load a node } // The effects can only be created after running the node init scripts _node->initEffects(); _shakeEffect = ShakeEffect::create(this); _rotationEffect = RotationEffect::create(this); // WORKAROUND: In Narayan, the scripts in node NACH 9 test on var 39 // without first reinitializing it leading to Saavedro not always giving // Releeshan to the player when he is trapped between both shields. if (nodeID == 9 && roomID == kRoomNarayan) _state->setVar(39, 0); } void Myst3Engine::unloadNode() { if (!_node) return; // Delete all movies removeMovie(0); // Remove all sunspots for (uint i = 0; i < _sunspots.size(); i++) delete _sunspots[i]; _sunspots.clear(); // Clean up the effects delete _shakeEffect; _shakeEffect = nullptr; _state->setShakeEffectAmpl(0); delete _rotationEffect; _rotationEffect = nullptr; delete _node; _node = nullptr; } void Myst3Engine::runNodeInitScripts() { NodePtr nodeData = _db->getNodeData( _state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); NodePtr nodeDataInit = _db->getNodeData( 32765, _state->getLocationRoom(), _state->getLocationAge()); if (nodeDataInit) runScriptsFromNode(32765); if (!nodeData) error("Node %d unknown in the database", _state->getLocationNode()); for (uint j = 0; j < nodeData->scripts.size(); j++) { if (_state->evaluate(nodeData->scripts[j].condition)) { _scriptEngine->run(&nodeData->scripts[j].script); } } // Mark the node as a reachable zip destination _state->markNodeAsVisited( _state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); } void Myst3Engine::runNodeBackgroundScripts() { NodePtr nodeDataRoom = _db->getNodeData(32765, _state->getLocationRoom(), _state->getLocationAge()); if (nodeDataRoom) { for (uint j = 0; j < nodeDataRoom->hotspots.size(); j++) { if (nodeDataRoom->hotspots[j].condition == -1) { if (!_scriptEngine->run(&nodeDataRoom->hotspots[j].script)) break; } } } NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); for (uint j = 0; j < nodeData->hotspots.size(); j++) { if (nodeData->hotspots[j].condition == -1) { if (!_scriptEngine->run(&nodeData->hotspots[j].script)) break; } } } void Myst3Engine::loadNodeCubeFaces(uint16 nodeID) { _state->setViewType(kCube); _cursor->lockPosition(true); updateCursor(); _node = new NodeCube(this, nodeID); } void Myst3Engine::loadNodeFrame(uint16 nodeID) { _state->setViewType(kFrame); _cursor->lockPosition(false); updateCursor(); _node = new NodeFrame(this, nodeID); } void Myst3Engine::loadNodeMenu(uint16 nodeID) { _state->setViewType(kMenu); _cursor->lockPosition(false); updateCursor(); _node = new NodeFrame(this, nodeID); } void Myst3Engine::runScriptsFromNode(uint16 nodeID, uint32 roomID, uint32 ageID) { if (roomID == 0) roomID = _state->getLocationRoom(); if (ageID == 0) ageID = _state->getLocationAge(); NodePtr nodeData = _db->getNodeData(nodeID, roomID, ageID); for (uint j = 0; j < nodeData->scripts.size(); j++) { if (_state->evaluate(nodeData->scripts[j].condition)) { if (!_scriptEngine->run(&nodeData->scripts[j].script)) break; } } } void Myst3Engine::runBackgroundSoundScriptsFromNode(uint16 nodeID, uint32 roomID, uint32 ageID) { if (_state->getSoundScriptsSuspended()) return; if (roomID == 0) roomID = _state->getLocationRoom(); if (ageID == 0) ageID = _state->getLocationAge(); NodePtr nodeData = _db->getNodeData(nodeID, roomID, ageID); if (!nodeData) return; if (_backgroundSoundScriptLastRoomId != roomID || _backgroundSoundScriptLastAgeId != ageID) { bool sameScript; if ( _backgroundSoundScriptLastRoomId != 0 && roomID != 0 && _backgroundSoundScriptLastAgeId != 0 && ageID != 0) { sameScript = _db->areRoomsScriptsEqual(_backgroundSoundScriptLastRoomId, _backgroundSoundScriptLastAgeId, roomID, ageID, kScriptTypeBackgroundSound); } else { sameScript = false; } // Stop previous music when the music script changes if (!sameScript && _backgroundSoundScriptLastRoomId != kRoomMenu && _backgroundSoundScriptLastRoomId != kRoomJournals && roomID != kRoomMenu && roomID != kRoomJournals) { _sound->stopMusic(_state->getSoundScriptFadeOutDelay()); if (!nodeData->backgroundSoundScripts.empty()) { _state->setSoundScriptsPaused(1); _state->setSoundScriptsTimer(0); } } _backgroundSoundScriptLastRoomId = roomID; _backgroundSoundScriptLastAgeId = ageID; } for (uint j = 0; j < nodeData->backgroundSoundScripts.size(); j++) { if (_state->evaluate(nodeData->backgroundSoundScripts[j].condition)) { if (!_scriptEngine->run(&nodeData->backgroundSoundScripts[j].script)) break; } } } void Myst3Engine::runAmbientScripts(uint32 node) { uint32 room = _ambient->_scriptRoom; uint32 age = _ambient->_scriptAge; if (room == 0) room = _state->getLocationRoom(); if (age == 0) age = _state->getLocationAge(); NodePtr nodeData = _db->getNodeData(node, room, age); if (!nodeData) return; for (uint j = 0; j < nodeData->soundScripts.size(); j++) if (_state->evaluate(nodeData->soundScripts[j].condition)) _scriptEngine->run(&nodeData->soundScripts[j].script); } void Myst3Engine::loadMovie(uint16 id, uint16 condition, bool resetCond, bool loop) { ScriptedMovie *movie; if (!_state->getMovieUseBackground()) { movie = new ScriptedMovie(this, id); } else { movie = new ProjectorMovie(this, id, _projectorBackground); _projectorBackground = nullptr; _state->setMovieUseBackground(0); } movie->setCondition(condition); movie->setDisableWhenComplete(resetCond); movie->setLoop(loop); if (_state->getMovieScriptDriven()) { movie->setScriptDriven(_state->getMovieScriptDriven()); _state->setMovieScriptDriven(0); } if (_state->getMovieStartFrameVar()) { movie->setStartFrameVar(_state->getMovieStartFrameVar()); _state->setMovieStartFrameVar(0); } if (_state->getMovieEndFrameVar()) { movie->setEndFrameVar(_state->getMovieEndFrameVar()); _state->setMovieEndFrameVar(0); } if (_state->getMovieStartFrame()) { movie->setStartFrame(_state->getMovieStartFrame()); _state->setMovieStartFrame(0); } if (_state->getMovieEndFrame()) { movie->setEndFrame(_state->getMovieEndFrame()); _state->setMovieEndFrame(0); } if (_state->getMovieNextFrameGetVar()) { movie->setNextFrameReadVar(_state->getMovieNextFrameGetVar()); _state->setMovieNextFrameGetVar(0); } if (_state->getMovieNextFrameSetVar()) { movie->setNextFrameWriteVar(_state->getMovieNextFrameSetVar()); _state->setMovieNextFrameSetVar(0); } if (_state->getMoviePlayingVar()) { movie->setPlayingVar(_state->getMoviePlayingVar()); _state->setMoviePlayingVar(0); } if (_state->getMovieOverridePosition()) { movie->setPosU(_state->getMovieOverridePosU()); movie->setPosV(_state->getMovieOverridePosV()); _state->setMovieOverridePosition(0); } if (_state->getMovieUVar()) { movie->setPosUVar(_state->getMovieUVar()); _state->setMovieUVar(0); } if (_state->getMovieVVar()) { movie->setPosVVar(_state->getMovieVVar()); _state->setMovieVVar(0); } if (_state->getMovieOverrideCondition()) { movie->setCondition(_state->getMovieOverrideCondition()); _state->setMovieOverrideCondition(0); } if (_state->getMovieConditionBit()) { movie->setConditionBit(_state->getMovieConditionBit()); _state->setMovieConditionBit(0); } if (_state->getMovieForce2d()) { movie->setForce2d(_state->getMovieForce2d()); _state->setMovieForce2d(0); } if (_state->getMovieVolume1()) { movie->setVolume(_state->getMovieVolume1()); _state->setMovieVolume1(0); } else { movie->setVolume(_state->getMovieVolume2()); } if (_state->getMovieVolumeVar()) { movie->setVolumeVar(_state->getMovieVolumeVar()); _state->setMovieVolumeVar(0); } if (_state->getMovieSoundHeading()) { movie->setSoundHeading(_state->getMovieSoundHeading()); _state->setMovieSoundHeading(0); } if (_state->getMoviePanningStrenght()) { movie->setSoundAttenuation(_state->getMoviePanningStrenght()); _state->setMoviePanningStrenght(0); } if (_state->getMovieAdditiveBlending()) { movie->setAdditiveBlending(true); _state->setMovieAdditiveBlending(0); } if (_state->getMovieTransparency()) { movie->setTransparency(_state->getMovieTransparency()); _state->setMovieTransparency(0); } else { movie->setTransparency(100); } if (_state->getMovieTransparencyVar()) { movie->setTransparencyVar(_state->getMovieTransparencyVar()); _state->setMovieTransparencyVar(0); } _movies.push_back(movie); } void Myst3Engine::playSimpleMovie(uint16 id, bool fullframe, bool refreshAmbientSounds) { SimpleMovie movie(this, id); if (!movie.isVideoLoaded()) { // The video was not loaded and it was optional, just do nothing return; } if (_state->getMovieSynchronized()) { movie.setSynchronized(_state->getMovieSynchronized()); _state->setMovieSynchronized(0); } if (_state->getMovieStartFrame()) { movie.setStartFrame(_state->getMovieStartFrame()); _state->setMovieStartFrame(0); } if (_state->getMovieEndFrame()) { movie.setEndFrame(_state->getMovieEndFrame()); _state->setMovieEndFrame(0); } if (_state->getMovieVolume1()) { movie.setVolume(_state->getMovieVolume1()); _state->setMovieVolume1(0); } else { movie.setVolume(_state->getMovieVolume2()); } if (fullframe) { movie.setForce2d(_state->getViewType() == kCube); movie.setForceOpaque(true); movie.setPosU(0); movie.setPosV(_state->getViewType() == kMenu ? Renderer::kTopBorderHeight : 0); } movie.play(); if (refreshAmbientSounds) { movie.refreshAmbientSounds(); } _drawables.push_back(&movie); while (!shouldQuit() && !movie.endOfVideo()) { movie.update(); // Process events processInput(false); // Handle skipping if (_inputSpacePressed || _inputEscapePressed) { // Consume the escape key press so the menu does not open _inputEscapePressedNotConsumed = false; break; } drawFrame(); } _drawables.pop_back(); // Reset the movie script so that the next movie will not try to run them // when the user has skipped this one before the script is triggered. _state->setMovieScriptStartFrame(0); _state->setMovieScript(0); _state->setMovieAmbiantScriptStartFrame(0); _state->setMovieAmbiantScript(0); } void Myst3Engine::removeMovie(uint16 id) { if (id == 0) { for (uint i = 0; i < _movies.size(); i++) delete _movies[i]; _movies.clear(); return; } else { for (uint i = 0; i < _movies.size(); i++) if (_movies[i]->getId() == id) { delete _movies[i]; _movies.remove_at(i); break; } } } void Myst3Engine::setMovieLooping(uint16 id, bool loop) { for (uint i = 0; i < _movies.size(); i++) { if (_movies[i]->getId() == id) { // Enable or disable looping _movies[i]->setLoop(loop); _movies[i]->setDisableWhenComplete(!loop); break; } } } void Myst3Engine::addSpotItem(uint16 id, int16 condition, bool fade) { assert(_node); _node->loadSpotItem(id, condition, fade); } SpotItemFace *Myst3Engine::addMenuSpotItem(uint16 id, int16 condition, const Common::Rect &rect) { assert(_node); SpotItemFace *face = _node->loadMenuSpotItem(condition, rect); _menu->setSaveLoadSpotItem(id, face); return face; } void Myst3Engine::loadNodeSubtitles(uint32 id) { assert(_node); _node->loadSubtitles(id); } ResourceDescription Myst3Engine::getFileDescription(const Common::String &room, uint32 index, uint16 face, Archive::ResourceType type) { Common::String archiveRoom = room; if (archiveRoom == "") { archiveRoom = _db->getRoomName(_state->getLocationRoom(), _state->getLocationAge()); } ResourceDescription desc; // Search common archives uint i = 0; while (!desc.isValid() && i < _archivesCommon.size()) { desc = _archivesCommon[i]->getDescription(archiveRoom, index, face, type); i++; } // Search currently loaded node archive if (!desc.isValid() && _archiveNode) desc = _archiveNode->getDescription(archiveRoom, index, face, type); return desc; } ResourceDescriptionArray Myst3Engine::listFilesMatching(const Common::String &room, uint32 index, uint16 face, Archive::ResourceType type) { Common::String archiveRoom = room; if (archiveRoom == "") { archiveRoom = _db->getRoomName(_state->getLocationRoom(), _state->getLocationAge()); } for (uint i = 0; i < _archivesCommon.size(); i++) { ResourceDescriptionArray list = _archivesCommon[i]->listFilesMatching(archiveRoom, index, face, type); if (!list.empty()) { return list; } } return _archiveNode->listFilesMatching(archiveRoom, index, face, type); } Graphics::Surface *Myst3Engine::loadTexture(uint16 id) { ResourceDescription desc = getFileDescription("GLOB", id, 0, Archive::kRawData); if (!desc.isValid()) error("Texture %d does not exist", id); Common::SeekableReadStream *data = desc.getData(); uint32 magic = data->readUint32LE(); if (magic != MKTAG('.', 'T', 'E', 'X')) error("Wrong texture format %d", id); data->readUint32LE(); // unk 1 uint32 width = data->readUint32LE(); uint32 height = data->readUint32LE(); data->readUint32LE(); // unk 2 data->readUint32LE(); // unk 3 #ifdef SCUMM_BIG_ENDIAN Graphics::PixelFormat onDiskFormat = Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 24, 16, 8); #else Graphics::PixelFormat onDiskFormat = Graphics::PixelFormat(4, 8, 8, 8, 8, 8, 16, 24, 0); #endif Graphics::Surface *s = new Graphics::Surface(); s->create(width, height, onDiskFormat); data->read(s->getPixels(), height * s->pitch); delete data; s->convertToInPlace(Texture::getRGBAPixelFormat()); return s; } Graphics::Surface *Myst3Engine::decodeJpeg(const ResourceDescription *jpegDesc) { Common::SeekableReadStream *jpegStream = jpegDesc->getData(); Image::JPEGDecoder jpeg; jpeg.setOutputPixelFormat(Texture::getRGBAPixelFormat()); if (!jpeg.loadStream(*jpegStream)) error("Could not decode Myst III JPEG"); delete jpegStream; const Graphics::Surface *bitmap = jpeg.getSurface(); assert(bitmap->format == Texture::getRGBAPixelFormat()); // JPEGDecoder owns the decoded surface, we have to make a copy... Graphics::Surface *rgbaSurface = new Graphics::Surface(); rgbaSurface->copyFrom(*bitmap); return rgbaSurface; } int16 Myst3Engine::openDialog(uint16 id) { Dialog *dialog; if (getPlatform() == Common::kPlatformXbox) { dialog = new GamepadDialog(this, id); } else { dialog = new ButtonsDialog(this, id); } _drawables.push_back(dialog); int16 result = -2; while (result == -2 && !shouldQuit()) { result = dialog->update(); drawFrame(); } _drawables.pop_back(); delete dialog; return result; } void Myst3Engine::dragSymbol(uint16 var, uint16 id) { DragItem drag(this, id); _drawables.push_back(&drag); _cursor->changeCursor(2); _state->setVar(var, -1); NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); while (inputValidatePressed() && !shouldQuit()) { processInput(false); HotSpot *hovered = getHoveredHotspot(nodeData, var); drag.setFrame(hovered ? 2 : 1); drawFrame(); } _state->setVar(var, 1); _drawables.pop_back(); HotSpot *hovered = getHoveredHotspot(nodeData, var); if (hovered) { _cursor->setVisible(false); _scriptEngine->run(&hovered->script); _cursor->setVisible(true); } } void Myst3Engine::dragItem(uint16 statusVar, uint16 movie, uint16 frame, uint16 hoverFrame, uint16 itemVar) { DragItem drag(this, movie); _drawables.push_back(&drag); _cursor->changeCursor(2); _state->setVar(statusVar, 0); _state->setVar(itemVar, 1); NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge()); while (inputValidatePressed() && !shouldQuit()) { processInput(false); HotSpot *hovered = getHoveredHotspot(nodeData, itemVar); drag.setFrame(hovered ? hoverFrame : frame); drawFrame(); } _drawables.pop_back(); HotSpot *hovered = getHoveredHotspot(nodeData, itemVar); if (hovered) { _cursor->setVisible(false); _scriptEngine->run(&hovered->script); _cursor->setVisible(true); } else { _state->setVar(statusVar, 1); _state->setVar(itemVar, 0); } } bool Myst3Engine::canSaveGameStateCurrently() { bool inMenuWithNoGameLoaded = _state->getLocationRoom() == kRoomMenu && _state->getMenuSavedAge() == 0; return canLoadGameStateCurrently() && !inMenuWithNoGameLoaded && _cursor->isVisible(); } bool Myst3Engine::canLoadGameStateCurrently() { // Loading from the GMM is only possible when the game is interactive // This is to prevent loading from inner loops. Loading while // in an inner loop can cause the exit condition to never happen, // or can unload required resources. return _interactive; } Common::Error Myst3Engine::loadGameState(int slot) { Common::StringArray filenames = Saves::list(_saveFileMan, getPlatform()); return loadGameState(filenames[slot], kTransitionNone); } Common::Error Myst3Engine::loadGameState(Common::String fileName, TransitionType transition) { Common::SharedPtr saveFile = Common::SharedPtr(_saveFileMan->openForLoading(fileName)); if (!saveFile) { return Common::kReadingFailed; } Common::Error loadError = _state->load(saveFile.get()); if (loadError.getCode() != Common::kNoError) { return loadError; } if (saveFile->eos()) { warning("Unexpected end of file reached when reading '%s'", fileName.c_str()); return Common::kReadingFailed; } if (saveFile->err()) { warning("An error occrured when reading '%s'", fileName.c_str()); return Common::kReadingFailed; } _inventory->loadFromState(); // Leave the load menu _state->setViewType(kMenu); _state->setLocationNextAge(_state->getMenuSavedAge()); _state->setLocationNextRoom(_state->getMenuSavedRoom()); _state->setLocationNextNode(_state->getMenuSavedNode()); _state->setMenuSavedAge(0); _state->setMenuSavedRoom(0); _state->setMenuSavedNode(0); _sound->resetSoundVars(); _sound->stopMusic(15); _state->setSoundScriptsSuspended(0); _sound->playEffect(696, 60); goToNode(0, transition); return Common::kNoError; } 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 Myst3Engine::saveGameState(int slot, const Common::String &desc, bool isAutosave) { assert(!desc.empty()); if (!isValidSaveFileName(desc)) { return Common::Error(Common::kCreatingFileFailed, _("Invalid file name for saving")); } // Try to use an already generated thumbnail const Graphics::Surface *thumbnail = _menu->borrowSaveThumbnail(); if (!thumbnail) { _menu->generateSaveThumbnail(); } thumbnail = _menu->borrowSaveThumbnail(); assert(thumbnail); return saveGameState(desc, thumbnail, isAutosave); } Common::Error Myst3Engine::saveGameState(const Common::String &desc, const Graphics::Surface *thumbnail, bool isAutosave) { // Strip extension Common::String saveName = desc; if (desc.hasSuffixIgnoreCase(".M3S") || desc.hasSuffixIgnoreCase(".M3X")) { saveName.erase(desc.size() - 4, desc.size()); } Common::String fileName = Saves::buildName(saveName.c_str(), getPlatform()); // Save the state and the thumbnail Common::SharedPtr save = Common::SharedPtr(_saveFileMan->openForSaving(fileName)); if (!save) { return Common::kCreatingFileFailed; } Common::Error saveError = _state->save(save.get(), saveName, thumbnail, isAutosave); if (saveError.getCode() != Common::kNoError) { return saveError; } if (save->err()) { warning("An error occurred when writing '%s'", fileName.c_str()); return Common::kWritingFailed; } return saveError; } void Myst3Engine::animateDirectionChange(float targetPitch, float targetHeading, uint16 scriptTicks) { float startPitch = _state->getLookAtPitch(); float startHeading = _state->getLookAtHeading(); if (startPitch == targetPitch && startHeading == targetHeading) return; // Fast path float pitchDistance = targetPitch - startPitch; float headingDistance = targetHeading - startHeading; // Make sure to use the shortest direction while (ABS(headingDistance) > 180) { if (headingDistance > 0) { headingDistance -= 360; } else { headingDistance += 360; } } // Compute animation duration in frames float numTicks; if (scriptTicks) { numTicks = scriptTicks; } else { numTicks = sqrt(pitchDistance * pitchDistance + headingDistance * headingDistance) * 30.0f / _state->getCameraMoveSpeed(); if (numTicks > 0.0f) numTicks += 10.0f; } uint startTick = _state->getTickCount(); // Draw animation if (numTicks != 0.0f) { while (1) { uint elapsedTicks = _state->getTickCount() - startTick; if (elapsedTicks >= numTicks || shouldQuit()) break; float step; if (numTicks >= 15) { // Fast then slow movement if (elapsedTicks > numTicks / 2.0f) step = 1.0f - (numTicks - elapsedTicks) * (numTicks - elapsedTicks) / (numTicks / 2.0f * numTicks / 2.0f) / 2.0f; else step = elapsedTicks * elapsedTicks / (numTicks / 2.0f * numTicks / 2.0f) / 2.0f; } else { // Constant speed movement step = elapsedTicks / numTicks; } float nextPitch = startPitch + pitchDistance * step; float nextHeading = startHeading + headingDistance * step; _state->lookAt(nextPitch, nextHeading); drawFrame(); } } _state->lookAt(targetPitch, targetHeading); drawFrame(); } void Myst3Engine::getMovieLookAt(uint16 id, bool start, float &pitch, float &heading) { ResourceDescription desc = getFileDescription("", id, 0, Archive::kMovie); if (!desc.isValid()) desc = getFileDescription("", id, 0, Archive::kMultitrackMovie); if (!desc.isValid()) error("Movie %d does not exist", id); Math::Vector3d v; if (start) v = desc.getVideoData().v1; else v = desc.getVideoData().v2; Math::Vector2d horizontalProjection(v.x(), v.z()); horizontalProjection.normalize(); pitch = 90 - Math::Angle::arcCosine(v.y()).getDegrees(); heading = Math::Angle::arcCosine(horizontalProjection.getY()).getDegrees(); if (horizontalProjection.getX() < 0.0) { heading = 360 - heading; } } void Myst3Engine::playMovieGoToNode(uint16 movie, uint16 node) { uint16 room = _state->getLocationNextRoom(); uint16 age = _state->getLocationNextAge(); if (_state->getLocationNextNode()) { node = _state->getLocationNextNode(); } if (_state->getViewType() == kCube && !_state->getCameraSkipAnimation()) { float startPitch, startHeading; getMovieLookAt(movie, true, startPitch, startHeading); animateDirectionChange(startPitch, startHeading, 0); } _state->setCameraSkipAnimation(0); loadNode(node, room, age); playSimpleMovie(movie, true, true); _state->setLocationNextNode(0); _state->setLocationNextRoom(0); _state->setLocationNextAge(0); if (_state->getViewType() == kCube) { float endPitch, endHeading; getMovieLookAt(movie, false, endPitch, endHeading); _state->lookAt(endPitch, endHeading); } setupTransition(); } void Myst3Engine::playMovieFullFrame(uint16 movie) { if (_state->getViewType() == kCube && !_state->getCameraSkipAnimation()) { float startPitch, startHeading; getMovieLookAt(movie, true, startPitch, startHeading); animateDirectionChange(startPitch, startHeading, 0); } _state->setCameraSkipAnimation(0); playSimpleMovie(movie, true, false); if (_state->getViewType() == kCube) { float endPitch, endHeading; getMovieLookAt(movie, false, endPitch, endHeading); _state->lookAt(endPitch, endHeading); } setupTransition(); } bool Myst3Engine::inputValidatePressed() { return _inputEnterPressed || _inputSpacePressed || getEventManager()->getButtonState() & Common::EventManager::LBUTTON; } bool Myst3Engine::inputEscapePressed() { return _inputEscapePressed; } bool Myst3Engine::inputSpacePressed() { return _inputSpacePressed; } bool Myst3Engine::inputTilePressed() { return _inputTildePressed; } void Myst3Engine::addSunSpot(uint16 pitch, uint16 heading, uint16 intensity, uint16 color, uint16 var, bool varControlledIntensity, uint16 radius) { SunSpot *s = new SunSpot(); s->pitch = pitch; s->heading = heading; s->intensity = intensity * 2.55; s->color = (color & 0xF) | 16 * ((color & 0xF) | 16 * (((color >> 4) & 0xF) | 16 * (((color >> 4) & 0xF) | 16 * (((color >> 8) & 0xF) | 16 * (((color >> 8) & 0xF)))))); s->var = var; s->variableIntensity = varControlledIntensity; s->radius = radius; _sunspots.push_back(s); } SunSpot Myst3Engine::computeSunspotsIntensity(float pitch, float heading) { SunSpot result; result.intensity = -1; result.color = 0; result.radius = 0; for (uint i = 0; i < _sunspots.size(); i++) { SunSpot *s = _sunspots[i]; uint32 value = _state->getVar(s->var); // Skip disabled items if (value == 0) continue; float distance = _scene->distanceToZone(s->heading, s->pitch, s->radius, heading, pitch); if (distance > result.radius) { result.radius = distance; result.color = s->color; result.intensity = s->intensity; result.variableIntensity = s->variableIntensity; if (result.variableIntensity) { result.radius = value * distance / 100; } } } return result; } void Myst3Engine::settingsInitDefaults() { int defaultLanguage = _db->getGameLanguageCode(); int defaultTextLanguage; if (getGameLocalizationType() == kLocMulti6) defaultTextLanguage = defaultLanguage; else defaultTextLanguage = getGameLanguage() != Common::EN_ANY; ConfMan.registerDefault("overall_volume", Audio::Mixer::kMaxMixerVolume); ConfMan.registerDefault("music_volume", Audio::Mixer::kMaxMixerVolume / 2); ConfMan.registerDefault("music_frequency", 75); ConfMan.registerDefault("audio_language", defaultLanguage); ConfMan.registerDefault("text_language", defaultTextLanguage); ConfMan.registerDefault("water_effects", true); ConfMan.registerDefault("transition_speed", 50); ConfMan.registerDefault("mouse_speed", 50); ConfMan.registerDefault("mouse_inverted", false); ConfMan.registerDefault("zip_mode", false); ConfMan.registerDefault("subtitles", false); ConfMan.registerDefault("vibrations", true); // Xbox specific } void Myst3Engine::settingsLoadToVars() { _state->setWaterEffects(ConfMan.getBool("water_effects")); _state->setTransitionSpeed(ConfMan.getInt("transition_speed")); _state->setMouseSpeed(ConfMan.getInt("mouse_speed")); _state->setZipModeEnabled(ConfMan.getBool("zip_mode")); _state->setSubtitlesEnabled(ConfMan.getBool("subtitles")); if (getPlatform() != Common::kPlatformXbox) { _state->setOverallVolume(CLIP(ConfMan.getInt("overall_volume") * 100 / 256, 0, 100)); _state->setMusicVolume(CLIP(ConfMan.getInt("music_volume") * 100 / 256, 0, 100)); _state->setMusicFrequency(ConfMan.getInt("music_frequency")); _state->setLanguageAudio(ConfMan.getInt("audio_language")); _state->setLanguageText(ConfMan.getInt("text_language")); } else { _state->setVibrationEnabled(ConfMan.getBool("vibrations")); } } void Myst3Engine::settingsApplyFromVars() { int32 oldTextLanguage = ConfMan.getInt("text_language"); ConfMan.setInt("transition_speed", _state->getTransitionSpeed()); ConfMan.setInt("mouse_speed", _state->getMouseSpeed()); ConfMan.setBool("zip_mode", _state->getZipModeEnabled()); ConfMan.setBool("subtitles", _state->getSubtitlesEnabled()); if (getPlatform() != Common::kPlatformXbox) { ConfMan.setInt("overall_volume", _state->getOverallVolume() * 256 / 100); ConfMan.setInt("music_volume", _state->getMusicVolume() * 256 / 100); ConfMan.setInt("music_frequency", _state->getMusicFrequency()); ConfMan.setInt("audio_language", _state->getLanguageAudio()); ConfMan.setInt("text_language", _state->getLanguageText()); ConfMan.setBool("water_effects", _state->getWaterEffects()); // The language changed, reload the correct archives if (_state->getLanguageText() != oldTextLanguage) { closeArchives(); openArchives(); } } else { ConfMan.setBool("vibrations", _state->getVibrationEnabled()); } // Mouse speed may have changed, refresh it _scene->updateMouseSpeed(); syncSoundSettings(); } void Myst3Engine::syncSoundSettings() { Engine::syncSoundSettings(); uint soundOverall = ConfMan.getInt("overall_volume"); uint soundVolumeMusic = ConfMan.getInt("music_volume"); _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, soundOverall); _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic * soundOverall / 256); } bool Myst3Engine::isWideScreenModEnabled() const { return ConfMan.getBool("widescreen_mod"); } void Myst3Engine::pauseEngineIntern(bool pause) { Engine::pauseEngineIntern(pause); if (!_state || !_cursor) { // This method may be called before the engine is fully initialized return; } for (uint i = 0; i < _movies.size(); i++) { _movies[i]->pause(pause); } _state->pauseEngine(pause); // Grab a game screen thumbnail in case we need one when writing a save file if (pause && !_menu->isOpen()) { // Opening the menu generates a save thumbnail so we only generate it if the menu is not open _menu->generateSaveThumbnail(); } // Unlock the mouse so that the cursor is visible when the GMM opens if (_state->getViewType() == kCube && _cursor->isPositionLocked()) { _system->lockMouse(!pause); } // The user may have moved the mouse or resized the screen while the engine was paused if (!pause) { _gfx->computeScreenViewport(); _cursor->updatePosition(_eventMan->getMousePos()); _inventory->reflow(); } } } // end of namespace Myst3