From b0348526cd9487136c7e99e4b41c861efb640483 Mon Sep 17 00:00:00 2001 From: Eidolon Date: Sat, 17 Feb 2024 23:58:03 -0600 Subject: [PATCH] Rewrite gamedata format --- src/CMakeLists.txt | 1 + src/g_game.c | 1099 +------------------------------------------- src/g_gamedata.cpp | 688 +++++++++++++++++++++++++++ src/g_gamedata.h | 229 +++++++++ src/io/streams.cpp | 156 +++++++ src/io/streams.hpp | 171 +++++++ 6 files changed, 1250 insertions(+), 1094 deletions(-) create mode 100644 src/g_gamedata.cpp create mode 100644 src/g_gamedata.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d48f2b506..bdaa9bec1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 g_build_ticcmd.cpp g_demo.c g_game.c + g_gamedata.cpp g_input.c g_party.cpp am_map.c diff --git a/src/g_game.c b/src/g_game.c index 3e69e344a..b4bf60bb5 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -73,6 +73,7 @@ #include "k_roulette.h" #include "k_objects.h" #include "k_credits.h" +#include "g_gamedata.h" #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -4708,652 +4709,6 @@ void G_LoadGameSettings(void) Color_cons_t[MAXSKINCOLORS].strvalue = Followercolor_cons_t[MAXSKINCOLORS+2].strvalue = NULL; } -#define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 10 // Change every format update - -// You can't rearrange these without a special format update -typedef enum -{ - GDEVER_ADDON = 1, - GDEVER_CREDITS = 1<<1, - GDEVER_REPLAY = 1<<2, - GDEVER_SPECIAL = 1<<3, - GDEVER_KEYTUTORIAL = 1<<4, - GDEVER_KEYMAJORSKIP = 1<<5, - GDEVER_TUTORIALSKIP = 1<<6, - GDEVER_ENTERTUTSKIP = 1<<7, - // --- Free up to 1<<23 --- // - GDEVER_GONERSHIFT = 24, // nothing above this -} gdeverdone_t; - -static const char *G_GameDataFolder(void) -{ - if (strcmp(srb2home,".")) - return srb2home; - else - return "the Ring Racers folder"; -} - -// G_LoadGameData -// Loads the main data file, which stores information such as emblems found, etc. -void G_LoadGameData(void) -{ - UINT32 i, j; - UINT32 versionID; - UINT8 versionMinor; - UINT8 rtemp; - boolean gridunusable = false; - savebuffer_t save = {0}; - - UINT16 emblemreadcount = MAXEMBLEMS; - UINT16 unlockreadcount = MAXUNLOCKABLES; - UINT16 conditionreadcount = MAXCONDITIONSETS; - size_t unlockreadsize = sizeof(UINT16); - - //For records - UINT32 numgamedataskins; - UINT32 numgamedatamapheaders; - UINT32 numgamedatacups; - - // Stop saving, until we successfully load it again. - gamedata->loaded = false; - - // Clear things so previously read gamedata doesn't transfer - // to new gamedata - // see also M_EraseDataResponse - G_ClearRecords(); // records - M_ClearStats(); // statistics - M_ClearSecrets(); // emblems, unlocks, maps visited, etc - - if (M_CheckParm("-nodata")) - { - // Don't load at all. - // The following used to be in M_ClearSecrets, but that was silly. - M_UpdateUnlockablesAndExtraEmblems(false, true); - return; - } - - if (M_CheckParm("-resetdata")) - { - // Don't load, but do save. (essentially, reset) - goto finalisegamedata; - } - - if (P_SaveBufferFromFile(&save, va(pandf, srb2home, gamedatafilename)) == false) - { - // No gamedata. We can save a new one. - goto finalisegamedata; - } - - // Version check - versionID = READUINT32(save.p); - if (versionID != GD_VERSIONCHECK) - { - const char *gdfolder = G_GameDataFolder(); - - P_SaveBufferFree(&save); - I_Error("Game data is not for Ring Racers v2.0.\nDelete %s (maybe in %s) and try again.", gamedatafilename, gdfolder); - } - - versionMinor = READUINT8(save.p); - if (versionMinor > GD_VERSIONMINOR) - { - const char *gdfolder = G_GameDataFolder(); - - P_SaveBufferFree(&save); - I_Error("Game data is from the future! (expected %d, got %d)\nRename or delete %s (maybe in %s) and try again.", GD_VERSIONMINOR, versionMinor, gamedatafilename, gdfolder); - } - else if (versionMinor < GD_VERSIONMINOR) - { - // We're converting - let'd create a backup. - FIL_WriteFile(va("%s" PATHSEP "%s.bak", srb2home, gamedatafilename), save.buffer, save.size); - } - - if ((versionMinor <= 6) -#ifdef DEVELOP - || M_CheckParm("-resetchallengegrid") -#endif - ) - { - gridunusable = true; - } - - if (versionMinor > 1) - { - gamedata->evercrashed = (boolean)READUINT8(save.p); - } - - gamedata->totalplaytime = READUINT32(save.p); - - if (versionMinor > 1) - { - gamedata->totalrings = READUINT32(save.p); - - if (versionMinor >= 9) - { - gamedata->totaltumbletime = READUINT32(save.p); - } - - for (i = 0; i < GDGT_MAX; i++) - { - gamedata->roundsplayed[i] = READUINT32(save.p); - } - - gamedata->pendingkeyrounds = READUINT32(save.p); - gamedata->pendingkeyroundoffset = READUINT8(save.p); - - if (versionMinor < 3) - { - gamedata->keyspending = READUINT8(save.p); - } - else - { - gamedata->keyspending = READUINT16(save.p); - } - - // Sanity check. - if (gamedata->pendingkeyroundoffset >= GDCONVERT_ROUNDSTOKEY) - { - gamedata->pendingkeyrounds += - (gamedata->pendingkeyroundoffset - - (GDCONVERT_ROUNDSTOKEY-1)); - gamedata->pendingkeyroundoffset = (GDCONVERT_ROUNDSTOKEY-1); - gamedata->keyspending = 0; // safe to nuke - will be recalc'd if the offset still permits - } - - gamedata->chaokeys = READUINT16(save.p); - - if (versionMinor >= 4) - { - UINT32 everflags = READUINT32(save.p); - - gamedata->everloadedaddon = !!(everflags & GDEVER_ADDON); - gamedata->everfinishedcredits = !!(everflags & GDEVER_CREDITS); - gamedata->eversavedreplay = !!(everflags & GDEVER_REPLAY); - gamedata->everseenspecial = !!(everflags & GDEVER_SPECIAL); - gamedata->chaokeytutorial = !!(everflags & GDEVER_KEYTUTORIAL); - gamedata->majorkeyskipattempted = !!(everflags & GDEVER_KEYMAJORSKIP); - gamedata->finishedtutorialchallenge = !!(everflags & GDEVER_TUTORIALSKIP); - gamedata->enteredtutorialchallenge = !!(everflags & GDEVER_ENTERTUTSKIP); - - gamedata->gonerlevel = everflags>>GDEVER_GONERSHIFT; - } - else - { - gamedata->everloadedaddon = (boolean)READUINT8(save.p); - gamedata->eversavedreplay = (boolean)READUINT8(save.p); - gamedata->everseenspecial = (boolean)READUINT8(save.p); - } - } - else - { - save.p += 4; // no direct equivalent to matchesplayed - } - - // Prison Egg Pickups - if (versionMinor >= 8) - { - gamedata->thisprisoneggpickup = READUINT16(save.p); - gamedata->prisoneggstothispickup = READUINT16(save.p); - } - - { - // Quick & dirty hash for what mod this save file is for. - UINT32 modID = READUINT32(save.p); - UINT32 expectedID = quickncasehash(timeattackfolder, 64); - - if (modID != expectedID) - { - // Aha! Someone's been screwing with the save file! - goto datacorrupt; - } - } - - if (versionMinor < 3) - { - emblemreadcount = 512; - unlockreadcount = conditionreadcount = UINT8_MAX; - unlockreadsize = sizeof(UINT8); - } - else if (versionMinor < 10) - { - emblemreadcount = 1024*2; - } - - // To save space, use one bit per collected/achieved/unlocked flag - for (i = 0; i < emblemreadcount;) - { - rtemp = READUINT8(save.p); - for (j = 0; j < 8 && j+i < emblemreadcount; ++j) - gamedata->collected[j+i] = ((rtemp >> j) & 1); - i += j; - } - for (i = 0; i < unlockreadcount;) - { - rtemp = READUINT8(save.p); - for (j = 0; j < 8 && j+i < unlockreadcount; ++j) - gamedata->unlocked[j+i] = ((rtemp >> j) & 1); - i += j; - } - for (i = 0; i < unlockreadcount;) - { - rtemp = READUINT8(save.p); - for (j = 0; j < 8 && j+i < unlockreadcount; ++j) - gamedata->unlockpending[j+i] = ((rtemp >> j) & 1); - i += j; - } - for (i = 0; i < conditionreadcount;) - { - rtemp = READUINT8(save.p); - for (j = 0; j < 8 && j+i < conditionreadcount; ++j) - gamedata->achieved[j+i] = ((rtemp >> j) & 1); - i += j; - } - - if (gridunusable) - { - UINT16 burn = READUINT16(save.p); // Previous challengegridwidth - UINT8 height = (versionMinor && versionMinor <= 6) ? 4 : CHALLENGEGRIDHEIGHT; - save.p += (burn * height * unlockreadsize); // Step over previous grid data - - gamedata->challengegridwidth = 0; - Z_Free(gamedata->challengegrid); - gamedata->challengegrid = NULL; - } - else - { - gamedata->challengegridwidth = READUINT16(save.p); - Z_Free(gamedata->challengegrid); - if (gamedata->challengegridwidth) - { - gamedata->challengegrid = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT16)), - PU_STATIC, NULL); - if (unlockreadsize == sizeof(UINT8)) - { - for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) - { - gamedata->challengegrid[i] = READUINT8(save.p); - if (gamedata->challengegrid[i] == unlockreadcount) - gamedata->challengegrid[i] = MAXUNLOCKABLES; - } - } - else - { - for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) - { - gamedata->challengegrid[i] = READUINT16(save.p); - } - } - - M_SanitiseChallengeGrid(); - } - else - { - gamedata->challengegrid = NULL; - } - } - - gamedata->timesBeaten = READUINT32(save.p); - - // Main records - - skinreference_t *tempskinreferences = NULL; - - if (versionMinor < 3) - { - gamedata->importprofilewins = true; - numgamedataskins = 0; - } - else - { - numgamedataskins = READUINT32(save.p); - - if (numgamedataskins) - { - tempskinreferences = Z_Malloc( - numgamedataskins * sizeof (skinreference_t), - PU_STATIC, - NULL - ); - - for (i = 0; i < numgamedataskins; i++) - { - char skinname[SKINNAMESIZE+1]; - INT32 skin; - - READSTRINGN(save.p, skinname, SKINNAMESIZE); - skin = R_SkinAvailable(skinname); - - skinrecord_t dummyrecord; - - dummyrecord.wins = READUINT32(save.p); - dummyrecord._saveid = i; - - tempskinreferences[i].id = MAXSKINS; - - if (skin != -1) - { - // We found a skin, so assign the win. - - M_Memcpy(&skins[skin].records, &dummyrecord, sizeof(skinrecord_t)); - - tempskinreferences[i].id = skin; - tempskinreferences[i].unloaded = NULL; - } - else if (dummyrecord.wins) - { - // Invalid, but we don't want to lose all the juicy statistics. - // Instead, update a FILO linked list of "unloaded skins". - - unloaded_skin_t *unloadedskin = - Z_Malloc( - sizeof(unloaded_skin_t), - PU_STATIC, NULL - ); - - // Establish properties, for later retrieval on file add. - strlcpy(unloadedskin->name, skinname, sizeof(unloadedskin->name)); - unloadedskin->namehash = quickncasehash(unloadedskin->name, SKINNAMESIZE); - - // Insert at the head, just because it's convenient. - unloadedskin->next = unloadedskins; - unloadedskins = unloadedskin; - - // Finally, copy into. - M_Memcpy(&unloadedskin->records, &dummyrecord, sizeof(skinrecord_t)); - - tempskinreferences[i].unloaded = unloadedskin; - } - } - } - } - - UINT16 *tempmapidreferences = NULL; - - numgamedatamapheaders = READUINT32(save.p); - - if (numgamedatamapheaders) - { - tempmapidreferences = Z_Malloc( - numgamedatamapheaders * sizeof (UINT16), - PU_STATIC, - NULL - ); - - for (i = 0; i < numgamedatamapheaders; i++) - { - char mapname[MAXMAPLUMPNAME]; - UINT16 mapnum; - - READSTRINGL(save.p, mapname, MAXMAPLUMPNAME); - mapnum = G_MapNumber(mapname); - - tempmapidreferences[i] = (UINT16)mapnum; - - recorddata_t dummyrecord; - - dummyrecord.mapvisited = READUINT8(save.p); - dummyrecord.time = (tic_t)READUINT32(save.p); - dummyrecord.lap = (tic_t)READUINT32(save.p); - - if (mapnum < nummapheaders && mapheaderinfo[mapnum]) - { - // Valid mapheader, time to populate with record data. - - dummyrecord.mapvisited &= MV_MAX; - M_Memcpy(&mapheaderinfo[mapnum]->records, &dummyrecord, sizeof(recorddata_t)); - } - else if ( - (dummyrecord.mapvisited & MV_BEATEN) - || dummyrecord.time != 0 - || dummyrecord.lap != 0 - ) - { - // 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(unloadedmap->lumpname, MAXMAPLUMPNAME); - - // Insert at the head, just because it's convenient. - unloadedmap->next = unloadedmapheaders; - unloadedmapheaders = unloadedmap; - - // Finally, copy into. - M_Memcpy(&unloadedmap->records, &dummyrecord, sizeof(recorddata_t)); - } - } - } - - if (versionMinor > 5) - { - gamedata->gotspraycans = 0; - gamedata->numspraycans = READUINT16(save.p); - Z_Free(gamedata->spraycans); - - if (gamedata->numspraycans) - { - gamedata->spraycans = Z_Malloc( - (gamedata->numspraycans * sizeof(candata_t)), - PU_STATIC, NULL); - - for (i = 0; i < gamedata->numspraycans; i++) - { - gamedata->spraycans[i].col = SKINCOLOR_NONE; - gamedata->spraycans[i].map = NEXTMAP_INVALID; - - UINT16 col = READUINT16(save.p); - UINT32 _saveid = READUINT32(save.p); - - if (col < SKINCOLOR_FIRSTFREESLOT) - { - gamedata->spraycans[i].col = col; - skincolors[col].cache_spraycan = i; - } - - if (_saveid >= numgamedatamapheaders) - { - // Can has not been grabbed on any map, this is intentional. - continue; - } - - UINT16 map = tempmapidreferences[_saveid]; - if (map >= nummapheaders || !mapheaderinfo[map]) - { - //CONS_Printf("LOAD - Can %u, color %s - id %u (unloaded header)\n", i, skincolors[col].name, _saveid); - continue; - } - - //CONS_Printf("LOAD - Can %u, color %s - id %u, map %d\n", i, skincolors[col].name, _saveid, map); - - gamedata->spraycans[i].map = map; - - 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]->cache_spraycan = i; - } - - mapheaderinfo[map]->cache_spraycan = gamedata->gotspraycans; - - gamedata->gotspraycans++; - } - } - else - { - gamedata->spraycans = NULL; - } - } - - if (versionMinor > 1) - { - numgamedatacups = READUINT32(save.p); - - for (i = 0; i < numgamedatacups; i++) - { - char cupname[MAXCUPNAME]; - cupheader_t *cup; - - cupwindata_t dummywindata[4]; - - // Find the relevant cup. - if (versionMinor < 5) - { - // Before this version cups were called things like RING. - // Now that example cup would be called RR_RING instead. - cupname[0] = cupname[1] = 'R'; - cupname[2] = '_'; - READSTRINGL(save.p, (cupname + 3), sizeof(cupname) - 3); - } - else - { - READSTRINGL(save.p, cupname, sizeof(cupname)); - } - - UINT32 hash = quickncasehash(cupname, MAXCUPNAME); - for (cup = kartcupheaders; cup; cup = cup->next) - { - if (cup->namehash != hash) - continue; - - if (strcmp(cup->name, cupname)) - continue; - - break; - } - - // Digest its data... - for (j = 0; j < KARTGP_MAX; j++) - { - rtemp = READUINT8(save.p); - - dummywindata[j].best_placement = (rtemp & 0x0F); - dummywindata[j].best_grade = (rtemp & 0x70)>>4; - dummywindata[j].got_emerald = !!(rtemp & 0x80); - - dummywindata[j].best_skin.id = MAXSKINS; - dummywindata[j].best_skin.unloaded = NULL; - if (versionMinor >= 3) - { - UINT32 _saveid = READUINT32(save.p); - if (_saveid < numgamedataskins) - { - M_Memcpy(&dummywindata[j].best_skin, &tempskinreferences[_saveid], sizeof(dummywindata[j].best_skin)); - } - } - } - - if (versionMinor < 3) - { - // We now require backfilling of placement information. - - cupwindata_t bestwindata; - bestwindata.best_placement = 0; - - j = KARTGP_MAX; - while (j > 0) - { - j--; - - if (bestwindata.best_placement == 0) - { - if (dummywindata[j].best_placement != 0) - { - M_Memcpy(&bestwindata, &dummywindata[j], sizeof(bestwindata)); - } - continue; - } - - if (dummywindata[j].best_placement != 0) - { - if (dummywindata[j].best_placement < bestwindata.best_placement) - bestwindata.best_placement = dummywindata[j].best_placement; - - if (dummywindata[j].best_grade > bestwindata.best_grade) - bestwindata.best_grade = dummywindata[j].best_grade; - - bestwindata.got_emerald |= dummywindata[j].got_emerald; - } - - M_Memcpy(&dummywindata[j], &bestwindata, sizeof(dummywindata[j])); - } - } - - if (cup) - { - // We found a cup, so assign the windata. - - M_Memcpy(&cup->windata, &dummywindata, sizeof(cup->windata)); - } - else if (dummywindata[0].best_placement != 0) - { - // Invalid, but we don't want to lose all the juicy statistics. - // Instead, update a FILO linked list of "unloaded cupheaders". - - unloaded_cupheader_t *unloadedcup = - Z_Malloc( - sizeof(unloaded_cupheader_t), - PU_STATIC, NULL - ); - - // Establish properties, for later retrieval on file add. - strlcpy(unloadedcup->name, cupname, sizeof(unloadedcup->name)); - unloadedcup->namehash = quickncasehash(unloadedcup->name, MAXCUPNAME); - - // Insert at the head, just because it's convenient. - unloadedcup->next = unloadedcupheaders; - unloadedcupheaders = unloadedcup; - - // Finally, copy into. - M_Memcpy(&unloadedcup->windata, &dummywindata, sizeof(unloadedcup->windata)); - } - } - } - - if (tempskinreferences) - Z_Free(tempskinreferences); - if (tempmapidreferences) - Z_Free(tempmapidreferences); - - // done - P_SaveBufferFree(&save); - - finalisegamedata: - { - M_FinaliseGameData(); - - return; - } - - // Landing point for corrupt gamedata - datacorrupt: - { - const char *gdfolder = "the Ring Racers folder"; - if (strcmp(srb2home,".")) - gdfolder = srb2home; - - P_SaveBufferFree(&save); - - I_Error("Corrupt game data file.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder); - } -} - // G_DirtyGameData // Modifies the gamedata as little as possible to maintain safety in a crash event, while still recording it. void G_DirtyGameData(void) @@ -5365,11 +4720,14 @@ void G_DirtyGameData(void) gamedata->evercrashed = true; //if (FIL_WriteFileOK(name)) - handle = fopen(va(pandf, srb2home, gamedatafilename), "r+"); + handle = fopen(va(pandf, srb2home, gamedatafilename), "r+b"); if (!handle) return; + // IO needs to be unbuffered too, so we try not to allocate anything. + setvbuf(handle, NULL, _IONBF, 0); + // Write a dirty byte immediately after the gamedata check + minor version. if (fseek(handle, 5, SEEK_SET) != -1) fwrite(&writebytesource, 1, 1, handle); @@ -5379,453 +4737,6 @@ void G_DirtyGameData(void) return; } -// G_SaveGameData -// Saves the main data file, which stores information such as emblems found, etc. -void G_SaveGameData(void) -{ - size_t length; - INT32 i, j; - cupheader_t *cup; - UINT8 btemp; - savebuffer_t save = {0}; - - if (gamedata == NULL || !gamedata->loaded) - return; // If never loaded (-nodata), don't save - - gamedata->deferredsave = false; - - if (usedCheats) - { -#ifdef DEVELOP - CONS_Alert(CONS_WARNING, M_GetText("Cheats used - Gamedata will not be saved.\n")); -#endif - return; - } - - length = (4+1+1+ - 4+4+4+ - (4*GDGT_MAX)+ - 4+1+2+2+ - 4+ - 2+2+ - 4+ - (MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+ - 4+2); - - if (gamedata->challengegrid) - { - length += (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT) * 2; - } - - - UINT32 numgamedataskins = 0; - unloaded_skin_t *unloadedskin; - - for (i = 0; i < numskins; i++) - { - // It's safe to assume a skin with no wins will have no other data worth keeping - if (skins[i].records.wins == 0) - { - continue; - } - - numgamedataskins++; - } - - for (unloadedskin = unloadedskins; unloadedskin; unloadedskin = unloadedskin->next) - { - // Ditto, with the exception that we should warn about it. - if (unloadedskin->records.wins == 0) - { - CONS_Alert(CONS_WARNING, "Unloaded skin \"%s\" has no wins!\n", unloadedskin->name); - continue; - } - - numgamedataskins++; - } - - length += 4 + (numgamedataskins * (SKINNAMESIZE+4)); - - - UINT32 numgamedatamapheaders = 0; - unloaded_mapheader_t *unloadedmap; - - for (i = 0; i < nummapheaders; i++) - { - // No spraycan attached. - if (mapheaderinfo[i]->cache_spraycan >= gamedata->numspraycans - // 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. - && !(mapheaderinfo[i]->records.mapvisited & MV_MAX)) - { - mapheaderinfo[i]->_saveid = UINT32_MAX; - continue; - } - - mapheaderinfo[i]->_saveid = numgamedatamapheaders; - 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; - } - - // It's far off on the horizon, beyond many memory limits, but prevent a potential misery moment of losing ALL your data. - if (++numgamedatamapheaders == UINT32_MAX) - { - CONS_Alert(CONS_WARNING, "Some unloaded map record data has been dropped due to datatype limitations.\n"); - break; - } - } - - length += 4 + (numgamedatamapheaders * (MAXMAPLUMPNAME+1+4+4)); - - length += 2; - - if (gamedata->numspraycans) - { - length += (gamedata->numspraycans * (2 + 4)); - } - - - UINT32 numgamedatacups = 0; - unloaded_cupheader_t *unloadedcup; - - for (cup = kartcupheaders; cup; cup = cup->next) - { - // Results are populated downwards, so no Easy win - // means there's no important player data to save. - if (cup->windata[0].best_placement == 0) - continue; - - numgamedatacups++; - } - - for (unloadedcup = unloadedcupheaders; unloadedcup; unloadedcup = unloadedcup->next) - { - // Ditto, with the exception that we should warn about it. - if (unloadedcup->windata[0].best_placement == 0) - { - CONS_Alert(CONS_WARNING, "Unloaded cup \"%s\" has no windata!\n", unloadedcup->name); - continue; - } - - // It's far off on the horizon, beyond many memory limits, but prevent a potential misery moment of losing ALL your data. - if (++numgamedatacups == UINT32_MAX) - { - CONS_Alert(CONS_WARNING, "Some unloaded cup standings data has been dropped due to datatype limitations.\n"); - break; - } - } - - length += 4 + (numgamedatacups * (MAXCUPNAME + 4*(1+4))); - - - if (P_SaveBufferAlloc(&save, length) == false) - { - CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n")); - return; - } - - // Version test - - WRITEUINT32(save.p, GD_VERSIONCHECK); // 4 - WRITEUINT8(save.p, GD_VERSIONMINOR); // 1 - - // Crash dirtiness - // cannot move, see G_DirtyGameData - WRITEUINT8(save.p, gamedata->evercrashed); // 1 - - // Statistics - - WRITEUINT32(save.p, gamedata->totalplaytime); // 4 - WRITEUINT32(save.p, gamedata->totalrings); // 4 - WRITEUINT32(save.p, gamedata->totaltumbletime); // 4 - - for (i = 0; i < GDGT_MAX; i++) // 4 * GDGT_MAX - { - WRITEUINT32(save.p, gamedata->roundsplayed[i]); - } - - WRITEUINT32(save.p, gamedata->pendingkeyrounds); // 4 - WRITEUINT8(save.p, gamedata->pendingkeyroundoffset); // 1 - WRITEUINT16(save.p, gamedata->keyspending); // 2 - WRITEUINT16(save.p, gamedata->chaokeys); // 2 - - { - UINT32 everflags = (gamedata->gonerlevel<everloadedaddon) - everflags |= GDEVER_ADDON; - if (gamedata->everfinishedcredits) - everflags |= GDEVER_CREDITS; - if (gamedata->eversavedreplay) - everflags |= GDEVER_REPLAY; - if (gamedata->everseenspecial) - everflags |= GDEVER_SPECIAL; - if (gamedata->chaokeytutorial) - everflags |= GDEVER_KEYTUTORIAL; - if (gamedata->majorkeyskipattempted) - everflags |= GDEVER_KEYMAJORSKIP; - if (gamedata->finishedtutorialchallenge) - everflags |= GDEVER_TUTORIALSKIP; - if (gamedata->enteredtutorialchallenge) - everflags |= GDEVER_ENTERTUTSKIP; - - WRITEUINT32(save.p, everflags); // 4 - } - - // Prison Egg Pickups - WRITEUINT16(save.p, gamedata->thisprisoneggpickup); // 2 - WRITEUINT16(save.p, gamedata->prisoneggstothispickup); // 2 - - WRITEUINT32(save.p, quickncasehash(timeattackfolder, 64)); // 4 - - // To save space, use one bit per collected/achieved/unlocked flag - for (i = 0; i < MAXEMBLEMS;) // MAXEMBLEMS * 1; - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j) - btemp |= (gamedata->collected[j+i] << j); - WRITEUINT8(save.p, btemp); - i += j; - } - - // MAXUNLOCKABLES * 2; - for (i = 0; i < MAXUNLOCKABLES;) - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) - btemp |= (gamedata->unlocked[j+i] << j); - WRITEUINT8(save.p, btemp); - i += j; - } - for (i = 0; i < MAXUNLOCKABLES;) - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j) - btemp |= (gamedata->unlockpending[j+i] << j); - WRITEUINT8(save.p, btemp); - i += j; - } - - for (i = 0; i < MAXCONDITIONSETS;) // MAXCONDITIONSETS * 1; - { - btemp = 0; - for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j) - btemp |= (gamedata->achieved[j+i] << j); - WRITEUINT8(save.p, btemp); - i += j; - } - - if (gamedata->challengegrid) // 2 + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT) * 2 - { - WRITEUINT16(save.p, gamedata->challengegridwidth); - for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) - { - WRITEUINT16(save.p, gamedata->challengegrid[i]); - } - } - else // 2 - { - WRITEUINT16(save.p, 0); - } - - WRITEUINT32(save.p, gamedata->timesBeaten); // 4 - - // Main records - - WRITEUINT32(save.p, numgamedataskins); // 4 - - { - // numgamedataskins * (SKINNAMESIZE+4) - - UINT32 maxid = 0; - - for (i = 0; i < numskins; i++) - { - if (skins[i].records.wins == 0) - { - skins[i].records._saveid = UINT32_MAX; - continue; - } - - WRITESTRINGN(save.p, skins[i].name, SKINNAMESIZE); - - WRITEUINT32(save.p, skins[i].records.wins); - - skins[i].records._saveid = maxid; - if (++maxid == numgamedataskins) - break; - } - - if (maxid < numgamedataskins) - { - for (unloadedskin = unloadedskins; unloadedskin; unloadedskin = unloadedskin->next) - { - if (unloadedskin->records.wins == 0) - continue; - - WRITESTRINGN(save.p, unloadedskin->name, SKINNAMESIZE); - - WRITEUINT32(save.p, unloadedskin->records.wins); - - unloadedskin->records._saveid = maxid; - if (++maxid == numgamedataskins) - break; - } - } - } - -#define GETSKINREFSAVEID(ref, var) \ - { \ - if (ref.unloaded != NULL) \ - var = ref.unloaded->records._saveid;\ - else if (ref.id < numskins)\ - var = skins[ref.id].records._saveid; \ - else \ - var = UINT32_MAX; \ - } - - WRITEUINT32(save.p, numgamedatamapheaders); // 4 - - if (numgamedatamapheaders) - { - // numgamedatamapheaders * (MAXMAPLUMPNAME+1+4+4) - - for (i = 0; i < nummapheaders; i++) - { - if (mapheaderinfo[i]->cache_spraycan >= gamedata->numspraycans - && !(mapheaderinfo[i]->records.mapvisited & MV_MAX)) - continue; - - WRITESTRINGL(save.p, mapheaderinfo[i]->lumpname, MAXMAPLUMPNAME); - - UINT8 mapvisitedtemp = (mapheaderinfo[i]->records.mapvisited & MV_MAX); - - WRITEUINT8(save.p, mapvisitedtemp); - - WRITEUINT32(save.p, mapheaderinfo[i]->records.time); - WRITEUINT32(save.p, mapheaderinfo[i]->records.lap); - - if (--numgamedatamapheaders == 0) - break; - } - - if (numgamedatamapheaders) - { - for (unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = unloadedmap->next) - { - if (!(unloadedmap->records.mapvisited & MV_MAX)) - continue; - - WRITESTRINGL(save.p, unloadedmap->lumpname, MAXMAPLUMPNAME); - - WRITEUINT8(save.p, unloadedmap->records.mapvisited); - - WRITEUINT32(save.p, unloadedmap->records.time); - WRITEUINT32(save.p, unloadedmap->records.lap); - - if (--numgamedatamapheaders == 0) - break; - } - } - } - - WRITEUINT16(save.p, gamedata->numspraycans); // 2 - - // gamedata->numspraycans * (2 + 4) - - for (i = 0; i < gamedata->numspraycans; i++) - { - WRITEUINT16(save.p, gamedata->spraycans[i].col); - - UINT32 _saveid = UINT32_MAX; - - UINT16 map = gamedata->spraycans[i].map; - - if (map < nummapheaders && mapheaderinfo[map]) - { - _saveid = mapheaderinfo[map]->_saveid; - } - - //CONS_Printf("SAVE - Can %u, color %s - id %u, map %d\n", i, skincolors[gamedata->spraycans[i].col].name, _saveid, map); - - WRITEUINT32(save.p, _saveid); - } - - WRITEUINT32(save.p, numgamedatacups); // 4 - - if (numgamedatacups) - { - // numgamedatacups * (MAXCUPNAME + 4*(1+4)) - -#define WRITECUPWINDATA(maybeunloadedcup) \ - for (i = 0; i < KARTGP_MAX; i++) \ - { \ - btemp = min(maybeunloadedcup->windata[i].best_placement, 0x0F); \ - btemp |= (maybeunloadedcup->windata[i].best_grade<<4); \ - if (maybeunloadedcup->windata[i].got_emerald == true) \ - btemp |= 0x80; \ - \ - WRITEUINT8(save.p, btemp); \ - \ - GETSKINREFSAVEID(maybeunloadedcup->windata[i].best_skin, j); \ - \ - WRITEUINT32(save.p, j); \ - } - - for (cup = kartcupheaders; cup; cup = cup->next) - { - if (cup->windata[0].best_placement == 0) - continue; - - WRITESTRINGL(save.p, cup->name, MAXCUPNAME); - - WRITECUPWINDATA(cup); - - if (--numgamedatacups == 0) - break; - } - - if (numgamedatacups) - { - for (unloadedcup = unloadedcupheaders; unloadedcup; unloadedcup = unloadedcup->next) - { - if (unloadedcup->windata[0].best_placement == 0) - continue; - - WRITESTRINGL(save.p, unloadedcup->name, MAXCUPNAME); - - WRITECUPWINDATA(unloadedcup); - - if (--numgamedatacups == 0) - break; - } - } - -#undef WRITECUPWINDATA - } - -#undef GETSKINREFSAVEID - - length = save.p - save.buffer; - - FIL_WriteFile(va(pandf, srb2home, gamedatafilename), save.buffer, length); - P_SaveBufferFree(&save); - - // Also save profiles here. - PR_SaveProfiles(); - - #ifdef DEVELOP - CONS_Alert(CONS_NOTICE, M_GetText("Gamedata saved.\n")); - #endif -} - #define VERSIONSIZE 16 // diff --git a/src/g_gamedata.cpp b/src/g_gamedata.cpp new file mode 100644 index 000000000..a685742b2 --- /dev/null +++ b/src/g_gamedata.cpp @@ -0,0 +1,688 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "g_gamedata.h" + +#include +#include +#include +#include + +#include + +#include "io/streams.hpp" +#include "d_main.h" +#include "m_argv.h" +#include "m_cond.h" +#include "g_game.h" +#include "r_skins.h" +#include "z_zone.h" + +namespace fs = std::filesystem; +using json = nlohmann::json; + +void srb2::save_ng_gamedata() +{ + if (gamedata == NULL || !gamedata->loaded) + return; // If never loaded (-nodata), don't save + + gamedata->deferredsave = false; + + if (usedCheats) + { +#ifdef DEVELOP + CONS_Alert(CONS_WARNING, M_GetText("Cheats used - Gamedata will not be saved.\n")); +#endif + return; + } + + GamedataJson ng{}; + + ng.playtime.total = gamedata->totalplaytime; + ng.rings.total = gamedata->totalrings; + ng.playtime.tumble = gamedata->totaltumbletime; + ng.rounds.race = gamedata->roundsplayed[GDGT_RACE]; + ng.rounds.battle = gamedata->roundsplayed[GDGT_BATTLE]; + ng.rounds.prisons = gamedata->roundsplayed[GDGT_PRISONS]; + ng.rounds.special = gamedata->roundsplayed[GDGT_SPECIAL]; + ng.rounds.custom = gamedata->roundsplayed[GDGT_CUSTOM]; + ng.challengekeys.pendingkeyrounds = gamedata->pendingkeyrounds; + ng.challengekeys.pendingkeyroundoffset = gamedata->pendingkeyroundoffset; + ng.challengekeys.keyspending = gamedata->keyspending; + ng.challengekeys.chaokeys = gamedata->chaokeys; + ng.goner.everloadedaddon = gamedata->everloadedaddon; + ng.goner.everfinishcredits = gamedata->everfinishedcredits; + ng.goner.eversavedreplay = gamedata->eversavedreplay; + ng.goner.everseenspecial = gamedata->everseenspecial; + ng.goner.chaokeytutorial = gamedata->chaokeytutorial; + ng.goner.majorkeyskipattempted = gamedata->majorkeyskipattempted; + ng.goner.finishedtutorialchallenge = gamedata->finishedtutorialchallenge; + ng.goner.enteredtutorialchallenge = gamedata->enteredtutorialchallenge; + ng.goner.level = gamedata->gonerlevel; + ng.prisons.thisprisoneggpickup = gamedata->thisprisoneggpickup; + ng.prisons.prisoneggstothispickup = gamedata->prisoneggstothispickup; + ng.tafolderhash = quickncasehash(timeattackfolder, 64); + ng.emblems.resize(MAXEMBLEMS, false); + for (int i = 0; i < MAXEMBLEMS; i++) + { + ng.emblems[i] = gamedata->collected[i]; + } + ng.unlockables.resize(MAXUNLOCKABLES, false); + for (int i = 0; i < MAXUNLOCKABLES; i++) + { + ng.unlockables[i] = gamedata->unlocked[i]; + } + ng.unlockpending.resize(MAXUNLOCKABLES, false); + for (int i = 0; i < MAXUNLOCKABLES; i++) + { + ng.unlockpending[i] = gamedata->unlockpending[i]; + } + ng.conditionsets.resize(MAXCONDITIONSETS, false); + for (int i = 0; i < MAXCONDITIONSETS; i++) + { + ng.conditionsets[i] = gamedata->achieved[i]; + } + if (gamedata->challengegrid) + { + ng.challengegrid.width = gamedata->challengegridwidth; + ng.challengegrid.grid.resize(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT, 0); + for (int i = 0; i < gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; i++) + { + ng.challengegrid.grid[i] = gamedata->challengegrid[i]; + } + } + ng.timesBeaten = gamedata->timesBeaten; + for (int i = 0; i < numskins; i++) + { + srb2::GamedataSkinJson skin; + std::string name = std::string(skins[i].name); + skin.records.wins = skins[i].records.wins; + ng.skins[name] = skin; + } + for (auto unloadedskin = unloadedskins; unloadedskin; unloadedskin = unloadedskin->next) + { + srb2::GamedataSkinJson skin; + std::string name = std::string(unloadedskin->name); + skin.records.wins = unloadedskin->records.wins; + ng.skins[name] = skin; + } + for (int i = 0; i < nummapheaders; i++) + { + srb2::GamedataMapJson map; + std::string lumpname = std::string(mapheaderinfo[i]->lumpname); + map.visited.visited = mapheaderinfo[i]->records.mapvisited & MV_VISITED; + map.visited.beaten = mapheaderinfo[i]->records.mapvisited & MV_BEATEN; + map.visited.encore = mapheaderinfo[i]->records.mapvisited & MV_ENCORE; + map.visited.spbattack = mapheaderinfo[i]->records.mapvisited & MV_SPBATTACK; + map.visited.mysticmelody = mapheaderinfo[i]->records.mapvisited & MV_MYSTICMELODY; + map.stats.besttime = mapheaderinfo[i]->records.time; + map.stats.bestlap = mapheaderinfo[i]->records.lap; + ng.maps[lumpname] = map; + } + for (auto unloadedmap = unloadedmapheaders; unloadedmap; unloadedmap = unloadedmap->next) + { + srb2::GamedataMapJson map; + std::string lumpname = std::string(unloadedmap->lumpname); + map.visited.visited = unloadedmap->records.mapvisited & MV_VISITED; + map.visited.beaten = unloadedmap->records.mapvisited & MV_BEATEN; + map.visited.encore = unloadedmap->records.mapvisited & MV_ENCORE; + map.visited.spbattack = unloadedmap->records.mapvisited & MV_SPBATTACK; + map.visited.mysticmelody = unloadedmap->records.mapvisited & MV_MYSTICMELODY; + map.stats.besttime = unloadedmap->records.time; + map.stats.bestlap = unloadedmap->records.lap; + ng.maps[lumpname] = map; + } + for (int i = 0; i < gamedata->numspraycans; i++) + { + srb2::GamedataSprayCanJson spraycan; + + candata_t* can = &gamedata->spraycans[i]; + + spraycan.color = can->col; + if (can->map >= nummapheaders) + { + continue; + } + mapheader_t* mapheader = mapheaderinfo[can->map]; + if (!mapheader) + { + continue; + } + spraycan.map = std::string(mapheader->lumpname); + ng.spraycans.push_back(spraycan); + } + for (auto cup = kartcupheaders; cup; cup = cup->next) + { + if (cup->windata[0].best_placement == 0) + { + continue; + } + srb2::GamedataCupJson cupdata; + cupdata.name = std::string(cup->name); + for (int i = 0; i < 4; i++) + { + cupdata.records[i].bestgrade = cup->windata[i].best_grade; + cupdata.records[i].bestplacement = cup->windata[i].best_placement; + cupdata.records[i].bestskin = std::string(skins[cup->windata[i].best_skin.id].name); + cupdata.records[i].emerald = cup->windata[i].got_emerald; + } + ng.cups[cupdata.name] = cupdata; + } + for (auto unloadedcup = unloadedcupheaders; unloadedcup; unloadedcup = unloadedcup->next) + { + if (unloadedcup->windata[0].best_placement == 0) + { + continue; + } + srb2::GamedataCupJson cupdata; + cupdata.name = std::string(unloadedcup->name); + for (int i = 0; i < 4; i++) + { + cupdata.records[i].bestgrade = unloadedcup->windata[i].best_grade; + cupdata.records[i].bestplacement = unloadedcup->windata[i].best_placement; + cupdata.records[i].bestskin = std::string(skins[unloadedcup->windata[i].best_skin.id].name); + cupdata.records[i].emerald = unloadedcup->windata[i].got_emerald; + } + ng.cups[cupdata.name] = cupdata; + } + + std::string gamedataname_s{gamedatafilename}; + fs::path savepath{fmt::format("{}/{}", srb2home, gamedataname_s)}; + fs::path tmpsavepath{fmt::format("{}/{}.tmp", srb2home, gamedataname_s)}; + + json ngdata_json = ng; + + try + { + std::string tmpsavepathstring = tmpsavepath.string(); + srb2::io::FileStream file {tmpsavepathstring, srb2::io::FileStreamMode::kWrite}; + srb2::io::BufferedOutputStream bos {std::move(file)}; + + // The header is necessary to validate during loading. + srb2::io::write(static_cast(0xBA5ED321), bos); // major + srb2::io::write(static_cast(0), bos); // minor/flags + srb2::io::write(static_cast(gamedata->evercrashed), bos); // dirty (crash recovery) + + std::vector ubjson = json::to_ubjson(ng); + srb2::io::write_exact(bos, tcb::as_bytes(tcb::make_span(ubjson))); + bos.flush(); + file = bos.stream(); + file.close(); + } + catch (const srb2::io::FileStreamException& ex) + { + CONS_Alert(CONS_ERROR, "NG Gamedata save failed: %s\n", ex.what()); + } + catch (...) + { + CONS_Alert(CONS_ERROR, "NG Gamedata save failed\n"); + } + + try + { + // Now that the save is written successfully, move it over the old save + fs::rename(tmpsavepath, savepath); + } + catch (const fs::filesystem_error& ex) + { + CONS_Alert(CONS_ERROR, "NG Gamedata save succeeded but did not replace old save successfully: %s\n", ex.what()); + } +} + +// G_SaveGameData +// Saves the main data file, which stores information such as emblems found, etc. +void G_SaveGameData(void) +{ + try + { + srb2::save_ng_gamedata(); + } + catch (...) + { + CONS_Alert(CONS_ERROR, "Gamedata save failed\n"); + return; + } + + // Also save profiles here. + PR_SaveProfiles(); + + #ifdef DEVELOP + CONS_Alert(CONS_NOTICE, M_GetText("Gamedata saved.\n")); + #endif +} + +static const char *G_GameDataFolder(void) +{ + if (strcmp(srb2home,".")) + return srb2home; + else + return "the Ring Racers folder"; +} + +void srb2::load_ng_gamedata() +{ + // Stop saving, until we successfully load it again. + gamedata->loaded = false; + + // Clear things so previously read gamedata doesn't transfer + // to new gamedata + // see also M_EraseDataResponse + G_ClearRecords(); // records + M_ClearStats(); // statistics + M_ClearSecrets(); // emblems, unlocks, maps visited, etc + + if (M_CheckParm("-nodata")) + { + // Don't load at all. + // The following used to be in M_ClearSecrets, but that was silly. + M_UpdateUnlockablesAndExtraEmblems(false, true); + M_FinaliseGameData(); + gamedata->loaded = true; + return; + } + + if (M_CheckParm("-resetdata")) + { + // Don't load, but do save. (essentially, reset) + M_FinaliseGameData(); + gamedata->loaded = true; + return; + } + + std::string datapath {fmt::format("{}/{}", srb2home, gamedatafilename)}; + + srb2::io::BufferedInputStream bis; + try + { + srb2::io::FileStream file {datapath, srb2::io::FileStreamMode::kRead }; + bis = srb2::io::BufferedInputStream(std::move(file)); + } + catch (const srb2::io::FileStreamException& ex) + { + M_FinaliseGameData(); + gamedata->loaded = true; + return; + } + + uint32_t majorversion; + uint8_t minorversion; + uint8_t dirty; + try + { + majorversion = srb2::io::read_uint32(bis); + minorversion = srb2::io::read_uint8(bis); + dirty = srb2::io::read_uint8(bis); + } + catch (...) + { + CONS_Alert(CONS_ERROR, "Failed to read ng gamedata header\n"); + return; + } + + if (majorversion != 0xBA5ED321) + { + const char* gdfolder = G_GameDataFolder(); + I_Error("Game data is not for Ring Racers v2.0.\nDelete %s (maybe in %s) and try again.", gamedatafilename, gdfolder); + return; + } + + std::vector remainder = srb2::io::read_to_vec(bis); + + GamedataJson js; + try + { + // safety: std::byte repr is always uint8_t 1-byte aligned + tcb::span remainder_as_u8 = tcb::span((uint8_t*)remainder.data(), remainder.size()); + json parsed = json::from_ubjson(remainder_as_u8); + js = parsed.template get(); + } + catch (...) + { + const char* gdfolder = G_GameDataFolder(); + I_Error("Game data is corrupt.\nDelete %s (maybe in %s) and try again.", gamedatafilename, gdfolder); + return; + } + + // Quick & dirty hash for what mod this save file is for. + if (js.tafolderhash != quickncasehash(timeattackfolder, 64)) + { + const char* gdfolder = G_GameDataFolder(); + I_Error("Game data is corrupt.\nDelete %s (maybe in %s) and try again.", gamedatafilename, gdfolder); + return; + } + + // Now we extract the json struct's data and put it into the C-side gamedata. + + gamedata->evercrashed = dirty; + + gamedata->totalplaytime = js.playtime.total; + gamedata->totalrings = js.rings.total; + gamedata->totaltumbletime = js.playtime.tumble; + gamedata->roundsplayed[GDGT_RACE] = js.rounds.race; + gamedata->roundsplayed[GDGT_BATTLE] = js.rounds.battle; + gamedata->roundsplayed[GDGT_PRISONS] = js.rounds.prisons; + gamedata->roundsplayed[GDGT_SPECIAL] = js.rounds.special; + gamedata->roundsplayed[GDGT_CUSTOM] = js.rounds.custom; + gamedata->pendingkeyrounds = js.challengekeys.pendingkeyrounds; + gamedata->pendingkeyroundoffset = js.challengekeys.pendingkeyroundoffset; + gamedata->keyspending = js.challengekeys.keyspending; + gamedata->chaokeys = js.challengekeys.chaokeys; + gamedata->everloadedaddon = js.goner.everloadedaddon; + gamedata->everfinishedcredits = js.goner.everfinishcredits; + gamedata->eversavedreplay = js.goner.eversavedreplay; + gamedata->everseenspecial = js.goner.everseenspecial; + gamedata->chaokeytutorial = js.goner.chaokeytutorial; + gamedata->majorkeyskipattempted = js.goner.majorkeyskipattempted; + gamedata->finishedtutorialchallenge = js.goner.finishedtutorialchallenge; + gamedata->enteredtutorialchallenge = js.goner.enteredtutorialchallenge; + gamedata->gonerlevel = js.goner.level; + gamedata->thisprisoneggpickup = js.prisons.thisprisoneggpickup; + gamedata->prisoneggstothispickup = js.prisons.prisoneggstothispickup; + + size_t emblems_size = js.emblems.size(); + for (size_t i = 0; i < std::min((size_t)MAXEMBLEMS, emblems_size); i++) + { + gamedata->collected[i] = js.emblems[i]; + } + + size_t unlocks_size = js.unlockables.size(); + for (size_t i = 0; i < std::min((size_t)MAXUNLOCKABLES, unlocks_size); i++) + { + gamedata->unlocked[i] = js.unlockables[i]; + } + + size_t pending_unlocks_size = js.unlockpending.size(); + for (size_t i = 0; i < std::min((size_t)MAXUNLOCKABLES, pending_unlocks_size); i++) + { + gamedata->unlockpending[i] = js.unlockpending[i]; + } + + size_t conditions_size = js.conditionsets.size(); + for (size_t i = 0; i < std::min((size_t)MAXCONDITIONSETS, conditions_size); i++) + { + gamedata->achieved[i] = js.conditionsets[i]; + } + + if (M_CheckParm("-resetchallengegrid")) + { + gamedata->challengegridwidth = 0; + if (gamedata->challengegrid) + { + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = nullptr; + } + } + else + { + gamedata->challengegridwidth = std::min(js.challengegrid.width, (uint32_t)0); + if (gamedata->challengegrid) + { + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = nullptr; + } + if (gamedata->challengegridwidth) + { + gamedata->challengegrid = static_cast(Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT16)), + PU_STATIC, NULL)); + for (size_t i = 0; i < std::min((size_t)(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT), js.challengegrid.grid.size()); i++) + { + int16_t gridvalue = js.challengegrid.grid[i]; + if (gridvalue < 0) + { + gamedata->challengegrid[i] = MAXUNLOCKABLES; + } + else + { + gamedata->challengegrid[i] = static_cast(gridvalue); + } + } + + M_SanitiseChallengeGrid(); + } + else + { + gamedata->challengegrid = NULL; + } + } + + gamedata->timesBeaten = js.timesBeaten; + + // Main records + + for (auto& skinpair : js.skins) + { + INT32 skin = R_SkinAvailable(skinpair.first.c_str()); + skinrecord_t dummyrecord {}; + dummyrecord.wins = skinpair.second.records.wins; + + if (skin != -1) + { + skins[skin].records = dummyrecord; + } + else if (dummyrecord.wins) + { + // Invalid, but we don't want to lose all the juicy statistics. + // Instead, update a FILO linked list of "unloaded skins". + + unloaded_skin_t *unloadedskin = + static_cast(Z_Malloc( + sizeof(unloaded_skin_t), + PU_STATIC, NULL + )); + + // Establish properties, for later retrieval on file add. + strlcpy(unloadedskin->name, skinpair.first.c_str(), sizeof(unloadedskin->name)); + unloadedskin->namehash = quickncasehash(unloadedskin->name, SKINNAMESIZE); + + // Insert at the head, just because it's convenient. + unloadedskin->next = unloadedskins; + unloadedskins = unloadedskin; + + // Finally, copy into. + unloadedskin->records = dummyrecord; + } + } + + for (auto& mappair : js.maps) + { + UINT16 mapnum = G_MapNumber(mappair.first.c_str()); + recorddata_t dummyrecord {}; + dummyrecord.mapvisited |= mappair.second.visited.visited ? MV_VISITED : 0; + dummyrecord.mapvisited |= mappair.second.visited.beaten ? MV_BEATEN : 0; + dummyrecord.mapvisited |= mappair.second.visited.encore ? MV_ENCORE : 0; + dummyrecord.mapvisited |= mappair.second.visited.spbattack ? MV_SPBATTACK : 0; + dummyrecord.mapvisited |= mappair.second.visited.mysticmelody ? MV_SPBATTACK : 0; + dummyrecord.lap = mappair.second.stats.bestlap; + dummyrecord.time = mappair.second.stats.besttime; + + if (mapnum < nummapheaders && mapheaderinfo[mapnum]) + { + // Valid mapheader, time to populate with record data. + + mapheaderinfo[mapnum]->records = dummyrecord; + } + else if (dummyrecord.mapvisited & MV_BEATEN || dummyrecord.time != 0 || dummyrecord.lap != 0) + { + // 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 = + static_cast(Z_Malloc( + sizeof(unloaded_mapheader_t), + PU_STATIC, NULL + )); + + // Establish properties, for later retrieval on file add. + unloadedmap->lumpname = Z_StrDup(mappair.first.c_str()); + unloadedmap->lumpnamehash = quickncasehash(unloadedmap->lumpname, MAXMAPLUMPNAME); + + // Insert at the head, just because it's convenient. + unloadedmap->next = unloadedmapheaders; + unloadedmapheaders = unloadedmap; + + // Finally, copy into. + unloadedmap->records = dummyrecord; + } + } + + gamedata->gotspraycans = 0; + gamedata->numspraycans = js.spraycans.size(); + if (gamedata->spraycans) + { + Z_Free(gamedata->spraycans); + } + if (gamedata->numspraycans) + { + gamedata->spraycans = static_cast(Z_Malloc( + (gamedata->numspraycans * sizeof(candata_t)), + PU_STATIC, NULL)); + + for (size_t i = 0; i < js.spraycans.size(); i++) + { + auto& can = js.spraycans[i]; + gamedata->spraycans[i].col = can.color; + gamedata->spraycans[i].map = NEXTMAP_INVALID; + if (can.map.empty()) + { + continue; + } + UINT16 mapnum = G_MapNumber(can.map.c_str()); + if (mapnum < 0) + { + continue; + } + gamedata->spraycans[i].map = mapnum; + + 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]->cache_spraycan = i; + } + mapheaderinfo[mapnum]->cache_spraycan = gamedata->gotspraycans; + gamedata->gotspraycans++; + } + } + else + { + gamedata->spraycans = nullptr; + } + + for (auto& cuppair : js.cups) + { + cupwindata_t dummywindata[4] {{}}; + cupheader_t* cup = nullptr; + + // Find the loaded cup + for (cup = kartcupheaders; cup; cup = cup->next) + { + std::string cupname = std::string(cup->name); + if (cupname == cuppair.first) + { + continue; + } + } + + // Digest its data... + for (size_t j = 0; j < (size_t)KARTGP_MAX; j++) + { + dummywindata[j].best_placement = cuppair.second.records[j].bestplacement; + dummywindata[j].best_grade = static_cast(cuppair.second.records[j].bestgrade); + dummywindata[j].got_emerald = cuppair.second.records[j].emerald; + + dummywindata[j].best_skin.id = MAXSKINS; + dummywindata[j].best_skin.unloaded = nullptr; + + bool skinfound = false; + for (int skin = 0; skin < numskins; skin++) + { + std::string skinname = std::string(skins[skin].name); + if (skinname == cuppair.second.records[j].bestskin) + { + skinreference_t ref {}; + ref.id = skin; + ref.unloaded = nullptr; + dummywindata[j].best_skin = ref; + skinfound = true; + break; + } + } + if (skinfound) + { + continue; + } + for (auto unloadedskin = unloadedskins; unloadedskin; unloadedskin = unloadedskin->next) + { + std::string skinname = std::string(unloadedskin->name); + if (skinname == cuppair.second.records[j].bestskin) + { + skinreference_t ref {}; + ref.id = MAXSKINS; + ref.unloaded = unloadedskin; + dummywindata[j].best_skin = ref; + skinfound = true; + break; + } + } + } + + if (cup) + { + // We found a cup, so assign the windata. + + memcpy(cup->windata, dummywindata, sizeof(cup->windata)); + } + else if (dummywindata[0].best_placement != 0) + { + // Invalid, but we don't want to lose all the juicy statistics. + // Instead, update a FILO linked list of "unloaded cupheaders". + + unloaded_cupheader_t *unloadedcup = + static_cast(Z_Malloc( + sizeof(unloaded_cupheader_t), + PU_STATIC, NULL + )); + + // Establish properties, for later retrieval on file add. + strlcpy(unloadedcup->name, cuppair.first.c_str(), sizeof(unloadedcup->name)); + unloadedcup->namehash = quickncasehash(unloadedcup->name, MAXCUPNAME); + + // Insert at the head, just because it's convenient. + unloadedcup->next = unloadedcupheaders; + unloadedcupheaders = unloadedcup; + + // Finally, copy into. + memcpy(unloadedcup->windata, dummywindata, sizeof(cup->windata)); + } + } + + M_FinaliseGameData(); +} + +// G_LoadGameData +// Loads the main data file, which stores information such as emblems found, etc. +void G_LoadGameData(void) +{ + try + { + srb2::load_ng_gamedata(); + } + catch (...) + { + CONS_Alert(CONS_ERROR, "NG Gamedata loading failed\n"); + } +} diff --git a/src/g_gamedata.h b/src/g_gamedata.h new file mode 100644 index 000000000..4b9ebeb9d --- /dev/null +++ b/src/g_gamedata.h @@ -0,0 +1,229 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef SRB2_G_GAMEDATA_H +#define SRB2_G_GAMEDATA_H + +#ifdef __cplusplus + +#include +#include +#include +#include +#include + +#include + +namespace srb2 +{ + +struct GamedataPlaytimeJson final +{ + uint32_t total; + uint32_t tumble; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataPlaytimeJson, total, tumble) +}; + +struct GamedataRingsJson final +{ + uint32_t total; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataRingsJson, total) +}; + +struct GamedataRoundsJson final +{ + uint32_t race; + uint32_t battle; + uint32_t prisons; + uint32_t special; + uint32_t custom; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataRoundsJson, race, battle, prisons, special, custom) +}; + +struct GamedataChallengeKeysJson final +{ + uint32_t pendingkeyrounds; + uint8_t pendingkeyroundoffset; + uint16_t keyspending; + uint16_t chaokeys; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataChallengeKeysJson, pendingkeyrounds, pendingkeyroundoffset, keyspending, chaokeys) +}; + +struct GamedataGonerJson final +{ + uint32_t level; + bool everloadedaddon; + bool everfinishcredits; + bool eversavedreplay; + bool everseenspecial; + bool chaokeytutorial; + bool majorkeyskipattempted; + bool finishedtutorialchallenge; + bool enteredtutorialchallenge; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( + GamedataGonerJson, + level, + everloadedaddon, + everfinishcredits, + eversavedreplay, + everseenspecial, + chaokeytutorial, + majorkeyskipattempted, + finishedtutorialchallenge, + enteredtutorialchallenge + ) +}; + +struct GamedataPrisonEggPickupsJson final +{ + uint16_t thisprisoneggpickup; + uint16_t prisoneggstothispickup; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataPrisonEggPickupsJson, thisprisoneggpickup, prisoneggstothispickup) +}; + +struct GamedataChallengeGridJson final +{ + uint32_t width; + std::vector grid; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataChallengeGridJson, width, grid) +}; + +struct GamedataSkinRecordsJson final +{ + uint32_t wins; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataSkinRecordsJson, wins) +}; + +struct GamedataSkinJson final +{ + GamedataSkinRecordsJson records; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataSkinJson, records) +}; + +struct GamedataMapVisitedJson final +{ + bool visited; + bool beaten; + bool encore; + bool spbattack; + bool mysticmelody; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataMapVisitedJson, visited, beaten, encore, spbattack, mysticmelody) +}; + +struct GamedataMapStatsJson final +{ + uint32_t besttime; + uint32_t bestlap; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataMapStatsJson, besttime, bestlap) +}; + +struct GamedataMapJson final +{ + GamedataMapVisitedJson visited; + GamedataMapStatsJson stats; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataMapJson, visited, stats) +}; + +struct GamedataSprayCanJson final +{ + std::string map; + uint16_t color; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataSprayCanJson, map, color) +}; + +struct GamedataCupRecordsJson final +{ + uint8_t bestplacement; + uint8_t bestgrade; + uint8_t emerald; + std::string bestskin; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataCupRecordsJson, bestplacement, bestgrade, emerald, bestskin) +}; + +struct GamedataCupJson final +{ + std::string name; + std::array records; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GamedataCupJson, name, records) +}; + +struct GamedataJson final +{ + GamedataPlaytimeJson playtime; + GamedataRingsJson rings; + GamedataRoundsJson rounds; + GamedataChallengeKeysJson challengekeys; + GamedataGonerJson goner; + GamedataPrisonEggPickupsJson prisons; + uint32_t tafolderhash; + std::vector emblems; + std::vector unlockables; + std::vector unlockpending; + std::vector conditionsets; + GamedataChallengeGridJson challengegrid; + uint32_t timesBeaten; + std::unordered_map skins; + std::unordered_map maps; + std::vector spraycans; + std::unordered_map cups; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( + GamedataJson, + playtime, + rings, + rounds, + challengekeys, + goner, + prisons, + tafolderhash, + emblems, + unlockables, + unlockpending, + conditionsets, + challengegrid, + timesBeaten, + skins, + maps, + spraycans, + cups + ) +}; + +void save_ng_gamedata(void); +void load_ng_gamedata(void); + +} + +extern "C" +{ +#endif // __cplusplus + +void G_SaveGameData(void); +void G_LoadGameData(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // SRB2_G_GAMEDATA_H diff --git a/src/io/streams.cpp b/src/io/streams.cpp index 6ba250a24..85ef2aab2 100644 --- a/src/io/streams.cpp +++ b/src/io/streams.cpp @@ -9,5 +9,161 @@ #include "streams.hpp" +#include +#include +#include + template class srb2::io::ZlibInputStream; template class srb2::io::ZlibInputStream; +template class srb2::io::BufferedOutputStream; +template class srb2::io::BufferedInputStream; + +using namespace srb2::io; + +static FileStreamException make_exception_from_errno(int err) +{ + char* errnostr = strerror(err); + return FileStreamException(std::string(errnostr)); +} + +FileStreamException::FileStreamException(const char* msg) : msg_(msg) {} +FileStreamException::FileStreamException(const std::string& msg) : msg_(msg) {} + +FileStreamException::FileStreamException(const FileStreamException&) = default; +FileStreamException::FileStreamException(FileStreamException&& r) noexcept = default; + +FileStreamException::~FileStreamException() = default; + +FileStreamException& FileStreamException::operator=(const FileStreamException&) = default; +FileStreamException& FileStreamException::operator=(FileStreamException&&) noexcept = default; + +const char* FileStreamException::what() const noexcept +{ + return msg_.c_str(); +} + +FileStream::FileStream() noexcept = default; +FileStream::FileStream(FileStream&& r) noexcept +{ + *this = std::move(r); +}; + +FileStream::~FileStream() +{ + if (file_) + { + // We don't care about the result. Exceptions can't be thrown in destructors. + std::fclose((std::FILE*)(this->file_)); + } + file_ = nullptr; +} + +FileStream::FileStream(const std::string& path, FileStreamMode mode) : file_(nullptr), mode_(mode) +{ + const char* fopenmode = "r"; + switch (mode_) + { + case FileStreamMode::kRead: + fopenmode = "rb"; + break; + case FileStreamMode::kWrite: + fopenmode = "wb"; + break; + case FileStreamMode::kAppend: + fopenmode = "ab"; + break; + default: + throw std::invalid_argument("file stream mode unsupported"); + } + + std::FILE* file = std::fopen(path.c_str(), fopenmode); + if (file == nullptr) + { + int err = errno; + throw make_exception_from_errno(err); + } + + // We want raw, unbuffered IO for the stream. Buffering can be layered on top. + if (std::setvbuf(file, nullptr, _IONBF, 0) != 0) + { + int err = errno; + throw make_exception_from_errno(err); + } + + this->file_ = (void*) file; +} + +FileStream& FileStream::operator=(FileStream&& r) noexcept +{ + file_ = r.file_; + r.file_ = nullptr; + mode_ = r.mode_; + return *this; +}; + +StreamSize FileStream::read(tcb::span buffer) +{ + if (this->file_ == nullptr) + { + throw std::domain_error("FileStream is empty"); + } + + if (this->mode_ != FileStreamMode::kRead) + { + throw std::domain_error("FileStream is not in read mode"); + } + + void* cbuf = (void*)(buffer.data()); + std::size_t cbufsize = buffer.size_bytes(); + std::size_t bytesread = fread(cbuf, 1, cbufsize, (std::FILE*)(this->file_)); + if (std::ferror((std::FILE*)(this->file_)) != 0) + { + int err = errno; + throw make_exception_from_errno(err); + } + return bytesread; +} + +StreamSize FileStream::write(tcb::span buffer) +{ + if (this->file_ == nullptr) + { + throw std::domain_error("FileStream is empty"); + } + + if (this->mode_ == FileStreamMode::kRead) + { + throw std::domain_error("FileStream is not in writable mode"); + } + + void* cbuf = (void*)(buffer.data()); + std::size_t cbufsize = buffer.size_bytes(); + std::size_t byteswritten = std::fwrite(cbuf, 1, cbufsize, (std::FILE*)(this->file_)); + if (std::ferror((std::FILE*)(this->file_)) != 0) + { + int err = errno; + throw make_exception_from_errno(err); + } + return byteswritten; +} + +void FileStream::close() +{ + if (!file_) + { + return; + } + + if (std::fclose((std::FILE*)(this->file_)) != 0) + { + int err = errno; + throw make_exception_from_errno(err); + } + + if (std::ferror((std::FILE*)(this->file_)) != 0) + { + int err = errno; + throw make_exception_from_errno(err); + } + file_ = nullptr; +} diff --git a/src/io/streams.hpp b/src/io/streams.hpp index 39f0b6299..c4456d2d4 100644 --- a/src/io/streams.hpp +++ b/src/io/streams.hpp @@ -47,6 +47,9 @@ struct IsSeekableStream : public std::is_same().seek(std::declval(), std::declval())), StreamSize> {}; +template +struct IsFlushableStream : public std::is_same().flush()), void> {}; + template struct IsStream : public std::disjunction, IsOutputStream> {}; @@ -60,6 +63,8 @@ inline constexpr const bool IsOutputStreamV = IsOutputStream::value; template inline constexpr const bool IsSeekableStreamV = IsSeekableStream::value; template +inline constexpr const bool IsFlushableStreamV = IsFlushableStream::value; +template inline constexpr const bool IsStreamV = IsStream::value; template inline constexpr const bool IsInputOutputStreamV = IsInputOutputStream::value; @@ -570,6 +575,54 @@ inline void read_exact(VecStream& stream, tcb::span buffer) std::copy(copy_begin, copy_end, buffer.begin()); } +enum class FileStreamMode +{ + kRead, + kWrite, + kAppend, +}; + +class FileStreamException final : public std::exception +{ + std::string msg_; + +public: + explicit FileStreamException(const char* msg); + explicit FileStreamException(const std::string& msg); + FileStreamException(const FileStreamException&); + FileStreamException(FileStreamException&&) noexcept; + ~FileStreamException(); + + FileStreamException& operator=(const FileStreamException&); + FileStreamException& operator=(FileStreamException&&) noexcept; + + virtual const char* what() const noexcept override; +}; + +class FileStream final +{ + void* file_ = nullptr; // Type is omitted to avoid include cstdio + FileStreamMode mode_; + +public: + FileStream() noexcept; + FileStream(const FileStream&) = delete; + FileStream(FileStream&&) noexcept; + FileStream(const std::string& path, FileStreamMode mode = FileStreamMode::kRead); + ~FileStream(); + + FileStream& operator=(const FileStream&) = delete; + FileStream& operator=(FileStream&&) noexcept; + + StreamSize read(tcb::span buffer); + StreamSize write(tcb::span buffer); + + // not bothering with seeking for now -- apparently 64-bit file positions is not available in ansi c + // StreamSize seek(SeekFrom seek_from, StreamOffset offset); + + void close(); +}; + class ZlibException : public std::exception { int err_ {0}; std::string msg_; @@ -759,6 +812,124 @@ private: } }; +extern template class ZlibInputStream; +extern template class ZlibInputStream; + +template && std::is_move_constructible_v && + std::is_move_assignable_v>* = nullptr> +class BufferedOutputStream final +{ + O inner_; + std::vector buf_; + tcb::span::size_type cap_; + +public: + explicit BufferedOutputStream(O&& o) : inner_(std::forward(o)), buf_(), cap_(8192) {} + BufferedOutputStream(O&& o, tcb::span::size_type capacity) : inner_(std::forward(o)), buf_(), cap_(capacity) {} + BufferedOutputStream(const BufferedOutputStream&) = delete; + BufferedOutputStream(BufferedOutputStream&&) = default; + ~BufferedOutputStream() = default; + + BufferedOutputStream& operator=(const BufferedOutputStream&) = delete; + BufferedOutputStream& operator=(BufferedOutputStream&&) = default; + + StreamSize write(tcb::span buffer) + { + StreamSize totalwritten = 0; + while (buffer.size() > 0) + { + std::size_t tocopy = std::min(std::min(cap_, cap_ - buf_.size()), buffer.size()); + tcb::span copy_slice = buffer.subspan(0, tocopy); + buf_.reserve(cap_); + buf_.insert(buf_.end(), copy_slice.begin(), copy_slice.end()); + flush(); + totalwritten += copy_slice.size(); + + buffer = buffer.subspan(tocopy); + } + + return totalwritten; + } + + void flush() + { + tcb::span writebuf = tcb::make_span(buf_); + write_exact(inner_, writebuf); + buf_.resize(0); + } + + O&& stream() noexcept + { + return std::move(inner_); + } +}; + +extern template class BufferedOutputStream; + +template && std::is_move_constructible_v && + std::is_move_assignable_v>* = nullptr> +class BufferedInputStream final +{ + I inner_; + std::vector buf_; + tcb::span::size_type cap_; + +public: + template >* = nullptr> + BufferedInputStream() : inner_(), buf_(), cap_(8192) {} + + explicit BufferedInputStream(I&& i) : inner_(std::forward(i)), buf_(), cap_(8192) {} + BufferedInputStream(I&& i, tcb::span::size_type capacity) : inner_(std::forward(i)), buf_(), cap_(capacity) {} + BufferedInputStream(const BufferedInputStream&) = delete; + BufferedInputStream(BufferedInputStream&&) = default; + ~BufferedInputStream() = default; + + BufferedInputStream& operator=(const BufferedInputStream&) = delete; + BufferedInputStream& operator=(BufferedInputStream&&) = default; + + StreamSize read(tcb::span buffer) + { + StreamSize totalread = 0; + buf_.reserve(cap_); + while (buffer.size() > 0) + { + std::size_t toread = cap_ - buf_.size(); + std::size_t prereadsize = buf_.size(); + buf_.resize(prereadsize + toread); + tcb::span readspan{buf_.data() + prereadsize, buf_.data() + prereadsize + toread}; + StreamSize bytesread = inner_.read(readspan); + buf_.resize(prereadsize + bytesread); + + StreamSize tocopyfrombuf = std::min(buffer.size(), buf_.size()); + std::copy(buf_.begin(), std::next(buf_.begin(), tocopyfrombuf), buffer.begin()); + buffer = buffer.subspan(tocopyfrombuf); + totalread += tocopyfrombuf; + + // Move the remaining buffer backwards + std::size_t bufremaining = buf_.size() - tocopyfrombuf; + std::move(std::next(buf_.begin(), tocopyfrombuf), buf_.end(), buf_.begin()); + buf_.resize(bufremaining); + + // If we read 0 bytes from the stream, assume the inner stream won't return more for a while. + // The caller can read in a loop if it must (i.e. read_exact) + if (bytesread == 0) + { + break; + } + } + return totalread; + } + + I&& stream() noexcept + { + return std::move(inner_); + } +}; + +extern template class BufferedInputStream; + // Utility functions template