mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
Rewrite gamedata format
This commit is contained in:
parent
e0fe5543fa
commit
b0348526cd
6 changed files with 1250 additions and 1094 deletions
|
|
@ -19,6 +19,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
|
||||||
g_build_ticcmd.cpp
|
g_build_ticcmd.cpp
|
||||||
g_demo.c
|
g_demo.c
|
||||||
g_game.c
|
g_game.c
|
||||||
|
g_gamedata.cpp
|
||||||
g_input.c
|
g_input.c
|
||||||
g_party.cpp
|
g_party.cpp
|
||||||
am_map.c
|
am_map.c
|
||||||
|
|
|
||||||
1099
src/g_game.c
1099
src/g_game.c
File diff suppressed because it is too large
Load diff
688
src/g_gamedata.cpp
Normal file
688
src/g_gamedata.cpp
Normal file
|
|
@ -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 <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#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<srb2::io::FileStream> bos {std::move(file)};
|
||||||
|
|
||||||
|
// The header is necessary to validate during loading.
|
||||||
|
srb2::io::write(static_cast<uint32_t>(0xBA5ED321), bos); // major
|
||||||
|
srb2::io::write(static_cast<uint8_t>(0), bos); // minor/flags
|
||||||
|
srb2::io::write(static_cast<uint8_t>(gamedata->evercrashed), bos); // dirty (crash recovery)
|
||||||
|
|
||||||
|
std::vector<uint8_t> 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<srb2::io::FileStream> 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<std::byte> remainder = srb2::io::read_to_vec(bis);
|
||||||
|
|
||||||
|
GamedataJson js;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// safety: std::byte repr is always uint8_t 1-byte aligned
|
||||||
|
tcb::span<uint8_t> remainder_as_u8 = tcb::span((uint8_t*)remainder.data(), remainder.size());
|
||||||
|
json parsed = json::from_ubjson(remainder_as_u8);
|
||||||
|
js = parsed.template get<GamedataJson>();
|
||||||
|
}
|
||||||
|
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<uint16_t*>(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<uint8_t>(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<unloaded_skin_t*>(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<unloaded_mapheader_t*>(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<candata_t*>(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<gp_rank_e>(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<unloaded_cupheader_t*>(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");
|
||||||
|
}
|
||||||
|
}
|
||||||
229
src/g_gamedata.h
Normal file
229
src/g_gamedata.h
Normal file
|
|
@ -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 <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
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<int16_t> 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<GamedataCupRecordsJson, 4> 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<bool> emblems;
|
||||||
|
std::vector<bool> unlockables;
|
||||||
|
std::vector<bool> unlockpending;
|
||||||
|
std::vector<bool> conditionsets;
|
||||||
|
GamedataChallengeGridJson challengegrid;
|
||||||
|
uint32_t timesBeaten;
|
||||||
|
std::unordered_map<std::string, GamedataSkinJson> skins;
|
||||||
|
std::unordered_map<std::string, GamedataMapJson> maps;
|
||||||
|
std::vector<GamedataSprayCanJson> spraycans;
|
||||||
|
std::unordered_map<std::string, GamedataCupJson> 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
|
||||||
|
|
@ -9,5 +9,161 @@
|
||||||
|
|
||||||
#include "streams.hpp"
|
#include "streams.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
template class srb2::io::ZlibInputStream<srb2::io::SpanStream>;
|
template class srb2::io::ZlibInputStream<srb2::io::SpanStream>;
|
||||||
template class srb2::io::ZlibInputStream<srb2::io::VecStream>;
|
template class srb2::io::ZlibInputStream<srb2::io::VecStream>;
|
||||||
|
template class srb2::io::BufferedOutputStream<srb2::io::FileStream>;
|
||||||
|
template class srb2::io::BufferedInputStream<srb2::io::FileStream>;
|
||||||
|
|
||||||
|
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<std::byte> 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<const std::byte> 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ struct IsSeekableStream
|
||||||
: public std::is_same<decltype(std::declval<T&>().seek(std::declval<SeekFrom>(), std::declval<StreamOffset>())),
|
: public std::is_same<decltype(std::declval<T&>().seek(std::declval<SeekFrom>(), std::declval<StreamOffset>())),
|
||||||
StreamSize> {};
|
StreamSize> {};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct IsFlushableStream : public std::is_same<decltype(std::declval<T&>().flush()), void> {};
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
struct IsStream : public std::disjunction<IsInputStream<T>, IsOutputStream<T>> {};
|
struct IsStream : public std::disjunction<IsInputStream<T>, IsOutputStream<T>> {};
|
||||||
|
|
||||||
|
|
@ -60,6 +63,8 @@ inline constexpr const bool IsOutputStreamV = IsOutputStream<T>::value;
|
||||||
template <typename T>
|
template <typename T>
|
||||||
inline constexpr const bool IsSeekableStreamV = IsSeekableStream<T>::value;
|
inline constexpr const bool IsSeekableStreamV = IsSeekableStream<T>::value;
|
||||||
template <typename T>
|
template <typename T>
|
||||||
|
inline constexpr const bool IsFlushableStreamV = IsFlushableStream<T>::value;
|
||||||
|
template <typename T>
|
||||||
inline constexpr const bool IsStreamV = IsStream<T>::value;
|
inline constexpr const bool IsStreamV = IsStream<T>::value;
|
||||||
template <typename T>
|
template <typename T>
|
||||||
inline constexpr const bool IsInputOutputStreamV = IsInputOutputStream<T>::value;
|
inline constexpr const bool IsInputOutputStreamV = IsInputOutputStream<T>::value;
|
||||||
|
|
@ -570,6 +575,54 @@ inline void read_exact(VecStream& stream, tcb::span<std::byte> buffer)
|
||||||
std::copy(copy_begin, copy_end, buffer.begin());
|
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<std::byte> buffer);
|
||||||
|
StreamSize write(tcb::span<const std::byte> 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 {
|
class ZlibException : public std::exception {
|
||||||
int err_ {0};
|
int err_ {0};
|
||||||
std::string msg_;
|
std::string msg_;
|
||||||
|
|
@ -759,6 +812,124 @@ private:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extern template class ZlibInputStream<SpanStream>;
|
||||||
|
extern template class ZlibInputStream<VecStream>;
|
||||||
|
|
||||||
|
template <typename O,
|
||||||
|
typename std::enable_if_t<IsOutputStreamV<O> && std::is_move_constructible_v<O> &&
|
||||||
|
std::is_move_assignable_v<O>>* = nullptr>
|
||||||
|
class BufferedOutputStream final
|
||||||
|
{
|
||||||
|
O inner_;
|
||||||
|
std::vector<std::byte> buf_;
|
||||||
|
tcb::span<const std::byte>::size_type cap_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BufferedOutputStream(O&& o) : inner_(std::forward<O>(o)), buf_(), cap_(8192) {}
|
||||||
|
BufferedOutputStream(O&& o, tcb::span<const std::byte>::size_type capacity) : inner_(std::forward<O>(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<const std::byte> buffer)
|
||||||
|
{
|
||||||
|
StreamSize totalwritten = 0;
|
||||||
|
while (buffer.size() > 0)
|
||||||
|
{
|
||||||
|
std::size_t tocopy = std::min(std::min(cap_, cap_ - buf_.size()), buffer.size());
|
||||||
|
tcb::span<const std::byte> 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<const std::byte> writebuf = tcb::make_span(buf_);
|
||||||
|
write_exact(inner_, writebuf);
|
||||||
|
buf_.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
O&& stream() noexcept
|
||||||
|
{
|
||||||
|
return std::move(inner_);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern template class BufferedOutputStream<FileStream>;
|
||||||
|
|
||||||
|
template <typename I,
|
||||||
|
typename std::enable_if_t<IsInputStreamV<I> && std::is_move_constructible_v<I> &&
|
||||||
|
std::is_move_assignable_v<I>>* = nullptr>
|
||||||
|
class BufferedInputStream final
|
||||||
|
{
|
||||||
|
I inner_;
|
||||||
|
std::vector<std::byte> buf_;
|
||||||
|
tcb::span<std::byte>::size_type cap_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
template <typename std::enable_if_t<std::is_default_constructible_v<I>>* = nullptr>
|
||||||
|
BufferedInputStream() : inner_(), buf_(), cap_(8192) {}
|
||||||
|
|
||||||
|
explicit BufferedInputStream(I&& i) : inner_(std::forward<I>(i)), buf_(), cap_(8192) {}
|
||||||
|
BufferedInputStream(I&& i, tcb::span<std::byte>::size_type capacity) : inner_(std::forward<I>(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<std::byte> 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<std::byte> 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<FileStream>;
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
|
|
||||||
template <typename I, typename O>
|
template <typename I, typename O>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue