mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-15 14:18:37 +00:00
1093 lines
33 KiB
C++
1093 lines
33 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 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 <vasyl@pacbell.net>
|
|
// Multi-slots by Claudio Matsuoka <claudio@helllabs.org>
|
|
//
|
|
|
|
#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<ImageStackElement>::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;
|
|
}
|
|
}
|
|
|
|
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
|