scummvm/engines/tinsel/saveload.cpp
Torbjörn Andersson 304ac2a7c4 TINSEL: Fix another Clang analyzer warning
Whether or not SaveFailure() can delete the save file, we want to
invalidate the save name because it probably only lives on the
stack so the pointer will become invalid once DoSave() has ended.
2013-01-02 21:25:51 +01:00

690 lines
18 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.
*
* Save and restore scene and game.
*/
#include "tinsel/actors.h"
#include "tinsel/config.h"
#include "tinsel/dialogs.h"
#include "tinsel/drives.h"
#include "tinsel/dw.h"
#include "tinsel/rince.h"
#include "tinsel/savescn.h"
#include "tinsel/timers.h"
#include "tinsel/tinlib.h"
#include "tinsel/tinsel.h"
#include "common/serializer.h"
#include "common/savefile.h"
#include "common/textconsole.h"
#include "common/translation.h"
#include "gui/message.h"
namespace Tinsel {
/**
* The current savegame format version.
* Our save/load system uses an elaborate scheme to allow us to modify the
* savegame while keeping full backward compatibility, in the sense that newer
* ScummVM versions always are able to load old savegames.
* In order to achieve that, we store a version in the savegame files, and whenever
* the savegame layout is modified, the version is incremented.
*
* This roughly works by marking each savegame entry with a range of versions
* for which it is valid; the save/load code iterates over all entries, but
* only saves/loads those which are valid for the version of the savegame
* which is being loaded/saved currently.
*/
#define CURRENT_VER 2
/**
* An auxillary macro, used to specify savegame versions. We use this instead
* of just writing the raw version, because this way they stand out more to
* the reading eye, making it a bit easier to navigate through the code.
*/
#define VER(x) x
//----------------- GLOBAL GLOBAL DATA --------------------
int g_thingHeld = 0;
int g_restoreCD = 0;
SRSTATE g_SRstate = SR_IDLE;
//----------------- EXTERN FUNCTIONS --------------------
// in DOS_DW.C
extern void syncSCdata(Common::Serializer &s);
// in PCODE.C
extern void syncGlobInfo(Common::Serializer &s);
// in POLYGONS.C
extern void syncPolyInfo(Common::Serializer &s);
extern int g_sceneCtr;
extern bool g_ASceneIsSaved;
//----------------- LOCAL DEFINES --------------------
struct SaveGameHeader {
uint32 id;
uint32 size;
uint32 ver;
char desc[SG_DESC_LEN];
TimeDate dateTime;
bool scnFlag;
byte language;
uint16 numInterpreters; // Savegame version 2 or later only
};
enum {
DW1_SAVEGAME_ID = 0x44575399, // = 'DWSc' = "DiscWorld 1 ScummVM"
DW2_SAVEGAME_ID = 0x44573253, // = 'DW2S' = "DiscWorld 2 ScummVM"
SAVEGAME_HEADER_SIZE = 4 + 4 + 4 + SG_DESC_LEN + 7 + 1 + 1 + 2
};
#define SAVEGAME_ID (TinselV2 ? (uint32)DW2_SAVEGAME_ID : (uint32)DW1_SAVEGAME_ID)
enum {
// FIXME: Save file names in ScummVM can be longer than 8.3, overflowing the
// name field in savedFiles. Raising it to 256 as a preliminary fix.
FNAMELEN = 256 // 8.3
};
struct SFILES {
char name[FNAMELEN];
char desc[SG_DESC_LEN + 2];
TimeDate dateTime;
};
//----------------- LOCAL GLOBAL DATA --------------------
// FIXME: Avoid non-const global vars
static int g_numSfiles = 0;
static SFILES g_savedFiles[MAX_SAVED_FILES];
static bool g_NeedLoad = true;
static SAVED_DATA *g_srsd = 0;
static int g_RestoreGameNumber = 0;
static char *g_SaveSceneName = 0;
static const char *g_SaveSceneDesc = 0;
static int *g_SaveSceneSsCount = 0;
static SAVED_DATA *g_SaveSceneSsData = 0; // points to 'SAVED_DATA ssdata[MAX_NEST]'
//------------- SAVE/LOAD SUPPORT METHODS ----------------
void setNeedLoad() {
g_NeedLoad = true;
}
static void syncTime(Common::Serializer &s, TimeDate &t) {
s.syncAsUint16LE(t.tm_year);
s.syncAsByte(t.tm_mon);
s.syncAsByte(t.tm_mday);
s.syncAsByte(t.tm_hour);
s.syncAsByte(t.tm_min);
s.syncAsByte(t.tm_sec);
}
static bool syncSaveGameHeader(Common::Serializer &s, SaveGameHeader &hdr) {
s.syncAsUint32LE(hdr.id);
s.syncAsUint32LE(hdr.size);
s.syncAsUint32LE(hdr.ver);
s.syncBytes((byte *)hdr.desc, SG_DESC_LEN);
hdr.desc[SG_DESC_LEN - 1] = 0;
syncTime(s, hdr.dateTime);
int tmp = hdr.size - s.bytesSynced();
// NOTE: We can't use SAVEGAME_ID here when attempting to remove a saved game from the launcher,
// as there is no TinselEngine initialized then. This means that we can't check if this is a DW1
// or DW2 savegame in this case, but it doesn't really matter, as the saved game is about to be
// deleted anyway. Refer to bug #3387551.
bool correctID = _vm ? (hdr.id == SAVEGAME_ID) : (hdr.id == DW1_SAVEGAME_ID || hdr.id == DW2_SAVEGAME_ID);
// Perform sanity check
if (tmp < 0 || !correctID || hdr.ver > CURRENT_VER || hdr.size > 1024)
return false;
if (tmp > 0) {
// If there's header space left, handling syncing the Scn flag and game language
s.syncAsByte(hdr.scnFlag);
s.syncAsByte(hdr.language);
tmp -= 2;
if (_vm && s.isLoading()) {
// If the engine is loaded, ensure the Scn/Gra usage is correct, and it's the correct language
if ((hdr.scnFlag != ((_vm->getFeatures() & GF_SCNFILES) != 0)) ||
(hdr.language != _vm->_config->_language))
return false;
}
}
// Handle the number of interpreter contexts that will be saved in the savegame
if (tmp >= 2) {
tmp -= 2;
hdr.numInterpreters = NUM_INTERPRET;
s.syncAsUint16LE(hdr.numInterpreters);
} else {
hdr.numInterpreters = (TinselV2 ? 70 : 64) - 20;
}
// Skip over any extra bytes
s.skip(tmp);
return true;
}
static void syncSavedMover(Common::Serializer &s, SAVED_MOVER &sm) {
int i, j;
s.syncAsUint32LE(sm.bActive);
s.syncAsSint32LE(sm.actorID);
s.syncAsSint32LE(sm.objX);
s.syncAsSint32LE(sm.objY);
s.syncAsUint32LE(sm.hLastfilm);
// Sync walk reels
for (i = 0; i < TOTAL_SCALES; ++i)
for (j = 0; j < 4; ++j)
s.syncAsUint32LE(sm.walkReels[i][j]);
// Sync stand reels
for (i = 0; i < TOTAL_SCALES; ++i)
for (j = 0; j < 4; ++j)
s.syncAsUint32LE(sm.standReels[i][j]);
// Sync talk reels
for (i = 0; i < TOTAL_SCALES; ++i)
for (j = 0; j < 4; ++j)
s.syncAsUint32LE(sm.talkReels[i][j]);
if (TinselV2) {
s.syncAsByte(sm.bHidden);
s.syncAsSint32LE(sm.brightness);
s.syncAsSint32LE(sm.startColor);
s.syncAsSint32LE(sm.paletteLength);
}
}
static void syncSavedActor(Common::Serializer &s, SAVED_ACTOR &sa) {
s.syncAsUint16LE(sa.actorID);
s.syncAsUint16LE(sa.zFactor);
s.syncAsUint16LE(sa.bAlive);
s.syncAsUint16LE(sa.bHidden);
s.syncAsUint32LE(sa.presFilm);
s.syncAsUint16LE(sa.presRnum);
s.syncAsUint16LE(sa.presPlayX);
s.syncAsUint16LE(sa.presPlayY);
}
extern void syncAllActorsAlive(Common::Serializer &s);
static void syncNoScrollB(Common::Serializer &s, NOSCROLLB &ns) {
s.syncAsSint32LE(ns.ln);
s.syncAsSint32LE(ns.c1);
s.syncAsSint32LE(ns.c2);
}
static void syncZPosition(Common::Serializer &s, Z_POSITIONS &zp) {
s.syncAsSint16LE(zp.actor);
s.syncAsSint16LE(zp.column);
s.syncAsSint32LE(zp.z);
}
static void syncPolyVolatile(Common::Serializer &s, POLY_VOLATILE &p) {
s.syncAsByte(p.bDead);
s.syncAsSint16LE(p.xoff);
s.syncAsSint16LE(p.yoff);
}
static void syncSoundReel(Common::Serializer &s, SOUNDREELS &sr) {
s.syncAsUint32LE(sr.hFilm);
s.syncAsSint32LE(sr.column);
s.syncAsSint32LE(sr.actorCol);
}
static void syncSavedData(Common::Serializer &s, SAVED_DATA &sd, int numInterp) {
s.syncAsUint32LE(sd.SavedSceneHandle);
s.syncAsUint32LE(sd.SavedBgroundHandle);
for (int i = 0; i < MAX_MOVERS; ++i)
syncSavedMover(s, sd.SavedMoverInfo[i]);
for (int i = 0; i < MAX_SAVED_ACTORS; ++i)
syncSavedActor(s, sd.SavedActorInfo[i]);
s.syncAsSint32LE(sd.NumSavedActors);
s.syncAsSint32LE(sd.SavedLoffset);
s.syncAsSint32LE(sd.SavedToffset);
for (int i = 0; i < numInterp; ++i)
sd.SavedICInfo[i].syncWithSerializer(s);
for (int i = 0; i < MAX_POLY; ++i)
s.syncAsUint32LE(sd.SavedDeadPolys[i]);
s.syncAsUint32LE(sd.SavedControl);
s.syncAsUint32LE(sd.SavedMidi);
s.syncAsUint32LE(sd.SavedLoop);
s.syncAsUint32LE(sd.SavedNoBlocking);
// SavedNoScrollData
for (int i = 0; i < MAX_VNOSCROLL; ++i)
syncNoScrollB(s, sd.SavedNoScrollData.NoVScroll[i]);
for (int i = 0; i < MAX_HNOSCROLL; ++i)
syncNoScrollB(s, sd.SavedNoScrollData.NoHScroll[i]);
s.syncAsUint32LE(sd.SavedNoScrollData.NumNoV);
s.syncAsUint32LE(sd.SavedNoScrollData.NumNoH);
// Tinsel 2 fields
if (TinselV2) {
// SavedNoScrollData
s.syncAsUint32LE(sd.SavedNoScrollData.xTrigger);
s.syncAsUint32LE(sd.SavedNoScrollData.xDistance);
s.syncAsUint32LE(sd.SavedNoScrollData.xSpeed);
s.syncAsUint32LE(sd.SavedNoScrollData.yTriggerTop);
s.syncAsUint32LE(sd.SavedNoScrollData.yTriggerBottom);
s.syncAsUint32LE(sd.SavedNoScrollData.yDistance);
s.syncAsUint32LE(sd.SavedNoScrollData.ySpeed);
for (int i = 0; i < NUM_ZPOSITIONS; ++i)
syncZPosition(s, sd.zPositions[i]);
s.syncBytes(sd.savedActorZ, MAX_SAVED_ACTOR_Z);
for (int i = 0; i < MAX_POLY; ++i)
syncPolyVolatile(s, sd.SavedPolygonStuff[i]);
for (int i = 0; i < 3; ++i)
s.syncAsUint32LE(sd.SavedTune[i]);
s.syncAsByte(sd.bTinselDim);
s.syncAsSint32LE(sd.SavedScrollFocus);
for (int i = 0; i < SV_TOPVALID; ++i)
s.syncAsSint32LE(sd.SavedSystemVars[i]);
for (int i = 0; i < MAX_SOUNDREELS; ++i)
syncSoundReel(s, sd.SavedSoundReels[i]);
}
}
/**
* Compare two TimeDate structs to see which one was earlier.
* Returns 0 if they are equal, a negative value if a is lower / first, and
* a positive value if b is lower / first.
*/
static int cmpTimeDate(const TimeDate &a, const TimeDate &b) {
int tmp;
#define CMP_ENTRY(x) tmp = a.x - b.x; if (tmp != 0) return tmp
CMP_ENTRY(tm_year);
CMP_ENTRY(tm_mon);
CMP_ENTRY(tm_mday);
CMP_ENTRY(tm_hour);
CMP_ENTRY(tm_min);
CMP_ENTRY(tm_sec);
#undef CMP_ENTRY
return 0;
}
/**
* Compute a list of all available saved game files.
* Store the file details, ordered by time, in savedFiles[] and return
* the number of files found.
*/
int getList(Common::SaveFileManager *saveFileMan, const Common::String &target) {
// No change since last call?
// TODO/FIXME: Just always reload this data? Be careful about slow downs!!!
if (!g_NeedLoad)
return g_numSfiles;
int i;
const Common::String pattern = target + ".???";
Common::StringArray files = saveFileMan->listSavefiles(pattern);
g_numSfiles = 0;
for (Common::StringArray::const_iterator file = files.begin(); file != files.end(); ++file) {
if (g_numSfiles >= MAX_SAVED_FILES)
break;
const Common::String &fname = *file;
Common::InSaveFile *f = saveFileMan->openForLoading(fname);
if (f == NULL) {
continue;
}
// Try to load save game header
Common::Serializer s(f, 0);
SaveGameHeader hdr;
bool validHeader = syncSaveGameHeader(s, hdr);
delete f;
if (!validHeader) {
continue; // Invalid header, or savegame too new -> skip it
// TODO: In SCUMM, we still show an entry for the save, but with description
// "incompatible version".
}
i = g_numSfiles;
#ifndef DISABLE_SAVEGAME_SORTING
for (i = 0; i < g_numSfiles; i++) {
if (cmpTimeDate(hdr.dateTime, g_savedFiles[i].dateTime) > 0) {
Common::copy_backward(&g_savedFiles[i], &g_savedFiles[g_numSfiles], &g_savedFiles[g_numSfiles + 1]);
break;
}
}
#endif
Common::strlcpy(g_savedFiles[i].name, fname.c_str(), FNAMELEN);
Common::strlcpy(g_savedFiles[i].desc, hdr.desc, SG_DESC_LEN);
g_savedFiles[i].dateTime = hdr.dateTime;
++g_numSfiles;
}
// Next getList() needn't do its stuff again
g_NeedLoad = false;
return g_numSfiles;
}
int getList() {
// No change since last call?
// TODO/FIXME: Just always reload this data? Be careful about slow downs!!!
if (!g_NeedLoad)
return g_numSfiles;
return getList(_vm->getSaveFileMan(), _vm->getTargetName());
}
char *ListEntry(int i, letype which) {
if (i == -1)
i = g_numSfiles;
assert(i >= 0);
if (i < g_numSfiles)
return which == LE_NAME ? g_savedFiles[i].name : g_savedFiles[i].desc;
else
return NULL;
}
static bool DoSync(Common::Serializer &s, int numInterp) {
int sg = 0;
if (TinselV2) {
if (s.isSaving())
g_restoreCD = GetCurrentCD();
s.syncAsSint16LE(g_restoreCD);
}
if (TinselV2 && s.isLoading())
HoldItem(INV_NOICON);
syncSavedData(s, *g_srsd, numInterp);
syncGlobInfo(s); // Glitter globals
syncInvInfo(s); // Inventory data
// Held object
if (s.isSaving())
sg = WhichItemHeld();
s.syncAsSint32LE(sg);
if (s.isLoading()) {
if (sg != -1 && !GetIsInvObject(sg))
// Not a valid inventory object, so return false
return false;
if (TinselV2)
g_thingHeld = sg;
else
HoldItem(sg);
}
syncTimerInfo(s); // Timer data
if (!TinselV2)
syncPolyInfo(s); // Dead polygon data
syncSCdata(s); // Hook Scene and delayed scene
s.syncAsSint32LE(*g_SaveSceneSsCount);
if (*g_SaveSceneSsCount != 0) {
SAVED_DATA *sdPtr = g_SaveSceneSsData;
for (int i = 0; i < *g_SaveSceneSsCount; ++i, ++sdPtr)
syncSavedData(s, *sdPtr, numInterp);
// Flag that there is a saved scene to return to. Note that in this context 'saved scene'
// is a stored scene to return to from another scene, such as from the Summoning Book close-up
// in Discworld 1 to whatever scene Rincewind was in prior to that
g_ASceneIsSaved = true;
}
if (!TinselV2)
syncAllActorsAlive(s);
return true;
}
/**
* DoRestore
*/
static bool DoRestore() {
Common::InSaveFile *f = _vm->getSaveFileMan()->openForLoading(g_savedFiles[g_RestoreGameNumber].name);
if (f == NULL) {
return false;
}
Common::Serializer s(f, 0);
SaveGameHeader hdr;
if (!syncSaveGameHeader(s, hdr)) {
delete f; // Invalid header, or savegame too new -> skip it
return false;
}
// Load in the data. For older savegame versions, we potentially need to load the data twice, once
// for pre 1.5 savegames, and if that fails, a second time for 1.5 savegames
int numInterpreters = hdr.numInterpreters;
int32 currentPos = f->pos();
for (int tryNumber = 0; tryNumber < ((hdr.ver >= 2) ? 1 : 2); ++tryNumber) {
// If it's the second loop iteration, try with the 1.5 savegame number of interpreter contexts
if (tryNumber == 1) {
f->seek(currentPos);
numInterpreters = 80;
}
// Load the savegame data
if (DoSync(s, numInterpreters))
// Data load was successful (or likely), so break out of loop
break;
}
uint32 id = f->readSint32LE();
if (id != (uint32)0xFEEDFACE)
error("Incompatible saved game");
bool failed = (f->eos() || f->err());
delete f;
if (failed) {
GUI::MessageDialog dialog(_("Failed to load game state from file."));
dialog.runModal();
}
return !failed;
}
static void SaveFailure(Common::OutSaveFile *f) {
if (f) {
delete f;
_vm->getSaveFileMan()->removeSavefile(g_SaveSceneName);
}
g_SaveSceneName = NULL; // Invalidate save name
GUI::MessageDialog dialog(_("Failed to save game state to file."));
dialog.runModal();
}
/**
* DoSave
*/
static void DoSave() {
Common::OutSaveFile *f;
char tmpName[FNAMELEN];
// Next getList() must do its stuff again
g_NeedLoad = true;
if (g_SaveSceneName == NULL) {
// Generate a new unique save name
int i;
int ano = 1; // Allocated number
while (1) {
Common::String fname = _vm->getSavegameFilename(ano);
strcpy(tmpName, fname.c_str());
for (i = 0; i < g_numSfiles; i++)
if (!strcmp(g_savedFiles[i].name, tmpName))
break;
if (i == g_numSfiles)
break;
ano++;
}
g_SaveSceneName = tmpName;
}
if (g_SaveSceneDesc[0] == 0)
g_SaveSceneDesc = "unnamed";
f = _vm->getSaveFileMan()->openForSaving(g_SaveSceneName);
Common::Serializer s(0, f);
if (f == NULL) {
SaveFailure(f);
return;
}
// Write out a savegame header
SaveGameHeader hdr;
hdr.id = SAVEGAME_ID;
hdr.size = SAVEGAME_HEADER_SIZE;
hdr.ver = CURRENT_VER;
memcpy(hdr.desc, g_SaveSceneDesc, SG_DESC_LEN);
hdr.desc[SG_DESC_LEN - 1] = 0;
g_system->getTimeAndDate(hdr.dateTime);
hdr.scnFlag = _vm->getFeatures() & GF_SCNFILES;
hdr.language = _vm->_config->_language;
if (!syncSaveGameHeader(s, hdr) || f->err()) {
SaveFailure(f);
return;
}
DoSync(s, hdr.numInterpreters);
// Write out the special Id for Discworld savegames
f->writeUint32LE(0xFEEDFACE);
if (f->err()) {
SaveFailure(f);
return;
}
f->finalize();
delete f;
g_SaveSceneName = NULL; // Invalidate save name
}
/**
* ProcessSRQueue
*/
void ProcessSRQueue() {
switch (g_SRstate) {
case SR_DORESTORE:
// If a load has been done directly from title screens, set a larger value for scene ctr so the
// code used to skip the title screens in Discworld 1 gets properly disabled
if (g_sceneCtr < 10)
g_sceneCtr = 10;
if (DoRestore()) {
DoRestoreScene(g_srsd, false);
}
g_SRstate = SR_IDLE;
break;
case SR_DOSAVE:
DoSave();
g_SRstate = SR_IDLE;
break;
default:
break;
}
}
void RequestSaveGame(char *name, char *desc, SAVED_DATA *sd, int *pSsCount, SAVED_DATA *pSsData) {
assert(g_SRstate == SR_IDLE);
g_SaveSceneName = name;
g_SaveSceneDesc = desc;
g_SaveSceneSsCount = pSsCount;
g_SaveSceneSsData = pSsData;
g_srsd = sd;
g_SRstate = SR_DOSAVE;
}
void RequestRestoreGame(int num, SAVED_DATA *sd, int *pSsCount, SAVED_DATA *pSsData) {
if (TinselV2) {
if (num == -1)
return;
else if (num == -2) {
// From CD change for restore
num = g_RestoreGameNumber;
}
}
assert(num >= 0);
g_RestoreGameNumber = num;
g_SaveSceneSsCount = pSsCount;
g_SaveSceneSsData = pSsData;
g_srsd = sd;
g_SRstate = SR_DORESTORE;
}
/**
* Returns the index of the most recently saved savegame. This will always be
* the file at the first index, since the list is sorted by date/time
*/
int NewestSavedGame() {
int numFiles = getList();
return (numFiles == 0) ? -1 : 0;
}
} // End of namespace Tinsel