scummvm/engines/myst3/myst3.cpp
Le Philousophe 1e4696f6d0 OPENGL: Rework renderer selection code
Add a class to group all renderer related (static) functions.
This allows to have getBestMatchingAvailableType inline in all engines.

The matching code is now shared between all engines but allows
customization for engines needing it (Grim, WME3D).

The new code takes runtime availability of features to select the
best renderer.
It avoid crashes when user choosed OpenGL but GLES2 is used.
2022-04-03 22:17:19 +02:00

1954 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/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::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")) {
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<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