Make custom stats fully compatible between different versions, so new fields can be added in future without save corruptions

This commit is contained in:
Indev 2025-01-19 02:28:16 +03:00 committed by NepDisk
parent c8e78711a6
commit 1922870773
2 changed files with 94 additions and 14 deletions

View file

@ -2,6 +2,7 @@
#include "doomstat.h"
#include "g_game.h"
#include "byteptr.h"
#include "z_zone.h"
kartstats_t kartstats = {0};
@ -65,6 +66,8 @@ void K_StatRound(void)
if (demo.playback)
return;
kartstats.somenewfield = 42;
int numplayers = 0;
for (int i = 0; i < MAXPLAYERS; ++i)
@ -87,23 +90,18 @@ void K_EraseStats(void)
// The only field we want to remember
boolean vanilla = kartstats.vanilla;
if (kartstats.copy)
Z_Free(kartstats.copy);
memset(&kartstats, 0, sizeof(kartstats_t));
kartstats.vanilla = vanilla;
}
void K_ReadStats(savebuffer_t *save, boolean vanilla)
static void K_ReadStatsCustom(savebuffer_t *save)
{
memset(&kartstats, 0, sizeof(kartstats_t));
kartstats.vanilla = vanilla;
kartstats.totalplaytime = READUINT32(save->p);
kartstats.matchesplayed = READUINT32(save->p);
// Vanilla only has those 2
if (vanilla)
return;
kartstats.size = READUINT32(save->p);
kartstats.version = READUINT32(save->p);
// So many similar-looking read's... scary...
kartstats.raplaytime = READUINT32(save->p);
@ -119,17 +117,51 @@ void K_ReadStats(savebuffer_t *save, boolean vanilla)
kartstats.sinks = READUINT32(save->p);
kartstats.sinked = READUINT32(save->p);
kartstats.respawns = READUINT32(save->p);
// If kartstats gets updated, uncomment this and read next fields after this early return. Do same on next updates,
// this way even data is read on different versions, it doesn't get corrupted (as long as fields aren't removed, which shouldn't happen)
//if (kartstats.version < 2)
// return;
//
//kartstats.somenewfield = READUINT32(save->p);
}
void K_WriteStats(savebuffer_t *save, boolean vanilla)
void K_ReadStats(savebuffer_t *save, boolean vanilla)
{
WRITEUINT32(save->p, kartstats.totalplaytime);
WRITEUINT32(save->p, kartstats.matchesplayed);
// Basically, free old kartstats.copy if needed and memset kartstats with zeros
K_EraseStats();
kartstats.vanilla = vanilla;
kartstats.totalplaytime = READUINT32(save->p);
kartstats.matchesplayed = READUINT32(save->p);
// Vanilla only has those 2
if (vanilla)
return;
// Save where block of custom stats starts
UINT8 *customblockstart = save->p;
K_ReadStatsCustom(save);
// Make a copy of entire custom stats block, so unread values will still be written back
kartstats.copy = Z_Malloc(kartstats.size, PU_STATIC, NULL);
memcpy(kartstats.copy, customblockstart, kartstats.size);
// If there were extra fields we couldn't read, skip them
save->p = customblockstart + kartstats.size;
}
static void K_WriteStatsCustom(savebuffer_t *save)
{
// Update, if needed
kartstats.version = max(kartstats.version, KARTSTATSVERSION);
WRITEUINT32(save->p, kartstats.size); // Note: if we will end up writing more that stored in there, this field in save file would be updated
WRITEUINT32(save->p, kartstats.version);
// Version 1
WRITEUINT32(save->p, kartstats.raplaytime);
WRITEUINT32(save->p, kartstats.onlineplaytime);
WRITEUINT32(save->p, kartstats.raceplaytime);
@ -143,4 +175,42 @@ void K_WriteStats(savebuffer_t *save, boolean vanilla)
WRITEUINT32(save->p, kartstats.sinks);
WRITEUINT32(save->p, kartstats.sinked);
WRITEUINT32(save->p, kartstats.respawns);
// No need for early returns, but please mark each new block of write's with version it corresponds to :3
// Version 2
//WRITEUINT32(save->p, kartstats.somenewfield);
}
void K_WriteStats(savebuffer_t *save, boolean vanilla)
{
WRITEUINT32(save->p, kartstats.totalplaytime);
WRITEUINT32(save->p, kartstats.matchesplayed);
// Vanilla only has those 2
if (vanilla)
return;
// Now need to be careful... We save start of custom stats block
UINT8 *customblockstart = save->p;
// Write everything we know how to write
K_WriteStatsCustom(save);
// Calculate size of data that we wrote
UINT32 size = (UINT32)(save->p - customblockstart);
// Now, if we wrote less data than save originally had, that means our copy that we kept in K_ReadStats
// has stats from newer version that we need to append
if (size < kartstats.size)
{
I_Assert(kartstats.copy != NULL); // If kartstats.copy is null, kartstats.size **always** should be 0, this can only happen if we create new save
WRITEMEM(save->p, kartstats.copy + size, kartstats.size - size);
}
else if (size > kartstats.size) // No need to do anything if we wrote exactly same amount...
{
// ...but, if we wrote more, we need to update size field in header. **It's always located at start of block**
// So we can just:
WRITEUINT32(customblockstart, size);
}
}

View file

@ -5,6 +5,8 @@
#include "d_player.h"
#include "p_saveg.h"
#define KARTSTATSVERSION 1
typedef struct kartstats_s {
// Remember if stats we loaded are vanilla ones or not
boolean vanilla;
@ -13,6 +15,12 @@ typedef struct kartstats_s {
tic_t totalplaytime;
UINT32 matchesplayed;
// Custom data, fields go in same order as they are written into game save
// IMPORTANT: size **must be first**, so K_WriteStats can overwrite it (not that we're gonna change order, but just in case)
UINT32 size; // Size required for our stats. If older version reads save from newer one, it will just skip unread fields, avoiding reading corrupted values
UINT32 version; // Version, so we know which fields we actually can read
tic_t raplaytime;
tic_t onlineplaytime;
tic_t raceplaytime;
@ -26,6 +34,8 @@ typedef struct kartstats_s {
UINT32 sinks; // Times hitting *others* by kitchen sink
UINT32 sinked; // Times *being hit* by kitchen sink
UINT32 respawns;
UINT8 *copy; // Copy of stats block that has been read from file. In case we're older version that reads newer save, we will keep values that we couldn't read
} kartstats_t;
extern kartstats_t kartstats;