/* 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 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. * */ // // Savegame support by Vasyl Tsvirkunov // Multi-slots by Claudio Matsuoka // #include "common/file.h" #include "common/config-manager.h" #include "common/savefile.h" #include "common/textconsole.h" #include "common/translation.h" #include "gui/saveload.h" #include "graphics/thumbnail.h" #include "graphics/surface.h" #include "agi/agi.h" #include "agi/graphics.h" #include "agi/text.h" #include "agi/sprite.h" #include "agi/keyboard.h" #include "agi/menu.h" #include "agi/systemui.h" #include "agi/words.h" #define SAVEGAME_CURRENT_VERSION 11 // // Version 0 (Sarien): view table has 64 entries // Version 1 (Sarien): view table has 256 entries (needed in KQ3) // Version 2 (ScummVM): first ScummVM version // Version 3 (ScummVM): added AGIPAL save/load support // Version 4 (ScummVM): added thumbnails and save creation date/time // Version 5 (ScummVM): Added game md5 // Version 6 (ScummVM): Added game played time // Version 7 (ScummVM): Added controller key mappings // required for some games for quick-loading from ScummVM main menu // for games, that do not set all key mappings right at the start // Added automatic save data (for command SetSimple) // Version 8 (ScummVM): Added Hold-Key-Mode boolean // required for at least Mixed Up Mother Goose // gets set at the start of the game only // Version 9 (ScummVM): Added seconds to saved game time stamp // Version 10 (ScummVM): Added priorityTableSet boolean namespace Agi { static const uint32 AGIflag = MKTAG('A', 'G', 'I', ':'); int AgiEngine::saveGame(const Common::String &fileName, const Common::String &descriptionString) { char gameIDstring[8] = "gameIDX"; int i; Common::OutSaveFile *out; int result = errOK; debugC(3, kDebugLevelMain | kDebugLevelSavegame, "AgiEngine::saveGame(%s, %s)", fileName.c_str(), descriptionString.c_str()); if (!(out = _saveFileMan->openForSaving(fileName))) { warning("Can't create file '%s', game not saved", fileName.c_str()); return errBadFileOpen; } else { debugC(3, kDebugLevelMain | kDebugLevelSavegame, "Successfully opened %s for writing", fileName.c_str()); } out->writeUint32BE(AGIflag); // Write description of saved game, limited to SAVEDGAME_DESCRIPTION_LEN characters + terminating NUL char description[SAVEDGAME_DESCRIPTION_LEN + 1]; memset(description, 0, sizeof(description)); strncpy(description, descriptionString.c_str(), SAVEDGAME_DESCRIPTION_LEN); assert(SAVEDGAME_DESCRIPTION_LEN + 1 == 31); // safety out->write(description, 31); out->writeByte(SAVEGAME_CURRENT_VERSION); debugC(5, kDebugLevelMain | kDebugLevelSavegame, "Writing save game version (%d)", SAVEGAME_CURRENT_VERSION); // Thumbnail Graphics::saveThumbnail(*out); // Creation date/time TimeDate curTime; _system->getTimeAndDate(curTime); uint32 saveDate = ((curTime.tm_mday & 0xFF) << 24) | (((curTime.tm_mon + 1) & 0xFF) << 16) | ((curTime.tm_year + 1900) & 0xFFFF); uint16 saveTime = ((curTime.tm_hour & 0xFF) << 8) | ((curTime.tm_min) & 0xFF); uint32 playTime = g_engine->getTotalPlayTime() / 1000; out->writeUint32BE(saveDate); debugC(5, kDebugLevelMain | kDebugLevelSavegame, "Writing save date (%d)", saveDate); out->writeUint16BE(saveTime); debugC(5, kDebugLevelMain | kDebugLevelSavegame, "Writing save time (%d)", saveTime); // Version 9+: save seconds of current time as well out->writeByte(curTime.tm_sec & 0xFF); out->writeUint32BE(playTime); debugC(5, kDebugLevelMain | kDebugLevelSavegame, "Writing play time (%d)", playTime); out->writeByte(2); // was _game.state, 2 = STATE_RUNNING Common::strlcpy(gameIDstring, _game.id, 8); out->write(gameIDstring, 8); debugC(5, kDebugLevelMain | kDebugLevelSavegame, "Writing game id (%s, %s)", gameIDstring, _game.id); const char *tmp = getGameMD5(); // As reported in bug report #2849084 "AGI: Crash when saving fallback-matched game" // getGameMD5 will return NULL for fallback matched games. Since there is also no // filename available we can not compute any MD5 here either. Thus we will just set // the MD5 sum in the savegame to all zero, when getGameMD5 returns NULL. if (!tmp) { for (i = 0; i < 32; ++i) out->writeByte(0); } else { for (i = 0; i < 32; ++i) out->writeByte(tmp[i]); } // Version 7+: Save automatic saving state (set.simple opcode) out->writeByte(_game.automaticSave); out->write(_game.automaticSaveDescription, 31); // touch VM_VAR_SECONDS, so that it gets updated getVar(VM_VAR_SECONDS); for (i = 0; i < MAX_FLAGS; i++) out->writeByte(_game.flags[i]); for (i = 0; i < MAX_VARS; i++) out->writeByte(_game.vars[i]); out->writeSint16BE((int8)_game.horizon); out->writeSint16BE((int16)_text->statusRow_Get()); out->writeSint16BE((int16)_text->promptRow_Get()); out->writeSint16BE((int16)_text->getWindowRowMin()); out->writeSint16BE(1); // was _game.inputMode, we set it to 1, which was INPUTMODE_NORMAL out->writeSint16BE((int16)_game.curLogicNr); out->writeSint16BE((int16)_game.playerControl); out->writeSint16BE((int16)shouldQuit()); if (_text->statusEnabled()) { out->writeSint16BE(0x7FFF); } else { out->writeSint16BE(0); } out->writeSint16BE(1); // was clock enabled // (previous in-game-timer, in-game-timer is always enabled during the regular game, so need to save/load it) out->writeSint16BE((int16)_game.exitAllLogics); out->writeSint16BE((int16)_game.pictureShown); out->writeSint16BE((int16)_text->promptIsEnabled()); // was "_game.hasPrompt", no longer needed out->writeSint16BE((int16)_game.gameFlags); if (_text->promptIsEnabled()) { out->writeSint16BE(0x7FFF); } else { out->writeSint16BE(0); } for (i = 0; i < SCRIPT_HEIGHT; i++) out->writeByte(_gfx->saveLoadGetPriority(i)); // Version 10+: Save, if priority table got modified (set.pri.base opcode) out->writeSint16BE((int16)_gfx->saveLoadWasPriorityTableModified()); out->writeSint16BE((int16)_game.gfxMode); out->writeByte(_text->inputGetCursorChar()); out->writeSint16BE((int16)_text->charAttrib_GetForeground()); out->writeSint16BE((int16)_text->charAttrib_GetBackground()); // game.hires // game.sbuf // game.ego_words // game.num_ego_words out->writeSint16BE((int16)_game.numObjects); for (i = 0; i < (int16)_game.numObjects; i++) out->writeSint16BE((int16)objectGetLocation(i)); // Version 7+: save controller key mappings // required for games, that do not set all key mappings right at the start // when quick restoring is used from ScummVM menu, only 1 cycle is executed for (i = 0; i < MAX_CONTROLLER_KEYMAPPINGS; i++) { out->writeUint16BE(_game.controllerKeyMapping[i].keycode); out->writeByte(_game.controllerKeyMapping[i].controllerSlot); } // Version 8+: hold-key-mode // required for at least Mixed Up Mother Goose out->writeByte(_keyHoldMode); // game.ev_keyp for (i = 0; i < MAX_STRINGS; i++) out->write(_game.strings[i], MAX_STRINGLEN); // record info about loaded resources for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) { out->writeByte(_game.dirLogic[i].flags); out->writeSint16BE((int16)_game.logics[i].sIP); out->writeSint16BE((int16)_game.logics[i].cIP); } for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) out->writeByte(_game.dirPic[i].flags); for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) out->writeByte(_game.dirView[i].flags); for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) out->writeByte(_game.dirSound[i].flags); // game.pictures // game.logics // game.views // game.sounds for (i = 0; i < SCREENOBJECTS_MAX; i++) { ScreenObjEntry *screenObj = &_game.screenObjTable[i]; out->writeByte(screenObj->stepTime); out->writeByte(screenObj->stepTimeCount); out->writeByte(screenObj->objectNr); out->writeSint16BE(screenObj->xPos); out->writeSint16BE(screenObj->yPos); out->writeByte(screenObj->currentViewNr); // v->view_data out->writeByte(screenObj->currentLoopNr); out->writeByte(screenObj->loopCount); // v->loop_data out->writeByte(screenObj->currentCelNr); out->writeByte(screenObj->celCount); // v->cel_data // v->cel_data_2 out->writeSint16BE(screenObj->xPos_prev); out->writeSint16BE(screenObj->yPos_prev); // v->s out->writeSint16BE(screenObj->xSize); out->writeSint16BE(screenObj->ySize); out->writeByte(screenObj->stepSize); out->writeByte(screenObj->cycleTime); out->writeByte(screenObj->cycleTimeCount); out->writeByte(screenObj->direction); out->writeByte(screenObj->motionType); out->writeByte(screenObj->cycle); // Version 11+: loop_flag, was saved previously under vt.parm1 out->writeByte(screenObj->loop_flag); out->writeByte(screenObj->priority); out->writeUint16BE(screenObj->flags); // this was done so that saved games compatibility isn't broken switch (screenObj->motionType) { case kMotionNormal: out->writeByte(0); out->writeByte(0); out->writeByte(0); out->writeByte(0); break; case kMotionWander: out->writeByte(screenObj->wander_count); out->writeByte(0); out->writeByte(0); out->writeByte(0); break; case kMotionFollowEgo: out->writeByte(screenObj->follow_stepSize); out->writeByte(screenObj->follow_flag); out->writeByte(screenObj->follow_count); out->writeByte(0); break; case kMotionEgo: case kMotionMoveObj: out->writeByte((byte)screenObj->move_x); // problematic! int16 -> byte out->writeByte((byte)screenObj->move_y); out->writeByte(screenObj->move_stepSize); out->writeByte(screenObj->move_flag); break; default: error("unknown motion-type"); } } // Save image stack for (Common::Stack::size_type j = 0; j < _imageStack.size(); ++j) { const ImageStackElement &ise = _imageStack[j]; out->writeByte(ise.type); out->writeSint16BE(ise.parm1); out->writeSint16BE(ise.parm2); out->writeSint16BE(ise.parm3); out->writeSint16BE(ise.parm4); out->writeSint16BE(ise.parm5); out->writeSint16BE(ise.parm6); out->writeSint16BE(ise.parm7); } out->writeByte(0); //Write which file number AGIPAL is using (0 if not being used) out->writeSint16BE(_gfx->getAGIPalFileNum()); out->finalize(); if (out->err()) { warning("Can't write file '%s'. (Disk full?)", fileName.c_str()); result = errIOError; } else debugC(1, kDebugLevelMain | kDebugLevelSavegame, "Saved game %s in file %s", descriptionString.c_str(), fileName.c_str()); delete out; debugC(3, kDebugLevelMain | kDebugLevelSavegame, "Closed %s", fileName.c_str()); _lastSaveTime = _system->getMillis(); return result; } int AgiEngine::loadGame(const Common::String &fileName, bool checkId) { char description[SAVEDGAME_DESCRIPTION_LEN + 1]; byte saveVersion = 0; char loadId[8]; int i, vtEntries = SCREENOBJECTS_MAX; uint8 t; int16 parm[7]; Common::InSaveFile *in; bool totalPlayTimeWasSet = false; byte oldLoopFlag = 0; debugC(3, kDebugLevelMain | kDebugLevelSavegame, "AgiEngine::loadGame(%s)", fileName.c_str()); if (!(in = _saveFileMan->openForLoading(fileName))) { warning("Can't open file '%s', game not loaded", fileName.c_str()); return errBadFileOpen; } else { debugC(3, kDebugLevelMain | kDebugLevelSavegame, "Successfully opened %s for reading", fileName.c_str()); } uint32 typea = in->readUint32BE(); if (typea == AGIflag) { debugC(6, kDebugLevelMain | kDebugLevelSavegame, "Has AGI flag, good start"); } else { warning("This doesn't appear to be an AGI savegame, game not restored"); delete in; return errOK; } assert(SAVEDGAME_DESCRIPTION_LEN + 1 == 31); // safety in->read(description, 31); // skip description // check, if there is a terminating NUL inside description uint16 descriptionPos = 0; while (description[descriptionPos]) { descriptionPos++; if (descriptionPos >= sizeof(description)) error("saved game description is corrupt"); } debugC(6, kDebugLevelMain | kDebugLevelSavegame, "Description is: %s", description); saveVersion = in->readByte(); if (saveVersion < 2) // is the save game pre-ScummVM? warning("Old save game version (%d, current version is %d). Will try and read anyway, but don't be surprised if bad things happen", saveVersion, SAVEGAME_CURRENT_VERSION); if (saveVersion < 3) warning("This save game contains no AGIPAL data, if the game is using the AGIPAL hack, it won't work correctly"); if (saveVersion > SAVEGAME_CURRENT_VERSION) error("Saved game was created with a newer version of ScummVM. Unable to load."); if (saveVersion >= 4) { // We don't need the thumbnail here, so just read it and discard it Graphics::skipThumbnail(*in); in->readUint32BE(); // save date in->readUint16BE(); // save time (hour + minute) if (saveVersion >= 9) { in->readByte(); // save time seconds } if (saveVersion >= 6) { uint32 playTime = in->readUint32BE(); inGameTimerReset(playTime * 1000); totalPlayTimeWasSet = true; } } in->readByte(); // was _game.state, not needed anymore in->read(loadId, 8); if (strcmp(loadId, _game.id) != 0 && checkId) { delete in; warning("This save seems to be from a different AGI game (save from %s, running %s), not loaded", loadId, _game.id); return errBadFileOpen; } Common::strlcpy(_game.id, loadId, 8); if (saveVersion >= 5) { char md5[32 + 1]; for (i = 0; i < 32; i++) { md5[i] = in->readByte(); } md5[i] = 0; // terminate // As noted above in AgiEngine::saveGame the MD5 sum field may be all zero // when the save was made via a fallback matched game. In this case we will // replace the MD5 sum with a nicer string, so that the user can easily see // this fact in the debug output. The string saved in "md5" will never match // any valid MD5 sum, thus it is safe to do that here. if (md5[0] == 0) strcpy(md5, "fallback matched"); debug(0, "Saved game MD5: \"%s\"", md5); if (!getGameMD5()) { warning("Since your game was only detected via the fallback detector, there is no possibility to assure the save is compatible with your game version"); debug(0, "The game used for saving is \"%s\".", md5); } else if (strcmp(md5, getGameMD5()) != 0) { warning("Game was saved with different gamedata - you may encounter problems"); debug(0, "Your game is \"%s\" and save is \"%s\".", getGameMD5(), md5); } } if (saveVersion >= 7) { // Restore automatic saving state (set.simple opcode) _game.automaticSave = in->readByte(); in->read(_game.automaticSaveDescription, 31); } else { _game.automaticSave = false; _game.automaticSaveDescription[0] = 0; } for (i = 0; i < MAX_FLAGS; i++) _game.flags[i] = in->readByte(); for (i = 0; i < MAX_VARS; i++) _game.vars[i] = in->readByte(); if (!totalPlayTimeWasSet) { // If we haven't gotten total play time by now, try to calculate it by using VM Variables // This will happen for at least saves before version 6 // Direct access because otherwise we would trigger an update to these variables according to ScummVM total play time byte playTimeSeconds = _game.vars[VM_VAR_SECONDS]; byte playTimeMinutes = _game.vars[VM_VAR_MINUTES]; byte playTimeHours = _game.vars[VM_VAR_HOURS]; byte playTimeDays = _game.vars[VM_VAR_DAYS]; uint32 playTime = (playTimeSeconds + (playTimeMinutes * 60) + (playTimeHours * 3600) + (playTimeDays * 86400)) * 1000; inGameTimerReset(playTime); } setVar(VM_VAR_FREE_PAGES, 180); // Set amount of free memory to realistic value (Overwriting the just loaded value) _game.horizon = in->readSint16BE(); _text->statusRow_Set(in->readSint16BE()); _text->promptRow_Set(in->readSint16BE()); _text->configureScreen(in->readSint16BE()); // These are never saved _text->promptReset(); in->readSint16BE(); // was _game.inputMode, not needed anymore _game.curLogicNr = in->readSint16BE(); _game.playerControl = in->readSint16BE(); if (in->readSint16BE()) quitGame(); if (in->readSint16BE()) { _text->statusEnable(); } else { _text->statusDisable(); } in->readSint16BE(); // was clock enabled, no longer needed _game.exitAllLogics = in->readSint16BE(); in->readSint16BE(); // was _game.pictureShown in->readSint16BE(); // was _game.hasPrompt, no longer needed _game.gameFlags = in->readSint16BE(); if (in->readSint16BE()) { _text->promptEnable(); } else { _text->promptDisable(); } for (i = 0; i < SCRIPT_HEIGHT; i++) _gfx->saveLoadSetPriority(i, in->readByte()); if (saveVersion >= 10) { // Version 10+: priority table was modified by scripts int16 priorityTableWasModified = in->readSint16BE(); if (priorityTableWasModified) { _gfx->saveLoadSetPriorityTableModifiedBool(true); } else { _gfx->saveLoadSetPriorityTableModifiedBool(false); } } else { // Try to figure it out by ourselves _gfx->saveLoadFigureOutPriorityTableModifiedBool(); } _text->closeWindow(); _game.block.active = false; _game.gfxMode = in->readSint16BE(); _text->inputSetCursorChar(in->readByte()); int16 textForeground = in->readSint16BE(); int16 textBackground = in->readSint16BE(); _text->charAttrib_Set(textForeground, textBackground); // game.ego_words - fixed by clean_input // game.num_ego_words - fixed by clean_input _game.numObjects = in->readSint16BE(); for (i = 0; i < (int16)_game.numObjects; i++) objectSetLocation(i, in->readSint16BE()); // Those are not serialized for (i = 0; i < MAX_CONTROLLERS; i++) { _game.controllerOccured[i] = false; } if (saveVersion >= 7) { // For old saves, we just keep the current controllers for (i = 0; i < MAX_CONTROLLER_KEYMAPPINGS; i++) { _game.controllerKeyMapping[i].keycode = in->readUint16BE(); _game.controllerKeyMapping[i].controllerSlot = in->readByte(); } } if (saveVersion >= 8) { // Version 8+: hold-key-mode if (in->readByte()) { _keyHoldMode = true; } else { _keyHoldMode = false; } } for (i = 0; i < MAX_STRINGS; i++) in->read(_game.strings[i], MAX_STRINGLEN); for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) { if (in->readByte() & RES_LOADED) agiLoadResource(RESOURCETYPE_LOGIC, i); else agiUnloadResource(RESOURCETYPE_LOGIC, i); _game.logics[i].sIP = in->readSint16BE(); _game.logics[i].cIP = in->readSint16BE(); } for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) { if (in->readByte() & RES_LOADED) agiLoadResource(RESOURCETYPE_PICTURE, i); else agiUnloadResource(RESOURCETYPE_PICTURE, i); } for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) { if (in->readByte() & RES_LOADED) agiLoadResource(RESOURCETYPE_VIEW, i); else agiUnloadResource(RESOURCETYPE_VIEW, i); } for (i = 0; i < MAX_DIRECTORY_ENTRIES; i++) { if (in->readByte() & RES_LOADED) agiLoadResource(RESOURCETYPE_SOUND, i); else agiUnloadResource(RESOURCETYPE_SOUND, i); } // game.pictures - loaded above // game.logics - loaded above // game.views - loaded above // game.sounds - loaded above for (i = 0; i < vtEntries; i++) { ScreenObjEntry *screenObj = &_game.screenObjTable[i]; screenObj->stepTime = in->readByte(); screenObj->stepTimeCount = in->readByte(); screenObj->objectNr = in->readByte(); screenObj->xPos = in->readSint16BE(); screenObj->yPos = in->readSint16BE(); screenObj->currentViewNr = in->readByte(); // screenObj->view_data - fixed below screenObj->currentLoopNr = in->readByte(); screenObj->loopCount = in->readByte(); // screenObj->loop_data - fixed below screenObj->currentCelNr = in->readByte(); screenObj->celCount = in->readByte(); // screenObj->cel_data - fixed below // screenObj->cel_data_2 - fixed below screenObj->xPos_prev = in->readSint16BE(); screenObj->yPos_prev = in->readSint16BE(); // screenObj->s - fixed below screenObj->xSize = in->readSint16BE(); screenObj->ySize = in->readSint16BE(); screenObj->stepSize = in->readByte(); screenObj->cycleTime = in->readByte(); screenObj->cycleTimeCount = in->readByte(); screenObj->direction = in->readByte(); screenObj->motionType = (MotionType)in->readByte(); screenObj->cycle = (CycleType)in->readByte(); if (saveVersion >= 11) { // Version 11+: loop_flag, was previously vt.parm1 screenObj->loop_flag = in->readByte(); } screenObj->priority = in->readByte(); screenObj->flags = in->readUint16BE(); // this was done so that saved games compatibility isn't broken switch (screenObj->motionType) { case kMotionNormal: oldLoopFlag = in->readByte(); in->readByte(); in->readByte(); in->readByte(); break; case kMotionWander: screenObj->wander_count = in->readByte(); in->readByte(); in->readByte(); in->readByte(); oldLoopFlag = screenObj->wander_count; break; case kMotionFollowEgo: screenObj->follow_stepSize = in->readByte(); screenObj->follow_flag = in->readByte(); screenObj->follow_count = in->readByte(); in->readByte(); oldLoopFlag = screenObj->follow_stepSize; break; case kMotionEgo: case kMotionMoveObj: screenObj->move_x = in->readByte(); // problematic! int16 -> byte screenObj->move_y = in->readByte(); screenObj->move_stepSize = in->readByte(); screenObj->move_flag = in->readByte(); oldLoopFlag = screenObj->move_x; break; default: error("unknown motion-type"); } if (saveVersion < 11) { if (saveVersion < 7) { // Recreate loop_flag from motion-type (was previously vt.parm1) // vt.parm1 was shared for multiple uses screenObj->loop_flag = oldLoopFlag; } else { // for Version 7-10 we can't really do anything, it was not saved screenObj->loop_flag = 0; // set it to 0 } } } // Fix some pointers in screenObjTable for (i = 0; i < SCREENOBJECTS_MAX; i++) { ScreenObjEntry *screenObj = &_game.screenObjTable[i]; if (_game.dirView[screenObj->currentViewNr].offset == _EMPTY) continue; if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) agiLoadResource(RESOURCETYPE_VIEW, screenObj->currentViewNr); setView(screenObj, screenObj->currentViewNr); // Fix v->view_data setLoop(screenObj, screenObj->currentLoopNr); // Fix v->loop_data setCel(screenObj, screenObj->currentCelNr); // Fix v->cel_data } _sprites->eraseSprites(); _game.pictureShown = false; _gfx->clearDisplay(0, false); // clear display screen, but not copy it to actual screen for now b/c transition // Recreate background from saved image stack clearImageStack(); while ((t = in->readByte()) != 0) { for (i = 0; i < 7; i++) parm[i] = in->readSint16BE(); replayImageStackCall(t, parm[0], parm[1], parm[2], parm[3], parm[4], parm[5], parm[6]); } // Load AGIPAL Data if (saveVersion >= 3) _gfx->setAGIPal(in->readSint16BE()); delete in; debugC(3, kDebugLevelMain | kDebugLevelSavegame, "Closed %s", fileName.c_str()); setFlag(VM_FLAG_RESTORE_JUST_RAN, true); _words->clearEgoWords(); // don't delay anything right after restoring a game artificialDelay_Reset(); _sprites->eraseSprites(); _sprites->buildAllSpriteLists(); _sprites->drawAllSpriteLists(); _picture->showPicWithTransition(); _game.pictureShown = true; _text->statusDraw(); _text->promptRedraw(); // copy everything over (we should probably only copy over the remaining parts of the screen w/o play screen _gfx->copyDisplayToScreen(); // Sync volume settings from ScummVM system settings, so that VM volume variable is overwritten setVolumeViaSystemSetting(); return errOK; } int AgiEngine::scummVMSaveLoadDialog(bool isSave) { GUI::SaveLoadChooser *dialog; Common::String desc; int slot; if (isSave) { dialog = new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true); slot = dialog->runModalWithCurrentTarget(); desc = dialog->getResultString(); if (desc.empty()) { // create our own description for the saved game, the user didnt enter it desc = dialog->createDefaultSaveDescription(slot); } if (desc.size() > 28) desc = Common::String(desc.c_str(), 28); } else { dialog = new GUI::SaveLoadChooser(_("Restore game:"), _("Restore"), false); slot = dialog->runModalWithCurrentTarget(); } delete dialog; if (slot < 0) return true; if (isSave) return doSave(slot, desc); else return doLoad(slot, false); } int AgiEngine::doSave(int slot, const Common::String &desc) { Common::String fileName = getSavegameFilename(slot); debugC(8, kDebugLevelMain | kDebugLevelResources, "file is [%s]", fileName.c_str()); // Make sure all graphics was blitted to screen. This fixes bug // #2960567: "AGI: Ego partly erased in Load/Save thumbnails" _gfx->updateScreen(); // _gfx->doUpdate(); return saveGame(fileName, desc); } int AgiEngine::doLoad(int slot, bool showMessages) { Common::String fileName = getSavegameFilename(slot); debugC(8, kDebugLevelMain | kDebugLevelResources, "file is [%s]", fileName.c_str()); _sprites->eraseSprites(); _sound->stopSound(); _text->closeWindow(); int result = loadGame(fileName); if (result == errOK) { _game.exitAllLogics = true; _menu->itemEnableAll(); } else { if (showMessages) _text->messageBox("Error restoring game."); } return result; } SavedGameSlotIdArray AgiEngine::getSavegameSlotIds() { Common::StringArray filenames; int16 numberPos = _targetName.size() + 1; int16 slotId = 0; SavedGameSlotIdArray slotIdArray; // search for saved game filenames... filenames = _saveFileMan->listSavefiles(_targetName + ".###"); Common::StringArray::iterator it; Common::StringArray::iterator end = filenames.end(); // convert to lower-case, just to be sure for (it = filenames.begin(); it != end; it++) { it->toLowercase(); } // sort Common::sort(filenames.begin(), filenames.end()); // now extract slot-Ids for (it = filenames.begin(); it != end; it++) { slotId = atoi(it->c_str() + numberPos); slotIdArray.push_back(slotId); } return slotIdArray; } Common::String AgiEngine::getSavegameFilename(int16 slotId) const { Common::String saveLoadSlot = _targetName; saveLoadSlot += Common::String::format(".%.3d", slotId); return saveLoadSlot; } bool AgiEngine::getSavegameInformation(int16 slotId, Common::String &saveDescription, uint32 &saveDate, uint32 &saveTime, bool &saveIsValid) { Common::InSaveFile *in; Common::String fileName = getSavegameFilename(slotId); char saveGameDescription[31]; int16 curPos = 0; byte saveVersion = 0; saveDescription.clear(); saveDate = 0; saveTime = 0; saveIsValid = false; debugC(4, kDebugLevelMain | kDebugLevelSavegame, "Current game id is %s", _targetName.c_str()); if (!(in = _saveFileMan->openForLoading(fileName))) { debugC(4, kDebugLevelMain | kDebugLevelSavegame, "File %s does not exist", fileName.c_str()); return false; } else { debugC(4, kDebugLevelMain | kDebugLevelSavegame, "Successfully opened %s for reading", fileName.c_str()); uint32 type = in->readUint32BE(); if (type != AGIflag) { warning("This doesn't appear to be an AGI savegame"); saveDescription += "[ScummVM: not an AGI save]"; delete in; return true; } debugC(6, kDebugLevelMain | kDebugLevelSavegame, "Has AGI flag, good start"); if (in->read(saveGameDescription, 31) != 31) { warning("unexpected EOF"); delete in; saveDescription += "[ScummVM: invalid save]"; return true; } for (curPos = 0; curPos < 31; curPos++) { if (!saveGameDescription[curPos]) break; } if (curPos >= 31) { warning("corrupted description"); delete in; saveDescription += "[ScummVM: invalid save]"; return true; } saveVersion = in->readByte(); if (saveVersion > SAVEGAME_CURRENT_VERSION) { warning("save from a future ScummVM, not supported"); delete in; saveDescription += "[ScummVM: not supported]"; return true; } if (saveVersion >= 4) { // We don't need the thumbnail here, so just read it and discard it Graphics::skipThumbnail(*in); saveDate = in->readUint32BE(); saveTime = in->readUint16BE() << 8; if (saveVersion >= 9) { saveTime |= in->readByte(); // add seconds (only available since saved game version 9+) } // save date is DDMMYYYY, we need a proper format byte saveDateDay = saveDate >> 24; byte saveDateMonth = (saveDate >> 16) & 0xFF; uint16 saveDateYear = saveDate & 0xFFFF; saveDate = (saveDateYear << 16) | (saveDateMonth << 8) | saveDateDay; } else { saveDate = 0; saveTime = 0; } saveDescription += saveGameDescription; saveIsValid = true; delete in; return true; } } bool AgiEngine::loadGameAutomatic() { int16 automaticRestoreGameSlotId = 0; automaticRestoreGameSlotId = _systemUI->figureOutAutomaticRestoreGameSlot(_game.automaticSaveDescription); if (automaticRestoreGameSlotId >= 0) { if (doLoad(automaticRestoreGameSlotId, true) == errOK) { return true; } } return false; } bool AgiEngine::loadGameDialog() { int16 restoreGameSlotId = 0; if (!ConfMan.getBool("originalsaveload")) return scummVMSaveLoadDialog(false); restoreGameSlotId = _systemUI->askForRestoreGameSlot(); if (restoreGameSlotId >= 0) { if (doLoad(restoreGameSlotId, true) == errOK) { return true; } } return errOK; } // Try to figure out either the slot, that is currently using the automatic saved game description // or get a new slot. // If we fail, return false, so that the regular saved game dialog is called // Original AGI was limited to 12 saves, we are effectively limited to 100 saves at the moment. // // btw. this also means that entering an existant name in Mixed Up Mother Goose will effectively overwrite // that saved game. This is also what original AGI did. bool AgiEngine::saveGameAutomatic() { int16 automaticSaveGameSlotId = 0; automaticSaveGameSlotId = _systemUI->figureOutAutomaticSaveGameSlot(_game.automaticSaveDescription); if (automaticSaveGameSlotId >= 0) { Common::String slotDescription(_game.automaticSaveDescription); // WORKAROUND: Remove window in case one is currently shown, otherwise it would get saved in the thumbnail // Happens for Mixed Up Mother Goose. The scripts close the window after saving. // Original interpreter obviously did not do this, but original interpreter also did not save thumbnails. _text->closeWindow(); if (doSave(automaticSaveGameSlotId, slotDescription) == errOK) { return true; } } return false; } bool AgiEngine::saveGameDialog() { int16 saveGameSlotId = 0; Common::String slotDescription; if (!ConfMan.getBool("originalsaveload")) return scummVMSaveLoadDialog(true); saveGameSlotId = _systemUI->askForSaveGameSlot(); if (saveGameSlotId >= 0) { if (_systemUI->askForSaveGameDescription(saveGameSlotId, slotDescription)) { if (doSave(saveGameSlotId, slotDescription) == errOK) { return true; } } } return false; } void AgiEngine::recordImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3, int16 p4, int16 p5, int16 p6, int16 p7) { ImageStackElement pnew; pnew.type = type; pnew.pad = 0; pnew.parm1 = p1; pnew.parm2 = p2; pnew.parm3 = p3; pnew.parm4 = p4; pnew.parm5 = p5; pnew.parm6 = p6; pnew.parm7 = p7; _imageStack.push(pnew); } void AgiEngine::replayImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3, int16 p4, int16 p5, int16 p6, int16 p7) { switch (type) { case ADD_PIC: debugC(8, kDebugLevelMain, "--- decoding picture %d ---", p1); agiLoadResource(RESOURCETYPE_PICTURE, p1); _picture->decodePicture(p1, p2, p3 != 0); break; case ADD_VIEW: agiLoadResource(RESOURCETYPE_VIEW, p1); _sprites->addToPic(p1, p2, p3, p4, p5, p6, p7); break; default: break; } } void AgiEngine::clearImageStack() { _imageStack.clear(); } void AgiEngine::releaseImageStack() { _imageStack.clear(); } void AgiEngine::checkQuickLoad() { if (ConfMan.hasKey("save_slot")) { Common::String saveNameBuffer = getSavegameFilename(ConfMan.getInt("save_slot")); _sprites->eraseSprites(); _sound->stopSound(); if (loadGame(saveNameBuffer, false) == errOK) { // Do not check game id _game.exitAllLogics = true; _menu->itemEnableAll(); } } } Common::Error AgiEngine::loadGameState(int slot) { Common::String saveLoadSlot = getSavegameFilename(slot); _sprites->eraseSprites(); _sound->stopSound(); if (loadGame(saveLoadSlot) == errOK) { _game.exitAllLogics = true; _menu->itemEnableAll(); return Common::kNoError; } else { return Common::kUnknownError; } } Common::Error AgiEngine::saveGameState(int slot, const Common::String &description) { Common::String saveLoadSlot = getSavegameFilename(slot); if (saveGame(saveLoadSlot, description) == errOK) return Common::kNoError; else return Common::kUnknownError; } } // End of namespace Agi