- major work on savegame code

Not tested yet!

* Added a JSON-based header to the savegames so that the unified menu can read from a common data source.
* moved loading and saving of frontend independent data to the wrapper so that support is automatic.
This commit is contained in:
Christoph Oelckers 2019-11-27 00:41:26 +01:00
parent 3b7aa74c27
commit 723b210c95
25 changed files with 371 additions and 539 deletions

View file

@ -92,6 +92,8 @@ struct GameInterface : ::GameInterface
void set_hud_scale(int size) override;
bool mouseInactiveConditional(bool condition) override;
FString statFPS() override;
FSavegameInfo GetSaveSig() override;
};
END_BLD_NS

View file

@ -46,8 +46,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "sound.h"
#include "i_specialpaths.h"
#include "view.h"
#include "statistics.h"
#include "secrets.h"
#include "savegamehelp.h"
BEGIN_BLD_NS
@ -102,7 +100,7 @@ void LoadSave::Write(void *pData, int nSize)
ThrowError("File error #%d writing save file.", errno);
}
void LoadSave::LoadGame(char *pzFile)
void LoadSave::LoadGame(const char *pzFile)
{
bool demoWasPlayed = gDemo.at1;
if (gDemo.at1)
@ -128,8 +126,6 @@ void LoadSave::LoadGame(char *pzFile)
rover->Load();
rover = rover->next;
}
if (!ReadStatistics() || !SECRET_Load()) // read the rest...
ThrowError("Error loading save file.");
hLFile.Close();
FinishSavegameRead();
@ -194,7 +190,7 @@ void LoadSave::LoadGame(char *pzFile)
//sndPlaySong(gGameOptions.zLevelSong, 1);
}
void LoadSave::SaveGame(char *pzFile)
void LoadSave::SaveGame(const char *pzFile)
{
OpenSaveGameForWrite(pzFile);
hSFile = WriteSavegameChunk("snapshot.bld");
@ -211,8 +207,9 @@ void LoadSave::SaveGame(char *pzFile)
dword_27AA38 = 0;
rover = rover->next;
}
SaveStatistics();
SECRET_Save();
auto & li = gEpisodeInfo[gGameOptions.nEpisode].at28[gGameOptions.nLevel];
G_WriteSaveHeader(gGameOptions.szUserGameName, li.at0, li.at90);
FinishSavegameWrite();
hSFile = NULL;
}

View file

@ -49,8 +49,8 @@ public:
virtual void Load(void);
void Read(void *, int);
void Write(void *, int);
static void LoadGame(char *);
static void SaveGame(char *);
static void LoadGame(const char *);
static void SaveGame(const char *);
};
extern unsigned int gSavedOffset;

View file

@ -43,6 +43,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "view.h"
#include "cmdlib.h"
#include "i_specialpaths.h"
#include "savegamehelp.h"
EXTERN_CVAR(Bool, hud_powerupduration)
@ -2069,7 +2070,6 @@ short gQuickSaveSlot = -1;
void SaveGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
{
char strSaveGameName[BMAX_PATH];
int nSlot = pItem->at28;
if (gGameOptions.nGameType > 0 || !gGameStarted)
return;
@ -2078,21 +2078,21 @@ void SaveGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
gGameMenuMgr.Deactivate();
return;
}
snprintf(strSaveGameName, BMAX_PATH, "%sgame00%02d.sav", M_GetSavegamesPath().GetChars(), nSlot);
FStringf basename("save%04d", nSlot);
auto strSaveGameName = G_BuildSaveName(basename);
strcpy(gGameOptions.szUserGameName, strRestoreGameStrings[nSlot]);
sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName);
sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName.GetChars());
gGameOptions.nSaveGameSlot = nSlot;
viewLoadingScreen(2518, "Saving", "Saving Your Game", strRestoreGameStrings[nSlot]);
videoNextPage();
gSaveGameNum = nSlot;
LoadSave::SaveGame(strSaveGameName);
LoadSave::SaveGame(strSaveGameName.GetChars());
gQuickSaveSlot = nSlot;
gGameMenuMgr.Deactivate();
}
void QuickSaveGame(void)
{
char strSaveGameName[BMAX_PATH];
if (gGameOptions.nGameType > 0 || !gGameStarted)
return;
/*if (strSaveGameName[0])
@ -2100,9 +2100,11 @@ void QuickSaveGame(void)
gGameMenuMgr.Deactivate();
return;
}*/
snprintf(strSaveGameName, BMAX_PATH, "%sgame00%02d.sav", M_GetSavegamesPath().GetChars(), gQuickSaveSlot);
FStringf basename("save%04d", gQuickSaveSlot);
auto strSaveGameName = G_BuildSaveName(basename);
strcpy(gGameOptions.szUserGameName, strRestoreGameStrings[gQuickSaveSlot]);
sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName);
sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName.GetChars());
gGameOptions.nSaveGameSlot = gQuickSaveSlot;
viewLoadingScreen(2518, "Saving", "Saving Your Game", strRestoreGameStrings[gQuickSaveSlot]);
videoNextPage();
@ -2116,11 +2118,11 @@ void QuickSaveGame(void)
void LoadGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
{
UNREFERENCED_PARAMETER(event);
char strLoadGameName[BMAX_PATH];
int nSlot = pItem->at28;
if (gGameOptions.nGameType > 0)
return;
snprintf(strLoadGameName, BMAX_PATH, "%sgame00%02d.sav", M_GetSavegamesPath().GetChars(), nSlot);
FStringf basename("save%04d", nSlot);
auto strLoadGameName = G_BuildSaveName(basename);
if (!FileExists(strLoadGameName))
return;
viewLoadingScreen(2518, "Loading", "Loading Saved Game", strRestoreGameStrings[nSlot]);
@ -2132,10 +2134,11 @@ void LoadGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
void QuickLoadGame(void)
{
char strLoadGameName[BMAX_PATH];
if (gGameOptions.nGameType > 0)
return;
snprintf(strLoadGameName, BMAX_PATH, "%sgame00%02d.sav", M_GetSavegamesPath().GetChars(), gQuickLoadSlot);
FStringf basename("save%04d", gQuickSaveSlot);
auto strLoadGameName = G_BuildSaveName(basename);
if (!FileExists(strLoadGameName))
return;
viewLoadingScreen(2518, "Loading", "Loading Saved Game", strRestoreGameStrings[gQuickLoadSlot]);
@ -2270,4 +2273,9 @@ void drawLoadingScreen(void)
viewLoadingScreen(2049, buffer, levelGetTitle(), NULL);
}
FSavegameInfo GameInterface::GetSaveSig()
{
return { SAVESIG_BLD, MINSAVEVER_BLD, SAVEVER_BLD };
}
END_BLD_NS

View file

@ -177,6 +177,13 @@ struct FGameStartup
int CustomLevel2;
};
struct FSavegameInfo
{
const char *savesig;
int minsavever;
int currentsavever;
};
struct GameInterface
{
enum EMenuSounds
@ -202,6 +209,7 @@ struct GameInterface
virtual bool CanSave() { return true; }
virtual void CustomMenuSelection(int menu, int item) {}
virtual void StartGame(FGameStartup& gs) {}
virtual FSavegameInfo GetSaveSig() { return { "", 0, 0}; }
};
extern GameInterface* gi;

View file

@ -267,7 +267,7 @@ static void D_AddDirectory (TArray<FString> &wadfiles, const char *dir)
{
skindir[stuffstart++] = '/';
int savedstart = stuffstart;
const char* validexts[] = { "*.grp", "*.zip", "*.pk3", "*.pk4", "*.7z", "*.pk7" };
const char* validexts[] = { "*.grp", "*.zip", "*.pk3", "*.pk4", "*.7z", "*.pk7", "*.dat" };
for (auto ext : validexts)
{
stuffstart = savedstart;

View file

@ -42,6 +42,11 @@
#include "gstrings.h"
#include "d_gui.h"
#include "v_draw.h"
#include "files.h"
#include "resourcefile.h"
#include "sjson.h"
#include "savegamehelp.h"
#include "i_specialpaths.h"
#include "../../platform/win32/i_findfile.h" // This is a temporary direct path. Needs to be fixed when stuff gets cleaned up.
@ -207,7 +212,7 @@ void DLoadSaveMenu::ReadSaveStrings ()
LastSaved = LastAccessed = -1;
quickSaveSlot = NULL;
filter = "";// G_BuildSaveName("*.zds", -1);
filter = G_BuildSaveName("*");
filefirst = I_FindFirst (filter.GetChars(), &c_file);
if (filefirst != ((void *)(-1)))
{
@ -215,25 +220,30 @@ void DLoadSaveMenu::ReadSaveStrings ()
{
// I_FindName only returns the file's name and not its full path
FString filepath = "";// G_BuildSaveName(I_FindName(&c_file), -1);
FILE *file = fopen (filepath, "rb");
if (file != NULL)
FResourceFile *savegame = FResourceFile::OpenResourceFile(filepath, true, true);
if (savegame != nullptr)
{
//PNGHandle *png;
//char sig[16];
FResourceLump *info = savegame->FindLump("info.json");
if (info == nullptr)
{
// savegame info not found. This is not a savegame so leave it alone.
delete savegame;
continue;
}
auto fr = info->NewReader();
FString title;
bool oldVer = true;
bool addIt = false;
bool missing = false;
// ZDoom 1.23 betas 21-33 have the savesig first.
// Earlier versions have the savesig second.
// Later versions have the savegame encapsulated inside a PNG.
//
// Old savegame versions are always added to the menu so
// the user can easily delete them if desired.
// Todo: Identify savegames here.
int check = G_ValidateSavegame(fr, &title);
delete savegame;
if (check != 0)
{
FSaveGameNode *node = new FSaveGameNode;
node->Filename = filepath;
node->bOldVersion = check == -1;
node->bMissingWads = check == -2;
node->Title = title;
InsertSaveNode(node);
}
}
} while (I_FindNext (filefirst, &c_file) == 0);
I_FindClose (filefirst);
@ -691,7 +701,7 @@ bool DLoadSaveMenu::Responder (event_t *ev)
{
FString EndString;
EndString.Format("%s" TEXTCOLOR_WHITE "%s" TEXTCOLOR_NORMAL "?\n\n%s",
GStrings("MNU_DELETESG"), SaveGames[Selected]->Title, GStrings("PRESSYN"));
GStrings("MNU_DELETESG"), SaveGames[Selected]->Title.GetChars(), GStrings("PRESSYN"));
M_StartMessage (EndString, 0);
}
return true;

View file

@ -1,5 +1,7 @@
/*
** savegame.cpp
**
** common savegame utilities for all front ends.
**
**---------------------------------------------------------------------------
** Copyright 2019 Christoph Oelckers
@ -29,21 +31,36 @@
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
** This is for keeping my sanity while working with the horrible mess
** that is the savegame code in Duke Nukem.
** Without handling this in global variables it is a losing proposition
** to save custom data along with the regular snapshot. :(
** With this the savegame code can mostly pretend to load from and write
** to files while really using a composite archive.
*/
*/
#include "compositesaveame.h"
#include "savegamehelp.h"
#include "sjson.h"
#include "baselayer.h"
#include "gstrings.h"
#include "i_specialpaths.h"
#include "cmdlib.h"
#include "filesystem/filesystem.h"
#include "statistics.h"
#include "secrets.h"
static CompositeSavegameWriter savewriter;
static FResourceFile *savereader;
//=============================================================================
//
// This is for keeping my sanity while working with the horrible mess
// that is the savegame code in Duke Nukem.
// Without handling this in global variables it is a losing proposition
// to save custom data along with the regular snapshot. :(
// With this the savegame code can mostly pretend to load from and write
// to files while really using a composite archive.
//
// All global non-game dependent state is also saved right here for convenience.
//
//=============================================================================
void OpenSaveGameForWrite(const char *name)
{
savewriter.Clear();
@ -54,6 +71,13 @@ bool OpenSaveGameForRead(const char *name)
{
if (savereader) delete savereader;
savereader = FResourceFile::OpenResourceFile(name, true, true);
if (savereader != nullptr)
{
ReadStatistics();
SECRET_Load();
}
return savereader != nullptr;
}
@ -80,3 +104,174 @@ void FinishSavegameRead()
delete savereader;
savereader = nullptr;
}
//=============================================================================
//
// Writes the header which is used to display the savegame in the menu.
//
//=============================================================================
void G_WriteSaveHeader(const char *name, const char*mapname, const char *maptitle)
{
sjson_context* ctx = sjson_create_context(0, 0, NULL);
if (!ctx)
{
return;
}
sjson_node* root = sjson_mkobject(ctx);
auto savesig = gi->GetSaveSig();
sjson_put_int(ctx, root, "Save Version", savesig.currentsavever);
sjson_put_string(ctx, root, "Engine", savesig.savesig);
sjson_put_string(ctx, root, "Game Resource", fileSystem.GetResourceFileName(1));
sjson_put_string(ctx, root, "map", mapname);
sjson_put_string(ctx, root, "Title", maptitle);
if (*mapname == '/') mapname++;
sjson_put_string(ctx, root, "Map Resource", mapname);
char* encoded = sjson_stringify(ctx, root, " ");
FileWriter* fil = WriteSavegameChunk("info.json");
if (!fil)
{
sjson_destroy_context(ctx);
return;
}
fil->Write(encoded, strlen(encoded));
sjson_free_string(ctx, encoded);
sjson_destroy_context(ctx);
SaveStatistics();
SECRET_Save();
}
//=============================================================================
//
//
//
//=============================================================================
static bool CheckSingleFile (const char *name, bool &printRequires, bool printwarn)
{
if (name == NULL)
{
return true;
}
if (fileSystem.CheckIfResourceFileLoaded(name) < 0)
{
if (printwarn)
{
if (!printRequires)
{
Printf ("%s:\n%s", GStrings("TXT_SAVEGAMENEEDS"), name);
}
else
{
Printf (", %s", name);
}
}
printRequires = true;
return false;
}
return true;
}
//=============================================================================
//
// Return false if not all the needed wads have been loaded.
//
//=============================================================================
bool G_CheckSaveGameWads (sjson_node* root, bool printwarn)
{
bool printRequires = false;
auto text = sjson_get_string(root, "Game Resource", "");
CheckSingleFile (text, printRequires, printwarn);
text = sjson_get_string(root, "MAP Resource", "");
CheckSingleFile (text, printRequires, printwarn);
if (printRequires)
{
if (printwarn)
{
Printf ("\n");
}
return false;
}
return true;
}
//=============================================================================
//
// Checks if the savegame is valid. Gets a reader to the included info.json
// Returns 1 if valid, 0 if invalid and -1 if old and -2 if content missing
//
//=============================================================================
int G_ValidateSavegame(FileReader &fr, FString *savetitle)
{
auto data = fr.ReadPadded(1);
sjson_context* ctx = sjson_create_context(0, 0, NULL);
if (ctx)
{
sjson_node* root = sjson_decode(ctx, (const char*)data.Data());
int savever = sjson_get_int(root, "Save Version", -1);
FString engine = sjson_get_string(root, "Engine", "");
FString gamegrp = sjson_get_string(root, "Game Resource", "");
FString title = sjson_get_string(root, "Title", "");
auto savesig = gi->GetSaveSig();
sjson_destroy_context(ctx);
if (savetitle) *savetitle = title;
if (engine.Compare(savesig.savesig) != 0 || savever > savesig.currentsavever)
{
// different engine or newer version:
// not our business. Leave it alone.
return 0;
}
if (savever < savesig.minsavever)
{
// old, incompatible savegame. List as not usable.
return -1;
}
else if (gamegrp.CompareNoCase(fileSystem.GetResourceFileName(1)) == 0)
{
return G_CheckSaveGameWads(root, false)? 0 : -2;
}
else
{
// different game. Skip this.
return 0;
}
}
}
//=============================================================================
//
//
//
//=============================================================================
FString G_BuildSaveName (const char *prefix)
{
FString name = M_GetSavegamesPath();
size_t len = name.Len();
if (name[0] != '\0' && name[len-1] != '\\' && name[len-1] != '/')
{
name << "/";
}
name << prefix;
if (!strchr(prefix, '.')) name << SAVEGAME_EXT; // only add an extension if the prefix doesn't have one already.
name = NicePath(name);
name.Substitute("\\", "/");
CreatePath(name);
return name;
}

View file

@ -10,3 +10,13 @@ FileReader ReadSavegameChunk(const char *name);
bool FinishSavegameWrite();
void FinishSavegameRead();
// Savegame utilities
class FileReader;
FString G_BuildSaveName (const char *prefix);
bool G_CheckSaveGameWads (struct sjson_node* root, bool printwarn);
int G_ValidateSavegame(FileReader &fr, FString *savetitle);
void G_WriteSaveHeader(const char *name, const char*mapname, const char *title);
#define SAVEGAME_EXT ".dsave"

View file

@ -41,11 +41,11 @@ const char *GetVersionString();
/** Lots of different version numbers **/
#define VERSIONSTR "0.0.1"
#define VERSIONSTR "0.1.0"
// The version as seen in the Windows resource
#define RC_FILEVERSION 0,0,1,0
#define RC_PRODUCTVERSION 0,0,1,0
#define RC_FILEVERSION 0,1,0,0
#define RC_PRODUCTVERSION 0,1,0,0
#define RC_PRODUCTVERSION2 VERSIONSTR
// These are for content versioning.
#define VER_MAJOR 0
@ -59,6 +59,21 @@ const char *GetVersionString();
#define FORUM_URL "http://forum.zdoom.org/"
//#define BUGS_FORUM_URL "http://forum.zdoom.org/viewforum.php?f=2"
#define SAVESIG_DN3D "Demolition.Duke"
#define SAVESIG_BLD "Demolition.Blood"
#define SAVESIG_RR "Demolition.Redneck"
#define SAVESIG_SW "Demolition.SW"
#define MINSAVEVER_DN3D 1
#define MINSAVEVER_BLD 1
#define MINSAVEVER_RR 1
#define MINSAVEVER_SW 1
#define SAVEVER_DN3D 1
#define SAVEVER_BLD 1
#define SAVEVER_RR 1
#define SAVEVER_SW 1
#if defined(__APPLE__) || defined(_WIN32)
#define GAME_DIR GAMENAME
#else

View file

@ -36,6 +36,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "c_bind.h"
#include "menu/menu.h"
#include "gstrings.h"
#include "version.h"
#include "../../glbackend/glbackend.h"
BEGIN_DUKE_NS
@ -465,6 +466,12 @@ void GameInterface::StartGame(FGameStartup& gs)
}
FSavegameInfo GameInterface::GetSaveSig()
{
return { SAVESIG_DN3D, MINSAVEVER_DN3D, SAVEVER_DN3D };
}
END_DUKE_NS
static TMenuClassDescriptor<Duke::MainMenu> _mm("Duke.MainMenu");

View file

@ -163,6 +163,7 @@ struct GameInterface : ::GameInterface
bool CanSave() override;
void CustomMenuSelection(int menu, int item) override;
void StartGame(FGameStartup& gs) override;
FSavegameInfo GetSaveSig() override;
};

View file

@ -5497,7 +5497,7 @@ static void G_FreeHashAnim(const char * /*string*/, intptr_t key)
static void G_Cleanup(void)
{
ReadSaveGameHeaders(); // for culling
//ReadSaveGameHeaders(); // for culling
int32_t i;
@ -6189,8 +6189,6 @@ int GameInterface::app_main()
Menu_Init();
}
ReadSaveGameHeaders();
FX_StopAllSounds();
S_ClearSoundLocks();

View file

@ -2543,31 +2543,6 @@ static void Menu_PreDraw(MenuID_t cm, MenuEntry_t *entry, const vec2_t origin)
}
static void Menu_ReadSaveGameHeaders();
static void Menu_LoadReadHeaders()
{
Menu_ReadSaveGameHeaders();
for (int i = 0; i < g_nummenusaves; ++i)
{
menusave_t const & msv = g_menusaves[i];
// MenuEntry_LookDisabledOnCondition(&ME_LOAD[i], msv.isOldVer && msv.brief.isExt);
MenuEntry_DisableOnCondition(&ME_LOAD[i], msv.isOldVer && !msv.brief.isExt);
}
}
static void Menu_SaveReadHeaders()
{
Menu_ReadSaveGameHeaders();
for (int i = 0; i < g_nummenusaves; ++i)
{
menusave_t const & msv = g_menusaves[i];
MenuEntry_LookDisabledOnCondition(&ME_SAVE[i], msv.isOldVer && !msv.brief.isExt);
}
}
static void Menu_PreInput(MenuEntry_t *entry)
{
switch (g_currentMenu)
@ -2862,7 +2837,6 @@ static void Menu_EntryLinkActivate(MenuEntry_t *entry)
}
else if (entry == &ME_SAVESETUP_CLEANUP)
{
g_oldSaveCnt = G_CountOldSaves();
Menu_Change(MENU_SAVECLEANVERIFY);
}
else if (entry == &ME_NETHOST_LAUNCH)
@ -3247,15 +3221,13 @@ static void Menu_Verify(int32_t input)
case MENU_LOADDELVERIFY:
if (input)
{
G_DeleteSave(g_menusaves[M_LOAD.currentEntry].brief);
Menu_LoadReadHeaders();
Menu_LoadReadHeaders();
M_LOAD.currentEntry = clamp(M_LOAD.currentEntry, 0, (int32_t)g_nummenusaves-1);
}
break;
case MENU_SAVEDELVERIFY:
if (input)
{
G_DeleteSave(g_menusaves[M_SAVE.currentEntry-1].brief);
Menu_SaveReadHeaders();
M_SAVE.currentEntry = clamp(M_SAVE.currentEntry, 0, (int32_t)g_nummenusaves);
}
@ -3506,42 +3478,6 @@ static void Menu_FileSelect(int32_t input)
}
static void Menu_ReadSaveGameHeaders()
{
ReadSaveGameHeaders();
int const numloaditems = max<int>(g_nummenusaves, 1), numsaveitems = g_nummenusaves+1;
ME_LOAD = (MenuEntry_t *)Xrealloc(ME_LOAD, g_nummenusaves * sizeof(MenuEntry_t));
MEL_LOAD = (MenuEntry_t **)Xrealloc(MEL_LOAD, numloaditems * sizeof(MenuEntry_t *));
MEO_SAVE = (MenuString_t *)Xrealloc(MEO_SAVE, g_nummenusaves * sizeof(MenuString_t));
ME_SAVE = (MenuEntry_t *)Xrealloc(ME_SAVE, g_nummenusaves * sizeof(MenuEntry_t));
MEL_SAVE = (MenuEntry_t **)Xrealloc(MEL_SAVE, numsaveitems * sizeof(MenuEntry_t *));
MEL_SAVE[0] = &ME_SAVE_NEW;
ME_SAVE_NEW.name = s_NewSaveGame;
for (int i = 0; i < g_nummenusaves; ++i)
{
MEL_LOAD[i] = &ME_LOAD[i];
MEL_SAVE[i+1] = &ME_SAVE[i];
ME_LOAD[i] = ME_LOAD_TEMPLATE;
ME_SAVE[i] = ME_SAVE_TEMPLATE;
ME_SAVE[i].entry = &MEO_SAVE[i];
MEO_SAVE[i] = MEO_SAVE_TEMPLATE;
ME_LOAD[i].name = g_menusaves[i].brief.name;
MEO_SAVE[i].variable = g_menusaves[i].brief.name;
}
if (g_nummenusaves == 0)
MEL_LOAD[0] = &ME_LOAD_EMPTY;
M_LOAD.entrylist = MEL_LOAD;
M_LOAD.numEntries = numloaditems;
M_SAVE.entrylist = MEL_SAVE;
M_SAVE.numEntries = numsaveitems;
// lexicographical sorting?
}
static void Menu_AboutToStartDisplaying(Menu_t * m)
{

View file

@ -913,12 +913,6 @@ static int osdcmd_kickban(osdcmdptr_t parm)
}
#endif
static int osdcmd_purgesaves(osdcmdptr_t UNUSED(parm))
{
UNREFERENCED_CONST_PARAMETER(parm);
G_DeleteOldSaves();
return OSDCMD_OK;
}
static int osdcmd_printtimes(osdcmdptr_t UNUSED(parm))
{
@ -1025,8 +1019,6 @@ int32_t registerosdcommands(void)
OSD_RegisterFunction("printtimes", "printtimes: prints VM timing statistics", osdcmd_printtimes);
OSD_RegisterFunction("purgesaves", "purgesaves: deletes obsolete and unreadable save files", osdcmd_purgesaves);
OSD_RegisterFunction("quicksave","quicksave: performs a quick save", osdcmd_quicksave);
OSD_RegisterFunction("quickload","quickload: performs a quick load", osdcmd_quickload);

View file

@ -30,8 +30,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "i_specialpaths.h"
#include "gamecontrol.h"
#include "version.h"
#include "statistics.h"
#include "secrets.h"
#include "savegamehelp.h"
#include "menu/menu.h"
@ -147,11 +145,6 @@ int32_t g_lastAutoSaveArbitraryID = -1;
bool g_saveRequested;
savebrief_t * g_quickload;
menusave_t * g_menusaves;
uint16_t g_nummenusaves;
static menusave_t * g_internalsaves;
static uint16_t g_numinternalsaves;
static FileReader *OpenSavegame(const char *fn)
{
@ -159,12 +152,17 @@ static FileReader *OpenSavegame(const char *fn)
{
return nullptr;
}
auto file = ReadSavegameChunk("DEMOLITION_ED");
auto file = ReadSavegameChunk("info.json");
if (!file.isOpen())
{
FinishSavegameRead();
return nullptr;
}
if (G_ValidateSavegame(file, nullptr) <= 0)
{
FinishSavegameRead();
return nullptr;
}
file = ReadSavegameChunk("snapshot.dat");
if (!file.isOpen())
{
@ -174,145 +172,6 @@ static FileReader *OpenSavegame(const char *fn)
return new FileReader(std::move(file));
}
static void ReadSaveGameHeaders_CACHE1D(TArray<FString> &saves)
{
savehead_t h;
for (FString &save : saves)
{
auto fil = OpenSavegame(save);
if (!fil)
continue;
menusave_t & msv = g_internalsaves[g_numinternalsaves];
msv.brief.isExt = 0;
int32_t k = sv_loadheader(*fil, 0, &h);
delete fil;
if (k)
{
if (k < 0)
msv.isUnreadable = 1;
else
{
if (FURY)
{
auto extfil = ReadSavegameChunk("ext.json");
if (extfil.isOpen())
{
msv.brief.isExt = 1;
}
}
}
msv.isOldVer = 1;
}
else
msv.isOldVer = 0;
msv.isAutoSave = h.isAutoSave();
strncpy(msv.brief.path, save.GetChars(), ARRAY_SIZE(msv.brief.path));
++g_numinternalsaves;
if (k >= 0 && h.savename[0] != '\0')
{
memcpy(msv.brief.name, h.savename, ARRAY_SIZE(msv.brief.name));
}
else
msv.isUnreadable = 1;
}
FinishSavegameRead();
}
static void ReadSaveGameHeaders_Internal(void)
{
FString pattern = M_GetSavegamesPath() + "*.bsv";
TArray<FString> saves;
D_AddWildFile(saves, pattern);
// potentially overallocating but programmatically simple
int const numfiles = saves.Size();
size_t const internalsavesize = sizeof(menusave_t) * numfiles;
g_internalsaves = (menusave_t *)Xrealloc(g_internalsaves, internalsavesize);
for (int x = 0; x < numfiles; ++x)
g_internalsaves[x].clear();
g_numinternalsaves = 0;
ReadSaveGameHeaders_CACHE1D(saves);
g_nummenusaves = 0;
for (int x = g_numinternalsaves-1; x >= 0; --x)
{
menusave_t & msv = g_internalsaves[x];
if (!msv.isUnreadable)
{
++g_nummenusaves;
}
}
size_t const menusavesize = sizeof(menusave_t) * g_nummenusaves;
g_menusaves = (menusave_t *)Xrealloc(g_menusaves, menusavesize);
for (int x = 0; x < g_nummenusaves; ++x)
g_menusaves[x].clear();
for (int x = g_numinternalsaves-1, y = 0; x >= 0; --x)
{
menusave_t & msv = g_internalsaves[x];
if (!msv.isUnreadable)
{
g_menusaves[y++] = msv;
}
}
for (int x = g_numinternalsaves-1; x >= 0; --x)
{
char const * const path = g_internalsaves[x].brief.path;
int const pathlen = Bstrlen(path);
if (pathlen < 12)
continue;
char const * const fn = path + (pathlen-12);
if (fn[0] == 's' && fn[1] == 'a' && fn[2] == 'v' && fn[3] == 'e' &&
isdigit(fn[4]) && isdigit(fn[5]) && isdigit(fn[6]) && isdigit(fn[7]))
{
char number[5];
memcpy(number, fn+4, 4);
number[4] = '\0';
savecounter.count = Batoi(number)+1;
break;
}
}
}
void ReadSaveGameHeaders(void)
{
ReadSaveGameHeaders_Internal();
if (!cl_autosavedeletion)
return;
bool didDelete = false;
int numautosaves = 0;
for (int x = 0; x < g_nummenusaves; ++x)
{
menusave_t & msv = g_menusaves[x];
if (!msv.isAutoSave)
continue;
if (numautosaves >= cl_maxautosaves)
{
G_DeleteSave(msv.brief);
didDelete = true;
}
++numautosaves;
}
if (didDelete)
ReadSaveGameHeaders_Internal();
}
int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
{
FileReader ssfil;
@ -681,7 +540,7 @@ int32_t G_LoadPlayer(savebrief_t & sv)
if (status == 2)
G_NewGame_EnterLevel();
else if ((status = sv_loadsnapshot(*fil, 0, &h)) || !ReadStatistics() || !SECRET_Load()) // read the rest...
else if ((status = sv_loadsnapshot(*fil, 0, &h))) // read the rest...
{
// in theory, we could load into an initial dump first and trivially
// recover if things go wrong...
@ -722,49 +581,6 @@ static void G_RestoreTimers(void)
//////////
void G_DeleteSave(savebrief_t const & sv)
{
if (!sv.isValid())
return;
char temp[BMAX_PATH];
if (snprintf(temp, sizeof(temp), "%s%s", M_GetSavegamesPath().GetChars(), sv.path))
{
OSD_Printf("G_SavePlayer: file name \"%s\" too long\n", sv.path);
return;
}
remove(temp);
}
void G_DeleteOldSaves(void)
{
ReadSaveGameHeaders();
for (int x = 0; x < g_numinternalsaves; ++x)
{
menusave_t const & msv = g_internalsaves[x];
if (msv.isOldVer || msv.isUnreadable)
G_DeleteSave(msv.brief);
}
}
uint16_t G_CountOldSaves(void)
{
ReadSaveGameHeaders();
int bad = 0;
for (int x = 0; x < g_numinternalsaves; ++x)
{
menusave_t const & msv = g_internalsaves[x];
if (msv.isOldVer || msv.isUnreadable)
++bad;
}
return bad;
}
int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
{
#ifdef __ANDROID__
@ -783,29 +599,28 @@ int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
if (sv.isValid())
{
fn.Format("%s%s", M_GetSavegamesPath().GetChars(), sv.path);
fn = G_BuildSaveName(sv.path);
OpenSaveGameForWrite(fn);
fil = WriteSavegameChunk("snapshot.dat");
}
else
{
static char const SaveName[] = "save0000.bsv";
fn.Format("%s%s", M_GetSavegamesPath().GetChars(), SaveName);
fn = G_BuildSaveName("save0000");
auto fnp = fn.LockBuffer();
char* zeros = fnp + (fn.Len() - 8);
fil = savecounter.opennextfile(fnp, zeros);
char* zeros = strstr(fnp, "0000");
fil = savecounter.opennextfile(fnp, zeros); // fixme: Rewrite this so that it won't create the file.
fn.UnlockBuffer();
if (fil)
{
delete fil;
remove(fnp);
OpenSaveGameForWrite(fnp);
remove(fn);
OpenSaveGameForWrite(fn);
fil = WriteSavegameChunk("snapshot.dat");
}
fn.UnlockBuffer();
savecounter.count++;
// don't copy the mod dir into sv.path
Bstrcpy(sv.path, fn + (fn.Len() - (ARRAY_SIZE(SaveName) - 1)));
// don't copy the mod dir into sv.path (G_BuildSaveName guarantees the presence of a slash.)
Bstrcpy(sv.path, strrchr(fn, '/') + 1);
}
if (!fil)
@ -821,7 +636,6 @@ int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
}
else
{
WriteSavegameChunk("DEMOLITION_ED");
auto& fw = *fil;
sv.isExt = 0;
@ -835,8 +649,7 @@ int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
// SAVE!
sv_saveandmakesnapshot(fw, sv.name, 0, 0, 0, 0, isAutoSave);
SaveStatistics();
SECRET_Save();
fw.Close();
FinishSavegameWrite();
@ -1712,6 +1525,8 @@ int32_t sv_saveandmakesnapshot(FileWriter &fil, char const *name, int8_t spot, i
Bstrncpyz(h.savename, name, sizeof(h.savename));
auto fw = WriteSavegameChunk("header.dat");
fw->Write(&h, sizeof(savehead_t));
G_WriteSaveHeader(name, currentboardfilename, g_mapInfo[(MAXLEVELS * ud.volume_number) + ud.level_number].name);
}
else
{

View file

@ -114,8 +114,6 @@ extern int32_t g_lastAutoSaveArbitraryID;
extern bool g_saveRequested;
extern savebrief_t * g_quickload;
extern menusave_t * g_menusaves;
extern uint16_t g_nummenusaves;
int32_t sv_updatestate(int32_t frominit);
int32_t sv_readdiff(FileReader& fil);
@ -124,9 +122,6 @@ int32_t sv_loadheader(FileReader &fil, int32_t spot, savehead_t *h);
int32_t sv_loadsnapshot(FileReader &fil, int32_t spot, savehead_t *h);
int32_t sv_saveandmakesnapshot(FileWriter &fil, char const *name, int8_t spot, int8_t recdiffsp, int8_t diffcompress, int8_t synccompress, bool isAutoSave = false);
void sv_freemem();
void G_DeleteSave(savebrief_t const & sv);
void G_DeleteOldSaves(void);
uint16_t G_CountOldSaves(void);
int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave);
int32_t G_LoadPlayer(savebrief_t & sv);
int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh);

View file

@ -158,6 +158,7 @@ struct GameInterface : ::GameInterface
bool mouseInactiveConditional(bool condition) override;
FString statFPS() override;
GameStats getStats() override;
FSavegameInfo GetSaveSig() override;
};
END_RR_NS

View file

@ -31,6 +31,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "cheats.h"
#include "gamecvars.h"
#include "menu/menu.h"
#include "version.h"
#include "../../glbackend/glbackend.h"
BEGIN_RR_NS
@ -3446,7 +3447,6 @@ static void Menu_EntryLinkActivate(MenuEntry_t *entry)
}
else if (entry == &ME_SAVESETUP_CLEANUP)
{
g_oldSaveCnt = G_CountOldSaves();
Menu_Change(MENU_SAVECLEANVERIFY);
}
else if (entry == &ME_COLCORR_RESET)
@ -3768,10 +3768,6 @@ static void Menu_Verify(int32_t input)
switch (g_currentMenu)
{
case MENU_SAVECLEANVERIFY:
if (input)
{
G_DeleteOldSaves();
}
break;
case MENU_RESETPLAYER:
@ -3839,7 +3835,7 @@ static void Menu_Verify(int32_t input)
case MENU_LOADDELVERIFY:
if (input)
{
G_DeleteSave(g_menusaves[M_LOAD.currentEntry].brief);
//G_DeleteSave(g_menusaves[M_LOAD.currentEntry].brief);
Menu_LoadReadHeaders();
M_LOAD.currentEntry = clamp(M_LOAD.currentEntry, 0, (int32_t)g_nummenusaves-1);
}
@ -3847,7 +3843,7 @@ static void Menu_Verify(int32_t input)
case MENU_SAVEDELVERIFY:
if (input)
{
G_DeleteSave(g_menusaves[M_SAVE.currentEntry-1].brief);
//G_DeleteSave(g_menusaves[M_SAVE.currentEntry-1].brief);
Menu_SaveReadHeaders();
M_SAVE.currentEntry = clamp(M_SAVE.currentEntry, 0, (int32_t)g_nummenusaves);
}
@ -7462,4 +7458,9 @@ bool GameInterface::mouseInactiveConditional(bool condition)
return MOUSEINACTIVECONDITIONAL(condition);
}
FSavegameInfo GameInterface::GetSaveSig()
{
return { SAVESIG_RR, MINSAVEVER_RR, SAVEVER_RR };
}
END_RR_NS

View file

@ -791,13 +791,6 @@ static int osdcmd_kickban(osdcmdptr_t parm)
#endif
#endif
static int osdcmd_purgesaves(osdcmdptr_t UNUSED(parm))
{
UNREFERENCED_CONST_PARAMETER(parm);
G_DeleteOldSaves();
return OSDCMD_OK;
}
static int osdcmd_printtimes(osdcmdptr_t UNUSED(parm))
{
UNREFERENCED_CONST_PARAMETER(parm);
@ -881,8 +874,6 @@ int32_t registerosdcommands(void)
OSD_RegisterFunction("printtimes", "printtimes: prints VM timing statistics", osdcmd_printtimes);
OSD_RegisterFunction("purgesaves", "purgesaves: deletes obsolete and unreadable save files", osdcmd_purgesaves);
OSD_RegisterFunction("quicksave","quicksave: performs a quick save", osdcmd_quicksave);
OSD_RegisterFunction("quickload","quickload: performs a quick load", osdcmd_quickload);

View file

@ -28,8 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "i_specialpaths.h"
#include "gamecontrol.h"
#include "version.h"
#include "statistics.h"
#include "secrets.h"
#include "savegamehelp.h"
BEGIN_RR_NS
@ -155,12 +154,17 @@ static FileReader *OpenSavegame(const char *fn)
{
return nullptr;
}
auto file = ReadSavegameChunk("DEMOLITION_RN");
auto file = ReadSavegameChunk("info.json");
if (!file.isOpen())
{
FinishSavegameRead();
return nullptr;
}
if (G_ValidateSavegame(file, nullptr) <= 0)
{
FinishSavegameRead();
return nullptr;
}
file = ReadSavegameChunk("snapshot.dat");
if (!file.isOpen())
{
@ -170,130 +174,9 @@ static FileReader *OpenSavegame(const char *fn)
return new FileReader(std::move(file));
}
static void ReadSaveGameHeaders_CACHE1D(TArray<FString>& saves)
{
savehead_t h;
for (FString &save : saves)
{
auto fil = OpenSavegame(save);
if (!fil)
continue;
menusave_t & msv = g_internalsaves[g_numinternalsaves];
int32_t k = sv_loadheader(*fil, 0, &h);
delete fil;
if (k)
{
if (k < 0)
msv.isUnreadable = 1;
msv.isOldVer = 1;
}
else
msv.isOldVer = 0;
msv.isAutoSave = h.isAutoSave();
strncpy(msv.brief.path, save.GetChars(), ARRAY_SIZE(msv.brief.path));
++g_numinternalsaves;
if (k >= 0 && h.savename[0] != '\0')
{
memcpy(msv.brief.name, h.savename, ARRAY_SIZE(msv.brief.name));
}
else
msv.isUnreadable = 1;
}
FinishSavegameRead();
}
static void ReadSaveGameHeaders_Internal(void)
{
FString pattern = M_GetSavegamesPath() + "*.bsv";
TArray<FString> saves;
D_AddWildFile(saves, pattern);
// potentially overallocating but programmatically simple
int const numfiles = saves.Size();
size_t const internalsavesize = sizeof(menusave_t) * numfiles;
g_internalsaves = (menusave_t *)Xrealloc(g_internalsaves, internalsavesize);
for (int x = 0; x < numfiles; ++x)
g_internalsaves[x].clear();
g_numinternalsaves = 0;
ReadSaveGameHeaders_CACHE1D(saves);
g_nummenusaves = 0;
for (int x = g_numinternalsaves-1; x >= 0; --x)
{
menusave_t & msv = g_internalsaves[x];
if (!msv.isUnreadable)
{
++g_nummenusaves;
}
}
size_t const menusavesize = sizeof(menusave_t) * g_nummenusaves;
g_menusaves = (menusave_t *)Xrealloc(g_menusaves, menusavesize);
for (int x = 0; x < g_nummenusaves; ++x)
g_menusaves[x].clear();
for (int x = g_numinternalsaves-1, y = 0; x >= 0; --x)
{
menusave_t & msv = g_internalsaves[x];
if (!msv.isUnreadable)
{
g_menusaves[y++] = msv;
}
}
for (int x = g_numinternalsaves-1; x >= 0; --x)
{
char const * const path = g_internalsaves[x].brief.path;
int const pathlen = Bstrlen(path);
if (pathlen < 12)
continue;
char const * const fn = path + (pathlen-12);
if (fn[0] == 's' && fn[1] == 'a' && fn[2] == 'v' && fn[3] == 'e' &&
isdigit(fn[4]) && isdigit(fn[5]) && isdigit(fn[6]) && isdigit(fn[7]))
{
char number[5];
memcpy(number, fn+4, 4);
number[4] = '\0';
savecounter.count = Batoi(number)+1;
break;
}
}
}
void ReadSaveGameHeaders(void)
{
ReadSaveGameHeaders_Internal();
if (!cl_autosavedeletion)
return;
bool didDelete = false;
int numautosaves = 0;
for (int x = 0; x < g_nummenusaves; ++x)
{
menusave_t & msv = g_menusaves[x];
if (!msv.isAutoSave)
continue;
if (numautosaves >= cl_maxautosaves)
{
G_DeleteSave(msv.brief);
didDelete = true;
}
++numautosaves;
}
if (didDelete)
ReadSaveGameHeaders_Internal();
}
int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
@ -415,7 +298,7 @@ int32_t G_LoadPlayer(savebrief_t & sv)
if (status == 2)
G_NewGame_EnterLevel();
else if ((status = sv_loadsnapshot(*fil, 0, &h)) || !ReadStatistics() || !SECRET_Load()) // read the rest...
else if ((status = sv_loadsnapshot(*fil, 0, &h))) // read the rest...
{
// in theory, we could load into an initial dump first and trivially
// recover if things go wrong...
@ -455,52 +338,6 @@ static void G_RestoreTimers(void)
lockclock = g_timers.lockclock;
}
//////////
void G_DeleteSave(savebrief_t const & sv)
{
if (!sv.isValid())
return;
char temp[BMAX_PATH];
if (snprintf(temp, sizeof(temp), "%s%s", M_GetSavegamesPath().GetChars(), sv.path))
{
OSD_Printf("G_SavePlayer: file name \"%s\" too long\n", sv.path);
return;
}
remove(temp);
}
void G_DeleteOldSaves(void)
{
ReadSaveGameHeaders();
for (int x = 0; x < g_numinternalsaves; ++x)
{
menusave_t const & msv = g_internalsaves[x];
if (msv.isOldVer || msv.isUnreadable)
G_DeleteSave(msv.brief);
}
}
uint16_t G_CountOldSaves(void)
{
ReadSaveGameHeaders();
int bad = 0;
for (int x = 0; x < g_numinternalsaves; ++x)
{
menusave_t const & msv = g_internalsaves[x];
if (msv.isOldVer || msv.isUnreadable)
++bad;
}
return bad;
}
int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
{
#ifdef __ANDROID__
@ -519,29 +356,28 @@ int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
if (sv.isValid())
{
fn.Format("%s%s", M_GetSavegamesPath().GetChars(), sv.path);
fn = G_BuildSaveName(sv.path);
OpenSaveGameForWrite(fn);
fil = WriteSavegameChunk("snapshot.dat");
}
else
{
static char const SaveName[] = "save0000.bsv";
fn.Format("%s%s", M_GetSavegamesPath().GetChars(), SaveName);
fn = G_BuildSaveName("save0000");
auto fnp = fn.LockBuffer();
char* zeros = fnp + (fn.Len() - 8);
fil = savecounter.opennextfile(fnp, zeros);
char* zeros = strstr(fnp, "0000");
fil = savecounter.opennextfile(fnp, zeros); // fixme: Rewrite this so that it won't create the file.
fn.UnlockBuffer();
if (fil)
{
delete fil;
remove(fnp);
OpenSaveGameForWrite(fnp);
remove(fn);
OpenSaveGameForWrite(fn);
fil = WriteSavegameChunk("snapshot.dat");
}
fn.UnlockBuffer();
savecounter.count++;
// don't copy the mod dir into sv.path
Bstrcpy(sv.path, fn + (fn.Len() - (ARRAY_SIZE(SaveName) - 1)));
// don't copy the mod dir into sv.path (G_BuildSaveName guarantees the presence of a slash.)
Bstrcpy(sv.path, strrchr(fn, '/') + 1);
}
if (!fil)
@ -567,8 +403,7 @@ int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
// SAVE!
sv_saveandmakesnapshot(fw, sv.name, 0, 0, 0, 0, isAutoSave);
SaveStatistics();
SECRET_Save();
fw.Close();
FinishSavegameWrite();
@ -1385,6 +1220,8 @@ int32_t sv_saveandmakesnapshot(FileWriter &fil, char const *name, int8_t spot, i
Bstrncpyz(h.savename, name, sizeof(h.savename));
auto fw = WriteSavegameChunk("header.dat");
fw->Write(&h, sizeof(savehead_t));
G_WriteSaveHeader(name, currentboardfilename, g_mapInfo[(MAXLEVELS * ud.volume_number) + ud.level_number].name);
}
else
{

View file

@ -117,9 +117,6 @@ int32_t sv_loadheader(FileReader &fil, int32_t spot, savehead_t *h);
int32_t sv_loadsnapshot(FileReader &fil, int32_t spot, savehead_t *h);
int32_t sv_saveandmakesnapshot(FileWriter &fil, char const *name, int8_t spot, int8_t recdiffsp, int8_t diffcompress, int8_t synccompress, bool isAutoSave = false);
void sv_freemem();
void G_DeleteSave(savebrief_t const & sv);
void G_DeleteOldSaves(void);
uint16_t G_CountOldSaves(void);
int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave);
int32_t G_LoadPlayer(savebrief_t & sv);
int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh);

View file

@ -2382,6 +2382,7 @@ struct GameInterface : ::GameInterface
void set_hud_layout(int size) override;
void set_hud_scale(int size) override;
bool mouseInactiveConditional(bool condition) override;
FSavegameInfo GetSaveSig() override;
};

View file

@ -53,6 +53,7 @@ Prepared for public release: 03/28/2005 - Charlie Wiederhold, 3D Realms
#include "fx_man.h"
#include "music.h"
#include "text.h"
#include "version.h"
#include "colormap.h"
#include "config.h"
@ -4737,5 +4738,10 @@ void ResetPalette(PLAYERp pp)
// vim:ts=4:sw=4:enc=utf-8:
FSavegameInfo GameInterface::GetSaveSig()
{
return { SAVESIG_SW, MINSAVEVER_SW, SAVEVER_SW };
}
END_SW_NS

View file

@ -245,6 +245,15 @@ int SaveGame(short save_num)
OrgTileP otp, next_otp;
Saveable_Init();
#if 0 // A lot of work is needed here... (Thank God for all the macros around the load/save functions. :) )
FStringf base("save%04d", save_num);
auto game_name = G_BuildSaveName(base);
OpenSaveGameForWrite(game_name);
G_WriteSaveHeader(SaveGameDescr[save_num], LevelInfo[Level].LevelName, LevelInfo[Level].Description);
auto fil = WriteSavegameChunk("snapshot.sw");
#endif
snprintf(game_name, 256, "%sgame%d.sav", M_GetSavegamesPath().GetChars(), save_num);
if ((fil = MOPEN_WRITE(game_name)) == MOPEN_WRITE_ERR)