/* ResidualVM - A 3D game interpreter * * ResidualVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the 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 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #define FORBIDDEN_SYMBOL_EXCEPTION_fprintf #define FORBIDDEN_SYMBOL_EXCEPTION_fgetc #define FORBIDDEN_SYMBOL_EXCEPTION_stderr #define FORBIDDEN_SYMBOL_EXCEPTION_stdin #include "common/archive.h" #include "common/debug-channels.h" #include "common/file.h" #include "common/foreach.h" #include "common/fs.h" #include "common/config-manager.h" #include "common/translation.h" #include "graphics/pixelbuffer.h" #include "graphics/renderer.h" #ifdef USE_OPENGL #include "graphics/opengl/context.h" #endif #include "gui/error.h" #include "gui/gui-manager.h" #include "gui/message.h" #include "image/png.h" #include "engines/engine.h" #include "engines/util.h" #include "engines/grim/md5check.h" #include "engines/grim/md5checkdialog.h" #include "engines/grim/debug.h" #include "engines/grim/grim.h" #include "engines/grim/lua.h" #include "engines/grim/lua_v1.h" #include "engines/grim/emi/poolsound.h" #include "engines/grim/emi/layer.h" #include "engines/grim/actor.h" #include "engines/grim/movie/movie.h" #include "engines/grim/savegame.h" #include "engines/grim/registry.h" #include "engines/grim/resource.h" #include "engines/grim/localize.h" #include "engines/grim/gfx_base.h" #include "engines/grim/bitmap.h" #include "engines/grim/font.h" #include "engines/grim/primitives.h" #include "engines/grim/objectstate.h" #include "engines/grim/set.h" #include "engines/grim/sound.h" #include "engines/grim/stuffit.h" #include "engines/grim/debugger.h" #include "engines/grim/imuse/imuse.h" #include "engines/grim/emi/sound/emisound.h" #include "engines/grim/lua/lua.h" namespace Grim { GrimEngine *g_grim = nullptr; GfxBase *g_driver = nullptr; int g_imuseState = -1; GrimEngine::GrimEngine(OSystem *syst, uint32 gameFlags, GrimGameType gameType, Common::Platform platform, Common::Language language) : Engine(syst), _currSet(nullptr), _selectedActor(nullptr), _pauseStartTime(0) { g_grim = this; setDebugger(new Debugger()); _gameType = gameType; _gameFlags = gameFlags; _gamePlatform = platform; _gameLanguage = language; if (getGameType() == GType_GRIM) g_registry = new Registry(); else g_registry = nullptr; g_resourceloader = nullptr; g_localizer = nullptr; g_movie = nullptr; g_imuse = nullptr; //Set default settings ConfMan.registerDefault("use_arb_shaders", true); _showFps = ConfMan.getBool("show_fps"); _softRenderer = true; _mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, 192); _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, ConfMan.getInt("sfx_volume")); _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, ConfMan.getInt("speech_volume")); _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, ConfMan.getInt("music_volume")); _currSet = nullptr; _selectedActor = nullptr; _controlsEnabled = new bool[KEYCODE_EXTRA_LAST]; _controlsState = new bool[KEYCODE_EXTRA_LAST]; for (int i = 0; i < KEYCODE_EXTRA_LAST; i++) { _controlsEnabled[i] = false; _controlsState[i] = false; } _joyAxisPosition = new float[NUM_JOY_AXES]; for (int i = 0; i < NUM_JOY_AXES; i++) { _joyAxisPosition[i] = 0; } _speechMode = TextAndVoice; _textSpeed = 7; _mode = _previousMode = NormalMode; _flipEnable = true; int speed = ConfMan.getInt("engine_speed"); if (speed == 0) { _speedLimitMs = 0; } else if (speed < 0 || speed > 100) { _speedLimitMs = 1000 / 60; ConfMan.setInt("engine_speed", 1000 / _speedLimitMs); } else { _speedLimitMs = 1000 / speed; } _listFilesIter = nullptr; _savedState = nullptr; _fps[0] = 0; _iris = new Iris(); _buildActiveActorsList = false; Color c(0, 0, 0); _printLineDefaults.setX(0); _printLineDefaults.setY(100); _printLineDefaults.setWidth(0); _printLineDefaults.setHeight(0); _printLineDefaults.setFGColor(c); _printLineDefaults.setFont(nullptr); _printLineDefaults.setJustify(TextObject::LJUSTIFY); _sayLineDefaults.setX(0); _sayLineDefaults.setY(100); _sayLineDefaults.setWidth(0); _sayLineDefaults.setHeight(0); _sayLineDefaults.setFGColor(c); _sayLineDefaults.setFont(nullptr); _sayLineDefaults.setJustify(TextObject::CENTER); _blastTextDefaults.setX(0); _blastTextDefaults.setY(200); _blastTextDefaults.setWidth(0); _blastTextDefaults.setHeight(0); _blastTextDefaults.setFGColor(c); _blastTextDefaults.setFont(nullptr); _blastTextDefaults.setJustify(TextObject::LJUSTIFY); const Common::FSNode gameDataDir(ConfMan.get("path")); SearchMan.addSubDirectoryMatching(gameDataDir, "movies"); // Add 'movies' subdirectory for the demo SearchMan.addSubDirectoryMatching(gameDataDir, "credits"); SearchMan.addSubDirectoryMatching(gameDataDir, "widescreen"); Debug::registerDebugChannels(); } GrimEngine::~GrimEngine() { delete[] _controlsEnabled; delete[] _controlsState; delete[] _joyAxisPosition; clearPools(); delete LuaBase::instance(); if (g_registry) { g_registry->save(); delete g_registry; g_registry = nullptr; } delete g_movie; g_movie = nullptr; delete g_imuse; g_imuse = nullptr; delete g_emiSound; g_emiSound = nullptr; delete g_sound; g_sound = nullptr; delete g_localizer; g_localizer = nullptr; delete g_resourceloader; g_resourceloader = nullptr; delete g_driver; g_driver = nullptr; delete _iris; ConfMan.flushToDisk(); DebugMan.clearAllDebugChannels(); g_grim = nullptr; } void GrimEngine::clearPools() { Set::getPool().deleteObjects(); Actor::getPool().deleteObjects(); PrimitiveObject::getPool().deleteObjects(); TextObject::getPool().deleteObjects(); Bitmap::getPool().deleteObjects(); Font::getPool().deleteObjects(); ObjectState::getPool().deleteObjects(); _currSet = nullptr; } LuaBase *GrimEngine::createLua() { return new Lua_V1(); } GfxBase *GrimEngine::createRenderer(int screenW, int screenH, bool fullscreen) { Common::String rendererConfig = ConfMan.get("renderer"); Graphics::RendererType desiredRendererType = Graphics::parseRendererTypeCode(rendererConfig); Graphics::RendererType matchingRendererType = Graphics::getBestMatchingAvailableRendererType(desiredRendererType); _softRenderer = matchingRendererType == Graphics::kRendererTypeTinyGL; initGraphics3d(screenW, screenH, fullscreen, !_softRenderer); #if defined(USE_OPENGL) // Check the OpenGL context actually supports shaders if (matchingRendererType == Graphics::kRendererTypeOpenGLShaders && !OpenGLContext.shadersSupported) { matchingRendererType = Graphics::kRendererTypeOpenGL; } #endif if (matchingRendererType != desiredRendererType && desiredRendererType != Graphics::kRendererTypeDefault) { // Display a warning if unable to use the desired renderer warning("Unable to create a '%s' renderer", rendererConfig.c_str()); } GfxBase *renderer = nullptr; #if defined(USE_GLES2) || defined(USE_OPENGL_SHADERS) if (matchingRendererType == Graphics::kRendererTypeOpenGLShaders) { renderer = CreateGfxOpenGLShader(); } #endif #if defined(USE_OPENGL) && !defined(USE_GLES2) if (matchingRendererType == Graphics::kRendererTypeOpenGL) { renderer = CreateGfxOpenGL(); } #endif if (matchingRendererType == Graphics::kRendererTypeTinyGL) { renderer = CreateGfxTinyGL(); } if (!renderer) { error("Unable to create a '%s' renderer", rendererConfig.c_str()); } renderer->setupScreen(screenW, screenH, fullscreen); renderer->loadEmergFont(); return renderer; } const char *GrimEngine::getUpdateFilename() { if (!(getGameFlags() & ADGF_DEMO)) return "gfupd101.exe"; else return nullptr; } Common::Error GrimEngine::run() { // Try to see if we have the EMI Mac installer present // Currently, this requires the data fork to be standalone if (getGameType() == GType_MONKEY4) { if (SearchMan.hasFile("Monkey Island 4 Installer")) { StuffItArchive *archive = new StuffItArchive(); if (archive->open("Monkey Island 4 Installer")) SearchMan.add("Monkey Island 4 Installer", archive, 0, true); else delete archive; } if (SearchMan.hasFile("EFMI Installer")) { StuffItArchive *archive = new StuffItArchive(); if (archive->open("EFMI Installer")) SearchMan.add("EFMI Installer", archive, 0, true); else delete archive; } } ConfMan.registerDefault("check_gamedata", true); if (ConfMan.getBool("check_gamedata")) { MD5CheckDialog d; if (!d.runModal()) { Common::U32String confirmString = Common::U32String::format(_( "ResidualVM found some problems with your game data files.\n" "Running ResidualVM nevertheless may cause game bugs or even crashes.\n" "Do you still want to run %s?"), GType_MONKEY4 == getGameType() ? "Escape From Monkey Island" : "Grim Fandango" ); GUI::MessageDialog msg(confirmString, _("Yes"), _("No")); if (!msg.runModal()) { return Common::kUserCanceled; } } ConfMan.setBool("check_gamedata", false); ConfMan.flushToDisk(); } g_resourceloader = new ResourceLoader(); bool demo = getGameFlags() & ADGF_DEMO; if (getGameType() == GType_GRIM) g_movie = CreateSmushPlayer(demo); else if (getGameType() == GType_MONKEY4) { if (_gamePlatform == Common::kPlatformPS2) g_movie = CreateMpegPlayer(); else g_movie = CreateBinkPlayer(demo); } if (getGameType() == GType_GRIM) { g_imuse = new Imuse(20, demo); g_emiSound = nullptr; } else if (getGameType() == GType_MONKEY4) { g_emiSound = new EMISound(20); g_imuse = nullptr; } g_sound = new SoundPlayer(); bool fullscreen = ConfMan.getBool("fullscreen"); g_driver = createRenderer(640, 480, fullscreen); if (getGameType() == GType_MONKEY4 && SearchMan.hasFile("AMWI.m4b")) { // Play EMI Mac Aspyr logo playAspyrLogo(); } Bitmap *splash_bm = nullptr; if (!(_gameFlags & ADGF_DEMO) && getGameType() == GType_GRIM) splash_bm = Bitmap::create("splash.bm"); else if ((_gameFlags & ADGF_DEMO) && getGameType() == GType_MONKEY4) splash_bm = Bitmap::create("splash.til"); else if (getGamePlatform() == Common::kPlatformPS2 && getGameType() == GType_MONKEY4) splash_bm = Bitmap::create("load.tga"); g_driver->clearScreen(); if (splash_bm != nullptr) splash_bm->draw(); // This flipBuffer() may make the OpenGL renderer show garbage instead of the splash, // while the TinyGL renderer needs it. if (_softRenderer) g_driver->flipBuffer(); LuaBase *lua = createLua(); lua->registerOpcodes(); lua->registerLua(); // One version of the demo doesn't set the demo flag in scripts. if (getGameType() == GType_GRIM && _gameFlags & ADGF_DEMO) { lua->forceDemo(); } //Initialize Localizer first. In system-script are already localizeable Strings g_localizer = new Localizer(); lua->loadSystemScript(); lua->boot(); _savegameLoadRequest = false; _savegameSaveRequest = false; // Load game from specified slot, if any if (ConfMan.hasKey("save_slot")) { loadGameState(ConfMan.getInt("save_slot")); } g_grim->setMode(NormalMode); delete splash_bm; g_grim->mainLoop(); return Common::kNoError; } void GrimEngine::playAspyrLogo() { // A trimmed down version of the code found in mainloop // for the purpose of playing the Aspyr-logo. // The reason for this, is that the logo needs a different // codec than all the other videos (which are Bink). // Code is provided to keep within the fps-limit, as well as to // allow for pressing ESC to skip the movie. MoviePlayer *defaultPlayer = g_movie; g_movie = CreateQuickTimePlayer(); g_movie->play("AMWI.m4b", false, 0, 0); setMode(SmushMode); while (g_movie->isPlaying()) { _doFlip = true; uint32 startTime = g_system->getMillis(); updateDisplayScene(); if (_doFlip) { doFlip(); } // Process events to allow the user to skip the logo. Common::Event event; while (g_system->getEventManager()->pollEvent(event)) { // Ignore everything but ESC when movies are playing Common::EventType type = event.type; if (type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) { g_movie->stop(); break; } } uint32 endTime = g_system->getMillis(); if (startTime > endTime) continue; uint32 diffTime = endTime - startTime; if (_speedLimitMs == 0) continue; if (diffTime < _speedLimitMs) { uint32 delayTime = _speedLimitMs - diffTime; g_system->delayMillis(delayTime); } } delete g_movie; setMode(NormalMode); g_movie = defaultPlayer; } Common::Error GrimEngine::loadGameState(int slot) { assert(slot >= 0); if (getGameType() == GType_MONKEY4) { if (getGamePlatform() == Common::kPlatformPS2) { _savegameFileName = Common::String::format("efmi%03d.ps2", slot); } else { _savegameFileName = Common::String::format("efmi%03d.gsv", slot); } } else { _savegameFileName = Common::String::format("grim%02d.gsv", slot); } _savegameLoadRequest = true; return Common::kNoError; } void GrimEngine::handlePause() { if (!LuaBase::instance()->callback("pauseHandler")) { error("handlePause: invalid handler"); } } void GrimEngine::handleExit() { if (!LuaBase::instance()->callback("exitHandler")) { error("handleExit: invalid handler"); } } void GrimEngine::handleUserPaint() { if (!LuaBase::instance()->callback("userPaintHandler")) { error("handleUserPaint: invalid handler"); } } void GrimEngine::cameraChangeHandle(int prev, int next) { LuaObjects objects; objects.add(prev); objects.add(next); LuaBase::instance()->callback("camChangeHandler", objects); } void GrimEngine::cameraPostChangeHandle(int num) { LuaObjects objects; objects.add(num); LuaBase::instance()->callback("postCamChangeHandler", objects); } void GrimEngine::savegameCallback() { if (!LuaBase::instance()->callback("saveGameCallback")) { error("GrimEngine::savegameCallback: invalid handler"); } } void GrimEngine::handleDebugLoadResource() { void *resource = nullptr; int c, i = 0; char buf[513]; // Tool for debugging the loading of a particular resource without // having to actually make it all the way to it in the game fprintf(stderr, "Enter resource to load (extension specifies type): "); while (i < 512 && (c = fgetc(stdin)) != EOF && c != '\n') buf[i++] = c; buf[i] = '\0'; if (strstr(buf, ".key")) resource = (void *)g_resourceloader->loadKeyframe(buf); else if (strstr(buf, ".zbm") || strstr(buf, ".bm")) resource = (void *)Bitmap::create(buf); else if (strstr(buf, ".cmp")) resource = (void *)g_resourceloader->loadColormap(buf); else if (strstr(buf, ".cos")) resource = (void *)g_resourceloader->loadCostume(buf, nullptr, nullptr); else if (strstr(buf, ".lip")) resource = (void *)g_resourceloader->loadLipSync(buf); else if (strstr(buf, ".snm")) resource = (void *)g_movie->play(buf, false, 0, 0); else if (strstr(buf, ".wav") || strstr(buf, ".imu")) { if (g_imuse) g_imuse->startSfx(buf); resource = (void *)1; } else if (strstr(buf, ".mat")) { CMap *cmap = g_resourceloader->loadColormap("item.cmp"); warning("Default colormap applied to resources loaded in this fashion"); // Default to repeating the texture as in GRIM resource = (void *)g_resourceloader->loadMaterial(buf, cmap, false); } else { warning("Resource type not understood"); } if (!resource) warning("Requested resouce (%s) not found", buf); } void GrimEngine::drawTextObjects() { foreach (TextObject *t, TextObject::getPool()) { t->draw(); } } void GrimEngine::playIrisAnimation(Iris::Direction dir, int x, int y, int time) { _iris->play(dir, x, y, time); } void GrimEngine::luaUpdate() { if (_savegameLoadRequest || _savegameSaveRequest || _changeHardwareState) return; // Update timing information unsigned newStart = g_system->getMillis(); if (newStart < _frameStart) { _frameStart = newStart; return; } _frameTime = newStart - _frameStart; _frameStart = newStart; if (_mode == PauseMode || _shortFrame) { _frameTime = 0; } LuaBase::instance()->update(_frameTime, _movieTime); if (_currSet && (_mode == NormalMode || _mode == SmushMode)) { // call updateTalk() before calling update(), since it may modify costumes state, and // the costumes are updated in update(). for (Common::List::iterator i = _talkingActors.begin(); i != _talkingActors.end(); ++i) { Actor *a = *i; if (!a->updateTalk(_frameTime)) { i = _talkingActors.reverse_erase(i); } } // Update the actors. Do it here so that we are sure to react asap to any change // in the actors state caused by lua. buildActiveActorsList(); foreach (Actor *a, _activeActors) { // Note that the actor need not be visible to update chores, for example: // when Manny has just brought Meche back he is offscreen several times // when he needs to perform certain chores a->update(_frameTime); } _iris->update(_frameTime); foreach (TextObject *t, TextObject::getPool()) { t->update(); } } } void GrimEngine::updateDisplayScene() { _doFlip = true; if (_mode == SmushMode) { if (g_movie->isPlaying()) { _movieTime = g_movie->getMovieTime(); if (g_movie->isUpdateNeeded()) { g_driver->prepareMovieFrame(g_movie->getDstSurface()); g_movie->clearUpdateNeeded(); } int frame = g_movie->getFrame(); if (frame >= 0) { if (frame != _prevSmushFrame) { _prevSmushFrame = g_movie->getFrame(); g_driver->drawMovieFrame(g_movie->getX(), g_movie->getY()); if (_showFps) g_driver->drawEmergString(550, 25, _fps, Color(255, 255, 255)); } else _doFlip = false; } else g_driver->releaseMovieFrame(); } // Draw Primitives _iris->draw(); g_movie->drawMovieSubtitle(); } else if (_mode == NormalMode || _mode == OverworldMode) { updateNormalMode(); } else if (_mode == DrawMode) { updateDrawMode(); } } void GrimEngine::updateNormalMode() { if (!_currSet || !_flipEnable) return; g_driver->clearScreen(); drawNormalMode(); _iris->draw(); drawTextObjects(); } void GrimEngine::updateDrawMode() { _doFlip = false; _prevSmushFrame = 0; _movieTime = 0; } void GrimEngine::drawNormalMode() { _prevSmushFrame = 0; _movieTime = 0; _currSet->drawBackground(); // Draw underlying scene components // Background objects are drawn underneath everything except the background // There are a bunch of these, especially in the tube-switcher room _currSet->drawBitmaps(ObjectState::OBJSTATE_BACKGROUND); // State objects are drawn on top of other things, such as the flag // on Manny's message tube _currSet->drawBitmaps(ObjectState::OBJSTATE_STATE); // Play SMUSH Animations // This should occur on top of all underlying scene objects, // a good example is the tube switcher room where some state objects // need to render underneath the animation or you can't see what's going on // This should not occur on top of everything though or Manny gets covered // up when he's next to Glottis's service room if (g_movie->isPlaying() && _movieSetup == _currSet->getCurrSetup()->_name) { _movieTime = g_movie->getMovieTime(); if (g_movie->isUpdateNeeded()) { g_driver->prepareMovieFrame(g_movie->getDstSurface()); g_movie->clearUpdateNeeded(); } if (g_movie->getFrame() >= 0) g_driver->drawMovieFrame(g_movie->getX(), g_movie->getY()); else g_driver->releaseMovieFrame(); } // Underlay objects must be drawn on top of movies // Otherwise the lighthouse door will always be open as the underlay for // the closed door will be overdrawn by a movie used as background image. _currSet->drawBitmaps(ObjectState::OBJSTATE_UNDERLAY); // Draw Primitives foreach (PrimitiveObject *p, PrimitiveObject::getPool()) { p->draw(); } _currSet->setupCamera(); g_driver->set3DMode(); if (_setupChanged) { cameraPostChangeHandle(_currSet->getSetup()); _setupChanged = false; setSideTextures(_currSet->getCurrSetup()->_name.c_str()); } // Draw actors buildActiveActorsList(); foreach (Actor *a, _activeActors) { if (a->isVisible()) a->draw(); } flagRefreshShadowMask(false); // Draw overlying scene components // The overlay objects should be drawn on top of everything else, // including 3D objects such as Manny and the message tube _currSet->drawBitmaps(ObjectState::OBJSTATE_OVERLAY); } void GrimEngine::doFlip() { _frameCounter++; if (!_doFlip) { return; } if (_showFps && _mode != DrawMode) g_driver->drawEmergString(550, 25, _fps, Color(255, 255, 255)); if (_flipEnable) g_driver->flipBuffer(); if (_showFps && _mode != DrawMode) { unsigned int currentTime = g_system->getMillis(); unsigned int delta = currentTime - _lastFrameTime; if (delta > 500) { snprintf(_fps, sizeof(_fps), "%7.2f", (double)(_frameCounter * 1000) / (double)delta); _frameCounter = 0; _lastFrameTime = currentTime; } } } void GrimEngine::mainLoop() { _movieTime = 0; _frameTime = 0; _frameStart = g_system->getMillis(); _frameCounter = 0; _lastFrameTime = 0; _prevSmushFrame = 0; _refreshShadowMask = false; _shortFrame = false; bool resetShortFrame = false; _changeHardwareState = false; _changeFullscreenState = false; _setupChanged = true; for (;;) { uint32 startTime = g_system->getMillis(); if (_shortFrame) { if (resetShortFrame) { _shortFrame = false; } resetShortFrame = !resetShortFrame; } if (shouldQuit()) return; if (_savegameLoadRequest) { savegameRestore(); } if (_savegameSaveRequest) { savegameSave(); } // If the backend can keep the OpenGL context when switching to fullscreen, // just toggle the fullscreen feature (SDL2 path). if (_changeFullscreenState && _system->hasFeature(OSystem::kFeatureFullscreenToggleKeepsContext)) { bool fullscreen = _system->getFeatureState(OSystem::kFeatureFullscreenMode); _system->setFeatureState(OSystem::kFeatureFullscreenMode, !fullscreen); _changeFullscreenState = false; } // If the backend destroys the OpenGL context or the user switched to a different // renderer, the GFX driver needs to be recreated (SDL1 path). if (_changeHardwareState || _changeFullscreenState) { _changeHardwareState = false; bool fullscreen = _system->getFeatureState(OSystem::kFeatureFullscreenMode); if (_changeFullscreenState) { fullscreen = !fullscreen; } uint screenWidth = g_driver->getScreenWidth(); uint screenHeight = g_driver->getScreenHeight(); EngineMode mode = getMode(); _savegameFileName = ""; savegameSave(); clearPools(); delete g_driver; g_driver = createRenderer(screenWidth, screenHeight, fullscreen); savegameRestore(); if (mode == DrawMode) { setMode(GrimEngine::NormalMode); updateDisplayScene(); g_driver->storeDisplay(); g_driver->dimScreen(); } setMode(mode); _changeFullscreenState = false; } g_sound->flushTracks(); if (g_imuse) { g_imuse->refreshScripts(); } // Process events Common::Event event; while (g_system->getEventManager()->pollEvent(event)) { // Handle any buttons, keys and joystick operations Common::EventType type = event.type; if (type == Common::EVENT_KEYDOWN || type == Common::EVENT_KEYUP) { if (type == Common::EVENT_KEYDOWN) { // Ignore everything but ESC when movies are playing // This matches the retail and demo versions of EMI // This also allows the PS2 version to skip movies if (_mode == SmushMode && g_grim->getGameType() == GType_MONKEY4) { if (event.kbd.keycode == Common::KEYCODE_ESCAPE) { g_movie->stop(); break; } continue; } if (_mode != DrawMode && _mode != SmushMode && (event.kbd.ascii == 'q')) { handleExit(); break; } else if (_mode != DrawMode && (event.kbd.keycode == Common::KEYCODE_PAUSE)) { handlePause(); break; } else { handleChars(type, event.kbd); } } handleControls(type, event.kbd); if (getGameType() != GType_MONKEY4) { // Allow lua to react to the event. // Without this lua_update switching the entries in the menu is slow because // if the button is not kept pressed the KEYUP will arrive just after the KEYDOWN // and it will break the lua scripts that checks for the state of the button // with GetControlState() // // This call seems to be only necessary to handle Grim's menu correctly. // In EMI it would have the side-effect that luaUpdate() is sometimes called // in the same millisecond which causes getPerSecond() to return 0 for // any given rate which is not compatible with e.g. actor walking. // We do not want the scripts to update while a movie is playing in the PS2-version. if (!(getGamePlatform() == Common::kPlatformPS2 && _mode == SmushMode)) { luaUpdate(); } } } if (type == Common::EVENT_JOYAXIS_MOTION) handleJoyAxis(event.joystick.axis, event.joystick.position); if (type == Common::EVENT_JOYBUTTON_DOWN || type == Common::EVENT_JOYBUTTON_UP) handleJoyButton(type, event.joystick.button); if (type == Common::EVENT_SCREEN_CHANGED) { handleUserPaint(); } } if (_mode != PauseMode) { // Draw the display scene before doing the luaUpdate. // This give a large performance boost as OpenGL stores commands // in a queue on the gpu to be rendered later. When doFlip is // called the cpu must wait for the gpu to finish its queue. // Now, it will queue all the OpenGL commands and draw them on the // GPU while the CPU is busy updating the game world. updateDisplayScene(); } if (_mode != PauseMode) { doFlip(); } // We do not want the scripts to update while a movie is playing in the PS2-version. if (!(getGamePlatform() == Common::kPlatformPS2 && _mode == SmushMode)) { luaUpdate(); } if (g_imuseState != -1) { g_sound->setMusicState(g_imuseState); g_imuseState = -1; } uint32 endTime = g_system->getMillis(); if (startTime > endTime) continue; uint32 diffTime = endTime - startTime; if (_speedLimitMs == 0) continue; if (diffTime < _speedLimitMs) { uint32 delayTime = _speedLimitMs - diffTime; g_system->delayMillis(delayTime); } } } void GrimEngine::changeHardwareState() { _changeHardwareState = true; } void GrimEngine::saveGame(const Common::String &file) { _savegameFileName = file; _savegameSaveRequest = true; } void GrimEngine::loadGame(const Common::String &file) { _savegameFileName = file; _savegameLoadRequest = true; } void GrimEngine::savegameRestore() { debug("GrimEngine::savegameRestore() started."); _savegameLoadRequest = false; Common::String filename; if (_savegameFileName.size() == 0) { filename = "grim.sav"; } else { filename = _savegameFileName; } _savedState = SaveGame::openForLoading(filename); if (!_savedState || !_savedState->isCompatible()) return; if (g_imuse) { g_imuse->stopAllSounds(); g_imuse->resetState(); } g_movie->stop(); if (g_imuse) g_imuse->pause(true); g_movie->pause(true); if (g_registry) g_registry->save(); _selectedActor = nullptr; delete _currSet; _currSet = nullptr; Bitmap::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Bitmaps restored successfully."); Font::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Fonts restored successfully."); ObjectState::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "ObjectStates restored successfully."); Set::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Sets restored successfully."); TextObject::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "TextObjects restored successfully."); PrimitiveObject::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "PrimitiveObjects restored successfully."); Actor::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Actors restored successfully."); if (getGameType() == GType_MONKEY4) { PoolSound::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Pool sounds saved successfully."); Layer::getPool().restoreObjects(_savedState); Debug::debug(Debug::Engine, "Layers restored successfully."); } restoreGRIM(); Debug::debug(Debug::Engine, "Engine restored successfully."); g_driver->restoreState(_savedState); Debug::debug(Debug::Engine, "Renderer restored successfully."); g_sound->restoreState(_savedState); Debug::debug(Debug::Engine, "iMuse restored successfully."); g_movie->restoreState(_savedState); Debug::debug(Debug::Engine, "Movie restored successfully."); _iris->restoreState(_savedState); Debug::debug(Debug::Engine, "Iris restored successfully."); lua_Restore(_savedState); Debug::debug(Debug::Engine, "Lua restored successfully."); delete _savedState; //Re-read the values, since we may have been in some state that changed them when loading the savegame, //e.g. running a cutscene, which sets the sfx volume to 0. _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, ConfMan.getInt("sfx_volume")); _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, ConfMan.getInt("speech_volume")); _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, ConfMan.getInt("music_volume")); LuaBase::instance()->postRestoreHandle(); if (g_imuse) g_imuse->pause(false); g_movie->pause(false); debug("GrimEngine::savegameRestore() finished."); _shortFrame = true; clearEventQueue(); invalidateActiveActorsList(); buildActiveActorsList(); _currSet->setupCamera(); g_driver->set3DMode(); } void GrimEngine::restoreGRIM() { _savedState->beginSection('GRIM'); _mode = (EngineMode)_savedState->readLEUint32(); _previousMode = (EngineMode)_savedState->readLEUint32(); // Actor stuff int32 id = _savedState->readLESint32(); if (id != 0) { _selectedActor = Actor::getPool().getObject(id); } //TextObject stuff _sayLineDefaults.setFGColor(_savedState->readColor()); _sayLineDefaults.setFont(Font::getPool().getObject(_savedState->readLESint32())); _sayLineDefaults.setHeight(_savedState->readLESint32()); _sayLineDefaults.setJustify(_savedState->readLESint32()); _sayLineDefaults.setWidth(_savedState->readLESint32()); _sayLineDefaults.setX(_savedState->readLESint32()); _sayLineDefaults.setY(_savedState->readLESint32()); _sayLineDefaults.setDuration(_savedState->readLESint32()); if (_savedState->saveMinorVersion() > 5) { _movieSubtitle = TextObject::getPool().getObject(_savedState->readLESint32()); } // Set stuff _currSet = Set::getPool().getObject(_savedState->readLESint32()); if (_savedState->saveMinorVersion() > 4) { _movieSetup = _savedState->readString(); } else { _movieSetup = _currSet->getCurrSetup()->_name; } _savedState->endSection(); } void GrimEngine::storeSaveGameImage(SaveGame *state) { const Graphics::PixelFormat image_format = Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0); int width = 250, height = 188; Bitmap *screenshot; debug("GrimEngine::StoreSaveGameImage() started."); screenshot = g_driver->getScreenshot(width, height, true); state->beginSection('SIMG'); if (screenshot) { int size = screenshot->getWidth() * screenshot->getHeight(); screenshot->setActiveImage(0); screenshot->getBitmapData()->convertToColorFormat(image_format); uint16 *data = (uint16 *)screenshot->getData().getRawBuffer(); for (int l = 0; l < size; l++) { state->writeLEUint16(data[l]); } } else { error("Unable to store screenshot"); } state->endSection(); delete screenshot; debug("GrimEngine::StoreSaveGameImage() finished."); } void GrimEngine::savegameSave() { debug("GrimEngine::savegameSave() started."); _savegameSaveRequest = false; Common::String filename; if (_savegameFileName.size() == 0) { filename = "grim.sav"; } else { filename = _savegameFileName; } if (getGameType() == GType_MONKEY4 && filename.contains('/')) { filename = Common::lastPathComponent(filename, '/'); } _savedState = SaveGame::openForSaving(filename); if (!_savedState) { //TODO: Translate this! GUI::displayErrorDialog(_("Error: the game could not be saved.")); return; } storeSaveGameImage(_savedState); if (g_imuse) g_imuse->pause(true); g_movie->pause(true); savegameCallback(); Bitmap::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Bitmaps saved successfully."); Font::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Fonts saved successfully."); ObjectState::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "ObjectStates saved successfully."); Set::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Sets saved successfully."); TextObject::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "TextObjects saved successfully."); PrimitiveObject::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "PrimitiveObjects saved successfully."); Actor::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Actors saved successfully."); if (getGameType() == GType_MONKEY4) { PoolSound::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Pool sounds saved successfully."); Layer::getPool().saveObjects(_savedState); Debug::debug(Debug::Engine, "Layers saved successfully."); } saveGRIM(); Debug::debug(Debug::Engine, "Engine saved successfully."); g_driver->saveState(_savedState); Debug::debug(Debug::Engine, "Renderer saved successfully."); g_sound->saveState(_savedState); Debug::debug(Debug::Engine, "iMuse saved successfully."); g_movie->saveState(_savedState); Debug::debug(Debug::Engine, "Movie saved successfully."); _iris->saveState(_savedState); Debug::debug(Debug::Engine, "Iris saved successfully."); lua_Save(_savedState); delete _savedState; if (g_imuse) g_imuse->pause(false); g_movie->pause(false); debug("GrimEngine::savegameSave() finished."); _shortFrame = true; clearEventQueue(); } void GrimEngine::saveGRIM() { _savedState->beginSection('GRIM'); _savedState->writeLEUint32((uint32)_mode); _savedState->writeLEUint32((uint32)_previousMode); //Actor stuff if (_selectedActor) { _savedState->writeLESint32(_selectedActor->getId()); } else { _savedState->writeLESint32(0); } //TextObject stuff _savedState->writeColor(_sayLineDefaults.getFGColor()); _savedState->writeLESint32(_sayLineDefaults.getFont()->getId()); _savedState->writeLESint32(_sayLineDefaults.getHeight()); _savedState->writeLESint32(_sayLineDefaults.getJustify()); _savedState->writeLESint32(_sayLineDefaults.getWidth()); _savedState->writeLESint32(_sayLineDefaults.getX()); _savedState->writeLESint32(_sayLineDefaults.getY()); _savedState->writeLESint32(_sayLineDefaults.getDuration()); _savedState->writeLESint32(_movieSubtitle ? _movieSubtitle->getId() : 0); //Set stuff _savedState->writeLESint32(_currSet->getId()); _savedState->writeString(_movieSetup); _savedState->endSection(); } Set *GrimEngine::findSet(const Common::String &name) { // Find scene object foreach (Set *s, Set::getPool()) { if (s->getName() == name) return s; } return nullptr; } void GrimEngine::setSetLock(const char *name, bool lockStatus) { Set *scene = findSet(name); if (!scene) { Debug::warning(Debug::Engine, "Set object '%s' not found in list", name); return; } // Change the locking status scene->_locked = lockStatus; } Set *GrimEngine::loadSet(const Common::String &name) { Set *s = findSet(name); if (!s) { Common::String filename(name); // EMI-scripts refer to their .setb files as .set if (g_grim->getGameType() == GType_MONKEY4) { filename += "b"; } Common::SeekableReadStream *stream; stream = g_resourceloader->openNewStreamFile(filename.c_str()); if (!stream) error("Could not find scene file %s", name.c_str()); s = new Set(name, stream); delete stream; } return s; } void GrimEngine::setSet(const char *name) { setSet(loadSet(name)); } void GrimEngine::setSet(Set *scene) { if (scene == _currSet) return; if (getGameType() == GType_MONKEY4) { foreach (PoolSound *s, PoolSound::getPool()) { s->stop(); } } // Stop the actors. This fixes bug #289 (https://github.com/residualvm/residualvm/issues/289) // and it makes sense too, since when changing set the directions // and coords change too. foreach (Actor *a, Actor::getPool()) { a->stopWalking(); } Set *lastSet = _currSet; _currSet = scene; _currSet->setSoundParameters(20, 127); // should delete the old scene after setting the new one if (lastSet && !lastSet->_locked) { delete lastSet; } _shortFrame = true; _setupChanged = true; invalidateActiveActorsList(); } void GrimEngine::makeCurrentSetup(int num) { int prevSetup = g_grim->getCurrSet()->getSetup(); if (prevSetup != num) { getCurrSet()->setSetup(num); getCurrSet()->setSoundParameters(20, 127); cameraChangeHandle(prevSetup, num); // here should be set sound position _setupChanged = true; } } void GrimEngine::setTextSpeed(int speed) { if (speed < 1) _textSpeed = 1; if (speed > 10) _textSpeed = 10; _textSpeed = speed; } float GrimEngine::getControlAxis(int num) { int idx = num - KEYCODE_AXIS_JOY1_X; if (idx >= 0 && idx < NUM_JOY_AXES) { return _joyAxisPosition[idx]; } return 0; } bool GrimEngine::getControlState(int num) { return _controlsState[num]; } float GrimEngine::getPerSecond(float rate) const { return rate * _frameTime / 1000; } void GrimEngine::invalidateActiveActorsList() { _buildActiveActorsList = true; } void GrimEngine::immediatelyRemoveActor(Actor *actor) { _activeActors.remove(actor); _talkingActors.remove(actor); } void GrimEngine::buildActiveActorsList() { if (!_buildActiveActorsList) { return; } _activeActors.clear(); foreach (Actor *a, Actor::getPool()) { if (((_mode == NormalMode || _mode == DrawMode) && a->isDrawableInSet(_currSet->getName())) || a->isInOverworld()) { _activeActors.push_back(a); } } _buildActiveActorsList = false; } void GrimEngine::addTalkingActor(Actor *a) { _talkingActors.push_back(a); } bool GrimEngine::areActorsTalking() const { //This takes into account that there may be actors which are still talking, but in the background. bool talking = false; foreach (Actor *a, _talkingActors) { if (a->isTalkingForeground()) { talking = true; break; } } return talking; } void GrimEngine::setMovieSubtitle(TextObject *to) { if (_movieSubtitle != to) { delete _movieSubtitle; _movieSubtitle = to; } } void GrimEngine::drawMovieSubtitle() { if (_movieSubtitle) _movieSubtitle->draw(); } void GrimEngine::setMovieSetup() { _movieSetup = _currSet->getCurrSetup()->_name; } void GrimEngine::setMode(EngineMode mode) { _mode = mode; invalidateActiveActorsList(); } void GrimEngine::clearEventQueue() { Common::Event event; while (g_system->getEventManager()->pollEvent(event)) { } for (int i = 0; i < KEYCODE_EXTRA_LAST; ++i) { _controlsState[i] = false; } } bool GrimEngine::hasFeature(EngineFeature f) const { return (f == kSupportsReturnToLauncher) || (f == kSupportsLoadingDuringRuntime); } void GrimEngine::pauseEngineIntern(bool pause) { if (g_imuse) g_imuse->pause(pause); if (g_movie) g_movie->pause(pause); if (pause) { _pauseStartTime = _system->getMillis(); } else { _frameStart += _system->getMillis() - _pauseStartTime; } } Graphics::Surface *loadPNG(const Common::String &filename) { Image::PNGDecoder d; Common::SeekableReadStream *s = SearchMan.createReadStreamForMember(filename); if (!s) return nullptr; d.loadStream(*s); delete s; Graphics::Surface *srf = d.getSurface()->convertTo(Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)); return srf; } void GrimEngine::setSideTextures(const Common::String &setup) { if (! g_system->hasFeature(OSystem::kFeatureSideTextures)) return; Graphics::Surface *t1 = loadPNG(Common::String::format("%s_left.png", setup.c_str())); Graphics::Surface *t2 = loadPNG(Common::String::format("%s_right.png", setup.c_str())); g_system->suggestSideTextures(t1, t2); if (t1) t1->free(); if (t2) t2->free(); delete t1; delete t2; } void GrimEngine::debugLua(const Common::String &str) { lua_dostring(str.c_str()); } } // end of namespace Grim