From 28677da5b917e26f21be43a08209397dc2ec314c Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 24 May 2023 18:17:48 +0100 Subject: [PATCH] Unloaded mapheader record tracking system In the previous entry in the series, due to level header records existing in a NUMMAPS-sized table always saved and loaded in full, Time Attack times persisted even across game loads without the relevant custom levels added. However, this was changed with the long map name system. Map records were assigned to level headers, which were only created on level load. This new system brings Ring Racers up to parity (or better, due to the reduced incidence of header conflicts!) - All levels currently loaded with records attached are written on gamedata save. - This reduces gamedata size for a player who has not unlocked every level! - On gamedata load, if a level is not loaded, store those extra records on a linked list. - On level header creation, check the linked list to see if an associated unloaded mapheader record exists. - If it does, write the record onto the map structure directly, and delete the "unloaded mapheader" storage struct! - Then on the NEXT gamedata save, in addition to all loaded mapheaders, it writes the extra records kept in long term storage in exactly the same format. --- src/deh_soc.c | 41 ++++++++++++++- src/doomstat.h | 12 +++++ src/g_game.c | 135 +++++++++++++++++++++++++++++++++++++++---------- src/typedef.h | 1 + 4 files changed, 159 insertions(+), 30 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index d5fb03cd3..8cdb8ba81 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -950,8 +950,45 @@ void readlevelheader(MYFILE *f, char * name) if (mapheaderinfo[num]->lumpname == NULL) { - mapheaderinfo[num]->lumpname = Z_StrDup(name); - mapheaderinfo[num]->lumpnamehash = quickncasehash(mapheaderinfo[num]->lumpname, MAXMAPLUMPNAME); + mapheaderinfo[num]->lumpnamehash = quickncasehash(name, MAXMAPLUMPNAME); + + // Check to see if we have any custom map record data that we could substitute in. + unloaded_mapheader_t *unloadedmap, *prev = NULL; + for (unloadedmap = unloadedmapheaders; unloadedmap; prev = unloadedmap, unloadedmap = unloadedmap->next) + { + if (unloadedmap->lumpnamehash != mapheaderinfo[num]->lumpnamehash) + continue; + + if (strcasecmp(name, unloadedmap->lumpname) != 0) + continue; + + // Copy in mapvisited, time, lap, etc. + M_Memcpy(&mapheaderinfo[num]->records, &unloadedmap->records, sizeof(recorddata_t)); + + // Reuse the zone-allocated lumpname string. + mapheaderinfo[num]->lumpname = unloadedmap->lumpname; + + // Remove this entry from the chain. + if (prev) + { + prev->next = unloadedmap->next; + } + else + { + unloadedmapheaders = unloadedmap->next; + } + + // Finally, free. + Z_Free(unloadedmap); + + break; + } + + if (mapheaderinfo[num]->lumpname == NULL) + { + // If there was no string to reuse, dup our own. + mapheaderinfo[num]->lumpname = Z_StrDup(name); + } } do diff --git a/src/doomstat.h b/src/doomstat.h index 4983442c2..b7c074a14 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -500,6 +500,18 @@ struct mapheader_t extern mapheader_t** mapheaderinfo; extern INT32 nummapheaders, mapallocsize; +struct unloaded_mapheader_t +{ + char *lumpname; + UINT32 lumpnamehash; + + recorddata_t records; + + unloaded_mapheader_t *next; +}; + +extern unloaded_mapheader_t *unloadedmapheaders; + // Gametypes #define NUMGAMETYPEFREESLOTS (128) #define MAXGAMETYPELENGTH (32) diff --git a/src/g_game.c b/src/g_game.c index b41da25d5..0a91da2ed 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -200,6 +200,8 @@ mapheader_t** mapheaderinfo = {NULL}; INT32 nummapheaders = 0; INT32 mapallocsize = 0; +unloaded_mapheader_t *unloadedmapheaders = NULL; + // Kart cup definitions cupheader_t *kartcupheaders = NULL; UINT16 numkartcupheaders = 0; @@ -448,13 +450,22 @@ INT32 player_name_changes[MAXPLAYERS]; void G_ClearRecords(void) { INT16 i; - cupheader_t *cup; for (i = 0; i < nummapheaders; ++i) { memset(&mapheaderinfo[i]->records, 0, sizeof(recorddata_t)); } + unloaded_mapheader_t *unloadedmap, *next = NULL; + for (unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = next) + { + next = unloadedmap->next; + Z_Free(unloadedmap->lumpname); + Z_Free(unloadedmap); + } + unloadedmapheaders = NULL; + + cupheader_t *cup; for (cup = kartcupheaders; cup; cup = cup->next) { memset(&cup->windata, 0, sizeof(cup->windata)); @@ -4920,43 +4931,54 @@ void G_LoadGameData(void) // Main records numgamedatamapheaders = READUINT32(save.p); - if (numgamedatamapheaders >= NEXTMAP_SPECIAL) - goto datacorrupt; for (i = 0; i < numgamedatamapheaders; i++) { char mapname[MAXMAPLUMPNAME]; INT16 mapnum; - tic_t rectime; - tic_t reclap; READSTRINGN(save.p, mapname, sizeof(mapname)); mapnum = G_MapNumber(mapname); + recorddata_t dummyrecord, *record = &dummyrecord; + rtemp = READUINT8(save.p); - rectime = (tic_t)READUINT32(save.p); - reclap = (tic_t)READUINT32(save.p); + + if (rtemp & ~MV_MAX) + goto datacorrupt; if (mapnum < nummapheaders && mapheaderinfo[mapnum]) { // Valid mapheader, time to populate with record data. - if ((mapheaderinfo[mapnum]->records.mapvisited = rtemp) & ~MV_MAX) - goto datacorrupt; - - if (rectime || reclap) - { - mapheaderinfo[i]->records.time = rectime; - mapheaderinfo[i]->records.lap = reclap; - //CONS_Printf("ID %d, Time = %d, Lap = %d\n", i, rectime/35, reclap/35); - } + record = &mapheaderinfo[mapnum]->records; } - else + else if (rtemp) // Mapvisited will never be 0 for a map with a record. { - // Since it's not worth declaring the entire gamedata - // corrupt over extra maps, we report and move on. - CONS_Alert(CONS_WARNING, "Map with lumpname %s does not exist, time record data will be discarded\n", mapname); + // Invalid, but we don't want to lose all the juicy statistics. + // Instead, update a FILO linked list of "unloaded mapheaders". + + unloaded_mapheader_t *unloadedmap = + Z_Malloc( + sizeof(unloaded_mapheader_t), + PU_STATIC, NULL + ); + + // Establish properties, for later retrieval on file add. + unloadedmap->lumpname = Z_StrDup(mapname); + unloadedmap->lumpnamehash = quickncasehash(mapname, MAXMAPLUMPNAME); + + // Insert at the head, just because it's convenient. + unloadedmap->next = unloadedmapheaders; + unloadedmapheaders = unloadedmap; + + // Finally, set the pointer to write into. + record = &unloadedmap->records; } + + record->mapvisited = rtemp; + record->time = (tic_t)READUINT32(save.p); + record->lap = (tic_t)READUINT32(save.p); } if (versionMinor > 1) @@ -5095,7 +5117,34 @@ void G_SaveGameData(void) { length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; } - length += 4 + (nummapheaders * (MAXMAPLUMPNAME+1+4+4)); + + UINT32 numgamedatamapheaders = 0; + unloaded_mapheader_t *unloadedmap; + + for (i = 0; i < nummapheaders; i++) + { + // It's safe to assume a level with no mapvisited will have no other data worth keeping, since you get MV_VISITED just for opening it. + if (!(mapheaderinfo[i]->records.mapvisited & MV_MAX)) + { + continue; + } + + numgamedatamapheaders++; + } + + for (unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = unloadedmap->next) + { + // Ditto, with the exception that we should warn about it. + if (!(unloadedmap->records.mapvisited & MV_MAX)) + { + CONS_Alert(CONS_WARNING, "Unloaded map \"%s\" has no mapvisited!\n", unloadedmap->lumpname); + continue; + } + + numgamedatamapheaders++; + } + + length += 4 + (numgamedatamapheaders * (MAXMAPLUMPNAME+1+4+4)); numcups = 0; for (cup = kartcupheaders; cup; cup = cup->next) @@ -5193,17 +5242,47 @@ void G_SaveGameData(void) WRITEUINT32(save.p, gamedata->timesBeaten); // 4 // Main records - WRITEUINT32(save.p, nummapheaders); // 4 + WRITEUINT32(save.p, numgamedatamapheaders); // 4 - for (i = 0; i < nummapheaders; i++) // nummapheaders * (255+1+4+4) { - // For figuring out which header to assign it to on load - WRITESTRINGN(save.p, mapheaderinfo[i]->lumpname, MAXMAPLUMPNAME); + // numgamedatamapheaders * (MAXMAPLUMPNAME+1+4+4) - WRITEUINT8(save.p, (mapheaderinfo[i]->records.mapvisited & MV_MAX)); + UINT32 writtengamedatamapheaders = 0; - WRITEUINT32(save.p, mapheaderinfo[i]->records.time); - WRITEUINT32(save.p, mapheaderinfo[i]->records.lap); + for (i = 0; i < nummapheaders; i++) + { + if (!(mapheaderinfo[i]->records.mapvisited & MV_MAX)) + continue; + + WRITESTRINGN(save.p, mapheaderinfo[i]->lumpname, MAXMAPLUMPNAME); + + WRITEUINT8(save.p, (mapheaderinfo[i]->records.mapvisited & MV_MAX)); + + WRITEUINT32(save.p, mapheaderinfo[i]->records.time); + WRITEUINT32(save.p, mapheaderinfo[i]->records.lap); + + if (++writtengamedatamapheaders >= numgamedatamapheaders) + break; + } + + if (writtengamedatamapheaders < numgamedatamapheaders) + { + for (unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = unloadedmap->next) + { + if (!(unloadedmap->records.mapvisited & MV_MAX)) + continue; + + WRITESTRINGN(save.p, unloadedmap->lumpname, MAXMAPLUMPNAME); + + WRITEUINT8(save.p, (unloadedmap->records.mapvisited & MV_MAX)); + + WRITEUINT32(save.p, unloadedmap->records.time); + WRITEUINT32(save.p, unloadedmap->records.lap); + + if (++writtengamedatamapheaders >= numgamedatamapheaders) + break; + } + } } WRITEUINT32(save.p, numcups); // 4 diff --git a/src/typedef.h b/src/typedef.h index 00eb492a0..afb3aba00 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -125,6 +125,7 @@ TYPEDEF (customoption_t); TYPEDEF (gametype_t); TYPEDEF (staffbrief_t); TYPEDEF (mapheader_t); +TYPEDEF (unloaded_mapheader_t); TYPEDEF (tolinfo_t); TYPEDEF (cupheader_t);