/* 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. * * $URL$ * $Id$ * * Save and restore scene and game. */ #include "tinsel/actors.h" #include "tinsel/dialogs.h" #include "tinsel/drives.h" #include "tinsel/dw.h" #include "tinsel/rince.h" #include "tinsel/savescn.h" #include "tinsel/serializer.h" #include "tinsel/timers.h" #include "tinsel/tinlib.h" #include "tinsel/tinsel.h" #include "common/savefile.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 1 // TODO: Not yet used /** * 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 thingHeld = 0; int restoreCD = 0; SRSTATE SRstate = SR_IDLE; //----------------- EXTERN FUNCTIONS -------------------- // in DOS_DW.C extern void syncSCdata(Serializer &s); // in DOS_MAIN.C //char HardDriveLetter(void); // in PCODE.C extern void syncGlobInfo(Serializer &s); // in POLYGONS.C extern void syncPolyInfo(Serializer &s); //----------------- LOCAL DEFINES -------------------- struct SaveGameHeader { uint32 id; uint32 size; uint32 ver; char desc[SG_DESC_LEN]; struct tm dateTime; }; 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 }; #define SAVEGAME_ID (TinselV2 ? (uint32)DW2_SAVEGAME_ID : (uint32)DW1_SAVEGAME_ID) //----------------- LOCAL GLOBAL DATA -------------------- static int numSfiles = 0; static SFILES savedFiles[MAX_SAVED_FILES]; static bool NeedLoad = true; static SAVED_DATA *srsd = 0; static int RestoreGameNumber = 0; static char *SaveSceneName = 0; static const char *SaveSceneDesc = 0; static int *SaveSceneSsCount = 0; static char *SaveSceneSsData = 0; // points to 'SAVED_DATA ssdata[MAX_NEST]' //------------- SAVE/LOAD SUPPORT METHODS ---------------- void setNeedLoad() { NeedLoad = true; } static void syncTime(Serializer &s, struct tm &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); if (s.isLoading()) { t.tm_wday = 0; t.tm_yday = 0; t.tm_isdst = 0; } } static bool syncSaveGameHeader(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(); // Perform sanity check if (tmp < 0 || hdr.id != SAVEGAME_ID || hdr.ver > CURRENT_VER || hdr.size > 1024) return false; // Skip over any extra bytes while (tmp-- > 0) { byte b = 0; s.syncAsByte(b); } return true; } static void syncSavedMover(Serializer &s, SAVED_MOVER &sm) { SCNHANDLE *pList[3] = { (SCNHANDLE *)&sm.walkReels, (SCNHANDLE *)&sm.standReels, (SCNHANDLE *)&sm.talkReels }; s.syncAsUint32LE(sm.bActive); s.syncAsSint32LE(sm.actorID); s.syncAsSint32LE(sm.objX); s.syncAsSint32LE(sm.objY); s.syncAsUint32LE(sm.hLastfilm); for (int pIndex = 0; pIndex < 3; ++pIndex) { SCNHANDLE *p = pList[pIndex]; for (int i = 0; i < TOTAL_SCALES * 4; ++i) s.syncAsUint32LE(*p++); } if (TinselV2) { s.syncAsByte(sm.bHidden); s.syncAsSint32LE(sm.brightness); s.syncAsSint32LE(sm.startColour); s.syncAsSint32LE(sm.paletteLength); } } static void syncSavedActor(Serializer &s, SAVED_ACTOR &sa) { s.syncAsUint16LE(sa.actorID); s.syncAsUint16LE(sa.zFactor); s.syncAsUint32LE(sa.bAlive); s.syncAsUint32LE(sa.presFilm); s.syncAsUint16LE(sa.presRnum); s.syncAsUint16LE(sa.presPlayX); s.syncAsUint16LE(sa.presPlayY); } extern void syncAllActorsAlive(Serializer &s); static void syncNoScrollB(Serializer &s, NOSCROLLB &ns) { s.syncAsSint32LE(ns.ln); s.syncAsSint32LE(ns.c1); s.syncAsSint32LE(ns.c2); } static void syncZPosition(Serializer &s, Z_POSITIONS &zp) { s.syncAsSint16LE(zp.actor); s.syncAsSint16LE(zp.column); s.syncAsSint32LE(zp.z); } static void syncPolyVolatile(Serializer &s, POLY_VOLATILE &p) { s.syncAsByte(p.bDead); s.syncAsSint16LE(p.xoff); s.syncAsSint16LE(p.yoff); } static void syncSoundReel(Serializer &s, SOUNDREELS &sr) { s.syncAsUint32LE(sr.hFilm); s.syncAsSint32LE(sr.column); s.syncAsSint32LE(sr.actorCol); } static void syncSavedData(Serializer &s, SAVED_DATA &sd) { 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 < NUM_INTERPRET; ++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]); } } /** * Called when saving a game to a new file. * Generates a new, unique, filename. */ static char *NewName(void) { static char result[FNAMELEN]; int i; int ano = 1; // Allocated number while (1) { Common::String fname = _vm->getSavegameFilename(ano); strcpy(result, fname.c_str()); for (i = 0; i < numSfiles; i++) if (!strcmp(savedFiles[i].name, result)) break; if (i == numSfiles) break; ano++; } return result; } /** * Interrogate the current DOS directory for 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 (!NeedLoad) return numSfiles; int i; const Common::String pattern = target + ".???"; Common::StringList files = saveFileMan->listSavefiles(pattern.c_str()); numSfiles = 0; for (Common::StringList::const_iterator file = files.begin(); file != files.end(); ++file) { if (numSfiles >= MAX_SAVED_FILES) break; const Common::String &fname = *file; Common::InSaveFile *f = saveFileMan->openForLoading(fname.c_str()); if (f == NULL) { continue; } // Try to load save game header 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 = numSfiles; #ifndef DISABLE_SAVEGAME_SORTING for (i = 0; i < numSfiles; i++) { if (difftime(mktime(&hdr.dateTime), mktime(&savedFiles[i].dateTime)) > 0) { Common::copy_backward(&savedFiles[i], &savedFiles[numSfiles], &savedFiles[numSfiles + 1]); break; } } #endif strncpy(savedFiles[i].name, fname.c_str(), FNAMELEN); strncpy(savedFiles[i].desc, hdr.desc, SG_DESC_LEN); savedFiles[i].desc[SG_DESC_LEN - 1] = 0; savedFiles[i].dateTime = hdr.dateTime; ++numSfiles; } // Next getList() needn't do its stuff again NeedLoad = false; return numSfiles; } int getList(void) { // No change since last call? // TODO/FIXME: Just always reload this data? Be careful about slow downs!!! if (!NeedLoad) return numSfiles; return getList(_vm->getSaveFileMan(), _vm->getTargetName()); } char *ListEntry(int i, letype which) { if (i == -1) i = numSfiles; assert(i >= 0); if (i < numSfiles) return which == LE_NAME ? savedFiles[i].name : savedFiles[i].desc; else return NULL; } static void DoSync(Serializer &s) { int sg; if (TinselV2) { if (s.isSaving()) restoreCD = GetCurrentCD(); s.syncAsSint16LE(restoreCD); } if (TinselV2 && s.isLoading()) HoldItem(INV_NOICON); syncSavedData(s, *srsd); syncGlobInfo(s); // Glitter globals syncInvInfo(s); // Inventory data // Held object if (s.isSaving()) sg = WhichItemHeld(); s.syncAsSint32LE(sg); if (s.isLoading()) { if (TinselV2) 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(*SaveSceneSsCount); if (*SaveSceneSsCount != 0) { SAVED_DATA *sdPtr = (SAVED_DATA *)SaveSceneSsData; for (int i = 0; i < *SaveSceneSsCount; ++i, ++sdPtr) syncSavedData(s, *sdPtr); } if (!TinselV2) syncAllActorsAlive(s); } /** * DoRestore */ static bool DoRestore(bool fromGMM) { Common::InSaveFile *f; uint32 id; if (!fromGMM) f = _vm->getSaveFileMan()->openForLoading(savedFiles[RestoreGameNumber].name); else f = _vm->getSaveFileMan()->openForLoading(_vm->getSavegameFilename(RestoreGameNumber).c_str()); if (f == NULL) { return false; } Serializer s(f, 0); SaveGameHeader hdr; if (!syncSaveGameHeader(s, hdr)) { delete f; // Invalid header, or savegame too new -> skip it return false; } DoSync(s); id = f->readSint32LE(); if (id != (uint32)0xFEEDFACE) error("Incompatible saved game"); bool failed = f->ioFailed(); delete f; return !failed; } /** * DoSave */ static void DoSave(void) { Common::OutSaveFile *f; const char *fname; // Next getList() must do its stuff again NeedLoad = true; if (SaveSceneName == NULL) SaveSceneName = NewName(); if (SaveSceneDesc[0] == 0) SaveSceneDesc = "unnamed"; fname = SaveSceneName; f = _vm->getSaveFileMan()->openForSaving(fname); if (f == NULL) return; Serializer s(0, f); // Write out a savegame header SaveGameHeader hdr; hdr.id = SAVEGAME_ID; hdr.size = SAVEGAME_HEADER_SIZE; hdr.ver = CURRENT_VER; memcpy(hdr.desc, SaveSceneDesc, SG_DESC_LEN); hdr.desc[SG_DESC_LEN - 1] = 0; g_system->getTimeAndDate(hdr.dateTime); if (!syncSaveGameHeader(s, hdr) || f->ioFailed()) { goto save_failure; } DoSync(s); // Write out the special Id for Discworld savegames f->writeUint32LE(0xFEEDFACE); if (f->ioFailed()) goto save_failure; f->finalize(); delete f; return; save_failure: delete f; _vm->getSaveFileMan()->removeSavefile(fname); } /** * ProcessSRQueue */ void ProcessSRQueue(void) { switch (SRstate) { case SR_DORESTORE: case SR_DORESTORE_GMM: if (DoRestore(SRstate == SR_DORESTORE_GMM)) { DoRestoreScene(srsd, false); } SRstate = SR_IDLE; break; case SR_DOSAVE: DoSave(); SRstate = SR_IDLE; break; default: break; } } void RequestSaveGame(char *name, char *desc, SAVED_DATA *sd, int *pSsCount, SAVED_DATA *pSsData) { assert(SRstate == SR_IDLE); SaveSceneName = name; SaveSceneDesc = desc; SaveSceneSsCount = pSsCount; SaveSceneSsData = (char *)pSsData; srsd = sd; SRstate = SR_DOSAVE; } void RequestRestoreGame(int num, SAVED_DATA *sd, int *pSsCount, SAVED_DATA *pSsData, bool fromGMM) { if (TinselV2) { if (num == -1) return; else if (num == -2) { // From CD change for restore num = RestoreGameNumber; } } assert(num >= 0); RestoreGameNumber = num; SaveSceneSsCount = pSsCount; SaveSceneSsData = (char *)pSsData; srsd = sd; SRstate = (!fromGMM) ? SR_DORESTORE : SR_DORESTORE_GMM; } /** * 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(void) { int numFiles = getList(); return (numFiles == 0) ? -1 : 0; } } // end of namespace Tinsel