scummvm/engines/agi/saveload.cpp
Martin Kiewitz cca9fc918f AGI: Save screen object loop_flag
loop_flag was previously vt.parm1, which was shared for multiple
uses. Was split up during graphics rewrite in commit
8a595e7771

Is indirectly part of bug #7046. Saving, restarting ScummVM and
restoring right after grabbing the eagle resulted in the glitch
not happening (which was of course an inaccuracy anyway). This
was caused by AGI currently not saving/restoring the loop_flag.
Needs to get further figured out what's exactly happening
internally and if this issue was just hidden by the shared
vt.parm1 in previous versions. If triggered, it would have
just set another pseudo-random flag on end-of-loop.
2016-02-19 02:00:35 +01:00

1096 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
strcpy(gameIDstring, _game.id);
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
}
}
}
for (i = vtEntries; i < SCREENOBJECTS_MAX; i++) {
memset(&_game.screenObjTable[i], 0, sizeof(ScreenObjEntry));
}
// 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