scummvm/engines/myst3/myst3.cpp
2023-12-24 13:19:25 +01:00

1951 lines
53 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#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/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.getPath("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::Renderer::parseTypeCode(rendererConfig);
Graphics::RendererType matchingRendererType = Graphics::Renderer::getBestMatchingAvailableType(desiredRendererType,
#if defined(USE_OPENGL_GAME)
Graphics::kRendererTypeOpenGL |
#endif
#if defined(USE_OPENGL_SHADERS)
Graphics::kRendererTypeOpenGLShaders |
#endif
#if defined(USE_TINYGL)
Graphics::kRendererTypeTinyGL |
#endif
0);
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")) {
const char* urlForPatchesDownload = "https://www.scummvm.org/frs/extras/patches/";
warning("Unable to open the update game archive 'OVER101.m3o'");
Common::U32String updateMessage =
Common::U32String::format(_("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"
"%s"), urlForPatchesDownload);
warning("%s", updateMessage.encode().c_str());
GUIErrorMessageWithURL(updateMessage, urlForPatchesDownload);
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(Common::U32String *msg) {
bool inMenuWithNoGameLoaded = _state->getLocationRoom() == kRoomMenu && _state->getMenuSavedAge() == 0;
return canLoadGameStateCurrently() && !inMenuWithNoGameLoaded && _cursor->isVisible();
}
bool Myst3Engine::canLoadGameStateCurrently(Common::U32String *msg) {
// 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<Common::InSaveFile> saveFile = Common::SharedPtr<Common::InSaveFile>(_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<Common::OutSaveFile> save = Common::SharedPtr<Common::OutSaveFile>(_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<uint>(ConfMan.getInt("overall_volume") * 100 / 256, 0, 100));
_state->setMusicVolume(CLIP<uint>(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