Rewrite how Spray Cans are stored in gamedata

- For programmers:
    - Deprecate GamedataSprayCanJson
        - Previously stored colour name and map name together.
        - Was swapped in place to move invalid entries to the back.
        - If the old info exists, we convert it.
    - Instead:
        - Store list of colour names
        - Index into that list in GamedataMapJson to write map ID
        - Stable-sort the list as collected then uncollected
        - Write only valid entries into gamedata_t
        - Use the map ID reference to link map back to final order
    - Sounds more complicated, and it kind of is - but the code is WAY more readable, elegant IMO, allows for expansions to be added later and takes advantage of CPP features it didn't originally
- For testers:
    - Ideally, nothing should change. Just be careful and remember to keep backups of your gamedata

# Conflicts:
#	src/g_gamedata.cpp
#	src/g_gamedata.h
This commit is contained in:
toaster 2025-02-24 15:51:41 +00:00
parent 3f9c0685eb
commit 1fa1da9b4e
2 changed files with 124 additions and 81 deletions

View file

@ -29,7 +29,9 @@
namespace fs = std::filesystem;
#define GD_VERSION_MAJOR (0xBA5ED321)
#define GD_VERSION_MINOR (1)
#define GD_VERSION_MINOR (2)
#define GD_MINIMUM_SPRAYCANSV2 (2)
void srb2::save_ng_gamedata()
{
@ -146,6 +148,18 @@ void srb2::save_ng_gamedata()
ng.skins[name] = std::move(skin);
}
for (int i = 0; i < gamedata->numspraycans; i++)
{
uint16_t col = gamedata->spraycans[i].col;
if (col >= SKINCOLOR_FIRSTFREESLOT)
{
col = SKINCOLOR_NONE;
}
ng.spraycans_v2.emplace_back(String(skincolors[col].name));
}
auto maptojson = [](recorddata_t *records)
{
srb2::GamedataMapJson map {};
@ -167,6 +181,7 @@ void srb2::save_ng_gamedata()
map.stats.time.custom = records->modetimeplayed[GDGT_CUSTOM];
map.stats.time.timeattack = records->timeattacktimeplayed;
map.stats.time.spbattack = records->spbattacktimeplayed;
map.spraycan = records->spraycan;
return map;
};
@ -183,38 +198,6 @@ void srb2::save_ng_gamedata()
srb2::String lumpname { unloadedmap->lumpname };
ng.maps[lumpname] = std::move(map);
}
for (int i = 0; i < gamedata->numspraycans; i++)
{
srb2::GamedataSprayCanJson spraycan {};
candata_t* can = &gamedata->spraycans[i];
if (can->col >= numskincolors)
{
continue;
}
spraycan.color = String(skincolors[can->col].name);
if (can->map == NEXTMAP_INVALID)
{
spraycan.map = "";
ng.spraycans.emplace_back(std::move(spraycan));
continue;
}
if (can->map >= nummapheaders)
{
continue;
}
mapheader_t* mapheader = mapheaderinfo[can->map];
if (!mapheader)
{
continue;
}
spraycan.map = String(mapheader->lumpname);
ng.spraycans.emplace_back(std::move(spraycan));
}
auto cuptojson = [](cupwindata_t *windata)
{
@ -401,6 +384,7 @@ void srb2::load_ng_gamedata()
uint32_t majorversion;
uint8_t minorversion;
uint8_t dirty;
bool converted = false;
try
{
majorversion = srb2::io::read_uint32(bis);
@ -591,6 +575,29 @@ void srb2::load_ng_gamedata()
}
}
std::vector<candata_t> tempcans;
for (auto& cancolor : js.spraycans_v2)
{
// Version 2 behaviour - spraycans_v2, not spraycans!
candata_t tempcan;
tempcan.col = SKINCOLOR_NONE;
tempcan.map = NEXTMAP_INVALID;
// Find the skin color index for the name
for (size_t i = 0; i < SKINCOLOR_FIRSTFREESLOT; i++)
{
if (cancolor != skincolors[i].name)
continue;
tempcan.col = i;
break;
}
tempcans.emplace_back(std::move(tempcan));
}
for (auto& mappair : js.maps)
{
UINT16 mapnum = G_MapNumber(mappair.first.c_str());
@ -614,10 +621,21 @@ void srb2::load_ng_gamedata()
dummyrecord.timeattacktimeplayed = mappair.second.stats.time.timeattack;
dummyrecord.spbattacktimeplayed = mappair.second.stats.time.spbattack;
dummyrecord.spraycan = (minorversion >= GD_MINIMUM_SPRAYCANSV2)
? mappair.second.spraycan
: UINT16_MAX;
if (mapnum < nummapheaders && mapheaderinfo[mapnum])
{
// Valid mapheader, time to populate with record data.
// Infill Spray Can info
if (dummyrecord.spraycan < tempcans.size())
{
tempcans[dummyrecord.spraycan].map = mapnum;
}
dummyrecord.spraycan = UINT16_MAX; // We repopulate this later.
mapheaderinfo[mapnum]->records = dummyrecord;
}
else if (dummyrecord.mapvisited & MV_BEATEN
@ -641,73 +659,95 @@ void srb2::load_ng_gamedata()
unloadedmap->next = unloadedmapheaders;
unloadedmapheaders = unloadedmap;
// Invalidate can.
dummyrecord.spraycan = UINT16_MAX;
// Finally, copy into.
unloadedmap->records = dummyrecord;
}
}
gamedata->gotspraycans = 0;
gamedata->numspraycans = js.spraycans.size();
if ((minorversion < GD_MINIMUM_SPRAYCANSV2) && (js.spraycans.size() > 1))
{
// Deprecated behaviour! Look above for spraycans_v2 handling
converted = true;
for (auto& deprecatedcan : js.spraycans)
{
candata_t tempcan;
tempcan.col = SKINCOLOR_NONE;
// Find the skin color index for the name
for (size_t i = 0; i < SKINCOLOR_FIRSTFREESLOT; i++)
{
if (deprecatedcan.color != skincolors[i].name)
continue;
tempcan.col = i;
break;
}
UINT16 mapnum = NEXTMAP_INVALID;
if (!deprecatedcan.map.empty())
{
mapnum = G_MapNumber(deprecatedcan.map.c_str());
}
tempcan.map = mapnum;
tempcans.emplace_back(std::move(tempcan));
}
}
{
// Post-process of Spray Cans - a component of both v1 and v2 spraycans behaviour
// Determine sizes.
for (auto& tempcan : tempcans)
{
if (tempcan.col == SKINCOLOR_NONE)
continue;
gamedata->numspraycans++;
if (tempcan.map >= nummapheaders)
continue;
gamedata->gotspraycans++;
}
if (gamedata->numspraycans)
{
// Arrange with collected first
std::stable_sort(tempcans.begin(), tempcans.end(), [ ]( auto& lhs, auto& rhs )
{
return (rhs.map >= basenummapheaders && lhs.map < basenummapheaders);
});
gamedata->spraycans = static_cast<candata_t*>(Z_Malloc(
(gamedata->numspraycans * sizeof(candata_t)),
PU_STATIC, NULL));
for (size_t i = 0; i < gamedata->numspraycans; i++)
// Finally, fill can data.
size_t i = 0;
for (auto& tempcan : tempcans)
{
auto& can = js.spraycans[i];
if (tempcan.col == SKINCOLOR_NONE)
continue;
skincolors[tempcan.col].cache_spraycan = i;
if (tempcan.map < nummapheaders)
mapheaderinfo[tempcan.map]->records.spraycan = i;
gamedata->spraycans[i] = tempcan;
if (++i < gamedata->numspraycans)
continue;
// Find the skin color index for the name
bool foundcolor = false;
for (size_t j = 0; j < numskincolors; j++)
{
if (can.color == skincolors[j].name)
{
gamedata->spraycans[i].col = j;
skincolors[j].cache_spraycan = i;
foundcolor = true;
break;
}
}
if (!foundcolor)
{
// Invalid color name? Ignore the spraycan
gamedata->numspraycans -= 1;
i -= 1;
continue;
}
gamedata->spraycans[i].map = NEXTMAP_INVALID;
UINT16 mapnum = NEXTMAP_INVALID;
if (!can.map.empty())
{
mapnum = G_MapNumber(can.map.c_str());
}
gamedata->spraycans[i].map = mapnum;
if (mapnum >= nummapheaders)
{
// Can has not been grabbed on any map, this is intentional.
continue;
}
if (gamedata->gotspraycans != i)
{
//CONS_Printf("LOAD - Swapping gotten can %u, color %s with prior ungotten can %u\n", i, skincolors[col].name, gamedata->gotspraycans);
// All grabbed cans should be at the head of the list.
// Let's swap with the can the disjoint occoured at.
// This will prevent a gap from occouring on reload.
candata_t copycan = gamedata->spraycans[gamedata->gotspraycans];
gamedata->spraycans[gamedata->gotspraycans] = gamedata->spraycans[i];
gamedata->spraycans[i] = copycan;
mapheaderinfo[copycan.map]->records.spraycan = i;
}
mapheaderinfo[mapnum]->records.spraycan = gamedata->gotspraycans;
gamedata->gotspraycans++;
}
}
for (auto& cuppair : js.cups)
@ -812,7 +852,6 @@ void srb2::load_ng_gamedata()
}
}
bool converted = false;
UINT32 chao_key_rounds = GDCONVERT_ROUNDSTOKEY;
UINT32 start_keys = GDINIT_CHAOKEYS;

View file

@ -243,10 +243,12 @@ struct GamedataMapJson final
{
GamedataMapVisitedJson visited;
GamedataMapStatsJson stats;
uint16_t spraycan;
SRB2_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataMapJson, visited, stats)
SRB2_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataMapJson, visited, stats, spraycan)
};
// Deprecated
struct GamedataSprayCanJson final
{
String map;
@ -296,8 +298,9 @@ struct GamedataJson final
GamedataChallengeGridJson challengegrid;
uint32_t timesBeaten;
HashMap<String, GamedataSkinJson> skins;
Vector<String> spraycans_v2;
HashMap<String, GamedataMapJson> maps;
Vector<GamedataSprayCanJson> spraycans;
Vector<GamedataSprayCanJson> spraycans; // Deprecated
HashMap<String, GamedataCupJson> cups;
Vector<GamedataSealedSwapJson> sealedswaps;
@ -317,6 +320,7 @@ struct GamedataJson final
challengegrid,
timesBeaten,
skins,
spraycans_v2,
maps,
spraycans,
cups,