From aa0180571e4035e188af205fd233e5a839d1bae3 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 4 Aug 2026 18:35:06 -0500 Subject: [PATCH] Unify save system into one save.h/.c; diverge storage format per platform Renames savefile_t to saveslot_t and folds last session's standalone settings.h/.c module back in as savemeta_t, so there's one save system (SAVE.slots[] + SAVE.meta) instead of two parallel ones - while letting each platform pick its own physical format for the two concepts: - Linux now writes human-editable JSON (slot0.json, settings.json, ...) via yyjson's mutable writer API, so players can hand-fix a bad setting. - PSP folds meta into the same sceUtilitySavedata binary payload as its one save slot (SAVE_SLOT_COUNT_MAX=1 there - a future save picker will let players manage multiple named saves via the OS's own browser). - GameCube consolidates the 3 per-slot memory card files and the separate settings file into one combined card file. Also fixes two bugs surfaced while building this: the CRC finalize step seeked to a hardcoded offset (only safe for one section per file, breaks once meta+slots share a buffer), and save.c's async/sync dispatch left an unconditional fallback call that doesn't exist on PSP-only platforms. Co-Authored-By: Claude Sonnet 5 --- src/dusk/engine/engine.c | 6 - src/dusk/rpg/entity/global/globalitemstore.c | 8 +- src/dusk/rpg/entity/global/globalitemstore.h | 16 +- src/dusk/rpg/rpg.c | 19 +- src/dusk/rpg/story/storyflag.c | 6 +- src/dusk/rpg/story/storyflag.h | 14 +- src/dusk/save/CMakeLists.txt | 2 - src/dusk/save/save.c | 159 +++++++------- src/dusk/save/save.h | 125 +++++++---- src/dusk/save/savemeta.h | 47 +++++ src/dusk/save/{savefile.h => saveslot.h} | 51 +++-- src/dusk/save/savestream.c | 121 +++++++---- src/dusk/save/savestream.h | 105 +++++++--- src/dusk/save/settings.c | 105 ---------- src/dusk/save/settings.h | 76 ------- src/dusk/save/settingsfile.h | 47 ----- src/dusk/save/settingsstream.c | 164 --------------- src/dusk/save/settingsstream.h | 208 ------------------- src/dusk/ui/frame/game/uigamemenu.c | 10 +- src/dusk/ui/frame/settings/uisettingsinput.c | 16 +- src/duskdolphin/input/inputdolphin.c | 4 +- src/duskdolphin/save/CMakeLists.txt | 2 - src/duskdolphin/save/savedolphin.c | 160 +++++--------- src/duskdolphin/save/savedolphin.h | 73 +++++-- src/duskdolphin/save/saveplatform.h | 17 +- src/duskdolphin/save/savestreamdolphin.c | 45 ++-- src/duskdolphin/save/savestreamdolphin.h | 31 +-- src/duskdolphin/save/settingsdolphin.c | 20 -- src/duskdolphin/save/settingsdolphin.h | 39 ---- src/duskdolphin/save/settingsplatform.h | 29 --- src/duskdolphin/save/settingsstreamdolphin.c | 120 ----------- src/duskdolphin/save/settingsstreamdolphin.h | 89 -------- src/dusklinux/input/inputlinux.c | 4 +- src/dusklinux/save/CMakeLists.txt | 4 +- src/dusklinux/save/savejsonlinux.c | 161 ++++++++++++++ src/dusklinux/save/savejsonlinux.h | 147 +++++++++++++ src/dusklinux/save/savelinux.c | 134 ++++++++---- src/dusklinux/save/savelinux.h | 43 ++-- src/dusklinux/save/saveplatform.h | 27 ++- src/dusklinux/save/savestreamlinux.c | 75 ------- src/dusklinux/save/savestreamlinux.h | 78 ------- src/dusklinux/save/settingslinux.c | 29 --- src/dusklinux/save/settingslinux.h | 37 ---- src/dusklinux/save/settingsplatform.h | 29 --- src/dusklinux/save/settingsstreamlinux.c | 68 ------ src/dusklinux/save/settingsstreamlinux.h | 74 ------- src/duskpsp/input/inputpsp.c | 4 +- src/duskpsp/save/CMakeLists.txt | 10 +- src/duskpsp/save/saveplatform.h | 26 ++- src/duskpsp/save/savepsp.c | 39 ++-- src/duskpsp/save/savepsp.h | 36 ++-- src/duskpsp/save/savestreampsp.c | 5 + src/duskpsp/save/savestreampsp.h | 9 + src/duskpsp/save/settingsplatform.h | 29 --- src/duskpsp/save/settingspsp.c | 20 -- src/duskpsp/save/settingspsp.h | 41 ---- src/duskpsp/save/settingsstreampsp.c | 72 ------- src/duskpsp/save/settingsstreampsp.h | 74 ------- src/duskvita/input/inputvita.c | 4 +- 59 files changed, 1142 insertions(+), 2071 deletions(-) create mode 100644 src/dusk/save/savemeta.h rename src/dusk/save/{savefile.h => saveslot.h} (54%) delete mode 100644 src/dusk/save/settings.c delete mode 100644 src/dusk/save/settings.h delete mode 100644 src/dusk/save/settingsfile.h delete mode 100644 src/dusk/save/settingsstream.c delete mode 100644 src/dusk/save/settingsstream.h delete mode 100644 src/duskdolphin/save/settingsdolphin.c delete mode 100644 src/duskdolphin/save/settingsdolphin.h delete mode 100644 src/duskdolphin/save/settingsplatform.h delete mode 100644 src/duskdolphin/save/settingsstreamdolphin.c delete mode 100644 src/duskdolphin/save/settingsstreamdolphin.h create mode 100644 src/dusklinux/save/savejsonlinux.c create mode 100644 src/dusklinux/save/savejsonlinux.h delete mode 100644 src/dusklinux/save/savestreamlinux.c delete mode 100644 src/dusklinux/save/savestreamlinux.h delete mode 100644 src/dusklinux/save/settingslinux.c delete mode 100644 src/dusklinux/save/settingslinux.h delete mode 100644 src/dusklinux/save/settingsplatform.h delete mode 100644 src/dusklinux/save/settingsstreamlinux.c delete mode 100644 src/dusklinux/save/settingsstreamlinux.h delete mode 100644 src/duskpsp/save/settingsplatform.h delete mode 100644 src/duskpsp/save/settingspsp.c delete mode 100644 src/duskpsp/save/settingspsp.h delete mode 100644 src/duskpsp/save/settingsstreampsp.c delete mode 100644 src/duskpsp/save/settingsstreampsp.h diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 143b5a31..b74392b5 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -20,7 +20,6 @@ #include "system/system.h" #include "console/console.h" #include "save/save.h" -#include "save/settings.h" engine_t ENGINE; @@ -39,10 +38,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(inputInit()); errorChain(assetInit()); errorChain(saveInit()); - // Must run after saveInit() - on GameCube, settingsInit() reuses the - // memory card mount saveInit() already established rather than mounting - // it a second time (see settingsInitDolphin()). - errorChain(settingsInit()); errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(uiInit()); @@ -94,7 +89,6 @@ errorret_t engineDispose(void) { errorChain(uiDispose()); consoleDispose(); errorChain(displayDispose()); - errorChain(settingsDispose()); errorChain(saveDispose()); errorChain(assetDispose()); diff --git a/src/dusk/rpg/entity/global/globalitemstore.c b/src/dusk/rpg/entity/global/globalitemstore.c index 000b14a3..c4b468d4 100644 --- a/src/dusk/rpg/entity/global/globalitemstore.c +++ b/src/dusk/rpg/entity/global/globalitemstore.c @@ -9,17 +9,17 @@ #include "assert/assert.h" bool_t globalItemStoreIsCollected( - const savefile_t *file, const entityglobalid_t id + const saveslot_t *file, const entityglobalid_t id ) { - assertNotNull(file, "Save file cannot be NULL"); + assertNotNull(file, "Save slot cannot be NULL"); assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range"); return file->globalItemCollected[id]; } void globalItemStoreSetCollected( - savefile_t *file, const entityglobalid_t id, const bool_t collected + saveslot_t *file, const entityglobalid_t id, const bool_t collected ) { - assertNotNull(file, "Save file cannot be NULL"); + assertNotNull(file, "Save slot cannot be NULL"); assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range"); file->globalItemCollected[id] = collected; } diff --git a/src/dusk/rpg/entity/global/globalitemstore.h b/src/dusk/rpg/entity/global/globalitemstore.h index ecfd0e83..ad6577ec 100644 --- a/src/dusk/rpg/entity/global/globalitemstore.h +++ b/src/dusk/rpg/entity/global/globalitemstore.h @@ -7,34 +7,34 @@ #pragma once #include "error/error.h" -#include "save/savefile.h" +#include "save/saveslot.h" #include "rpg/entity/entity.h" /** * Checks whether the global entity with the given ID has already been - * marked collected in the given save file's data - e.g. so a global item + * marked collected in the given save slot's data - e.g. so a global item * entity's init callback (see rpg/entity/global/entitygloballist.h) can * skip spawning itself if the player already picked it up in a prior * session, without needing to keep the entity itself alive to remember * that (which would need render/collision special-casing - this doesn't). * - * @param file The save file to check. + * @param file The save slot to check. * @param id The global entity ID to check. * @return True if already marked collected. */ bool_t globalItemStoreIsCollected( - const savefile_t *file, const entityglobalid_t id + const saveslot_t *file, const entityglobalid_t id ); /** * Marks the global entity with the given ID as collected (or not) in the - * given save file's data. Does not itself write the save to disk - call - * saveWrite() separately once ready to persist it. + * given save slot's data. Does not itself write the save to disk - call + * saveWriteSlot() separately once ready to persist it. * - * @param file The save file to write into. + * @param file The save slot to write into. * @param id The global entity ID to mark. * @param collected The new collected state. */ void globalItemStoreSetCollected( - savefile_t *file, const entityglobalid_t id, const bool_t collected + saveslot_t *file, const entityglobalid_t id, const bool_t collected ); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index f806b529..f84298ba 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -32,10 +32,18 @@ errorret_t rpgInit(void) { memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); + saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT); + // saveInit() eagerly loads every slot from disk, but there's no + // continue-game flow yet - every boot is a fresh game regardless of + // what was found on disk, so force this false rather than let a stale + // "exists" from a real save skip storyFlagInitDefaults() below while + // everything else here still hardcodes new-game state. + saveSlot->exists = false; + // Must run before any code reads a story flag - stamps CSV-defined - // defaults onto the active save slot if it's never actually been - // loaded from disk yet. - storyFlagInitDefaults(saveGet(SAVE_ACTIVE_SLOT)); + // defaults onto the active save slot since it's now forced to look + // unloaded. + storyFlagInitDefaults(saveSlot); backpackInit(); partyInit(); @@ -67,9 +75,8 @@ errorret_t rpgInit(void) { // header/version. Remove once there's an actual name-entry flow. On PSP // this shows the real native save dialog every boot - expected while // testing that path, not something to ship as-is. - savefile_t *saveFile = saveGet(SAVE_ACTIVE_SLOT); - stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX); - saveWrite(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL); + stringCopy(saveSlot->playerName, "Dusk", SAVE_PLAYER_NAME_MAX); + saveWriteSlot(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL); // All Good! errorOk(); diff --git a/src/dusk/rpg/story/storyflag.c b/src/dusk/rpg/story/storyflag.c index 95a28436..4a152f48 100644 --- a/src/dusk/rpg/story/storyflag.c +++ b/src/dusk/rpg/story/storyflag.c @@ -10,11 +10,11 @@ void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) { assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag"); - saveGet(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value; + saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value; } -void storyFlagInitDefaults(savefile_t *file) { - assertNotNull(file, "Save file cannot be NULL"); +void storyFlagInitDefaults(saveslot_t *file) { + assertNotNull(file, "Save slot cannot be NULL"); if(file->exists) return; assertTrue( STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX, diff --git a/src/dusk/rpg/story/storyflag.h b/src/dusk/rpg/story/storyflag.h index 509ec881..e202f6d1 100644 --- a/src/dusk/rpg/story/storyflag.h +++ b/src/dusk/rpg/story/storyflag.h @@ -11,17 +11,17 @@ /** * Gets the value of a story flag. Reads directly from the active save - * file (see SAVE_ACTIVE_SLOT) - flag values have no separate live copy. + * slot (see SAVE_ACTIVE_SLOT) - flag values have no separate live copy. * * @param flag The story flag to get. * @return The value of the story flag. */ -#define storyFlagGet(flag) (saveGet(SAVE_ACTIVE_SLOT)->storyFlags[(flag)]) +#define storyFlagGet(flag) (saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[(flag)]) /** - * Sets the value of a story flag, directly in the active save file (see + * Sets the value of a story flag, directly in the active save slot (see * SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call - * saveWrite() separately once ready to persist it. + * saveWriteSlot() separately once ready to persist it. * * @param flag The story flag to set. * @param value The value to set the story flag to. @@ -30,11 +30,11 @@ void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value); /** * Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto - * the given save file, but only if it hasn't actually been loaded from + * the given save slot, but only if it hasn't actually been loaded from * disk yet (file->exists is false) - otherwise leaves already-played * progress alone. Call once, e.g. during rpgInit(), before any gameplay * code reads a story flag. * - * @param file The save file to stamp defaults onto. + * @param file The save slot to stamp defaults onto. */ -void storyFlagInitDefaults(savefile_t *file); +void storyFlagInitDefaults(saveslot_t *file); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt index 467beb8a..55b35252 100644 --- a/src/dusk/save/CMakeLists.txt +++ b/src/dusk/save/CMakeLists.txt @@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC save.c savestream.c - settings.c - settingsstream.c ) diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 2365b6c3..709ddc67 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -13,8 +13,13 @@ save_t SAVE; +static void _saveEagerLoadComplete(errorret_t result, void *user) { + if(errorIsNotOk(result)) errorCatch(errorPrint(result)); +} + errorret_t saveInit(void) { memoryZero(&SAVE, sizeof(save_t)); + SAVE.meta.deadzone = SAVE_META_DEADZONE_DEFAULT; #ifdef saveInitPlatform // A missing/unreachable save medium is expected, recoverable state, @@ -27,6 +32,21 @@ errorret_t saveInit(void) { SAVE.available = false; #endif + // Eagerly pull meta + every slot into memory up front - cheap and + // harmless (nothing consumes loaded slot data automatically; there's no + // continue-game flow yet). PSP opts out entirely (see + // saveSkipEagerLoadPlatform) since its only read path is now the native + // savedata dialog, and running that on every boot would defeat the whole + // point of folding meta into it instead of a separate instant-write file. + #ifndef saveSkipEagerLoadPlatform + if(SAVE.available) { + saveLoadMeta(_saveEagerLoadComplete, NULL); + for(uint8_t i = 0; i < SAVE_SLOT_COUNT_MAX; i++) { + saveLoadSlot(i, _saveEagerLoadComplete, NULL); + } + } + #endif + errorOk(); } @@ -56,104 +76,89 @@ bool_t saveIsBusy(void) { #endif } -void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) { - assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); +void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) { + assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX"); assertNotNull(onComplete, "onComplete cannot be NULL"); - savefile_t *file = &SAVE.files[slot]; - file->exists = false; + SAVE.slots[slot].exists = false; // Some platforms (PSP's native save dialog) can't complete within this // call - they take over entirely and invoke onComplete later, from - // saveUpdate(), once their own multi-frame flow finishes. - #ifdef saveAsyncLoadPlatform - saveAsyncLoadPlatform(slot, onComplete, user); - return; + // saveUpdate(), once their own multi-frame flow finishes. Those + // platforms never define the sync saveSlotLoadPlatform() at all, so the + // fallback below must live in the #else, not just after an early return. + #ifdef saveSlotAsyncLoadPlatform + saveSlotAsyncLoadPlatform(slot, onComplete, user); + #else + errorret_t ret = saveSlotLoadPlatform(slot, &SAVE.slots[slot]); + SAVE.available = errorIsOk(ret); + onComplete(ret, user); #endif - - savestream_t stream; - memoryZero(&stream, sizeof(savestream_t)); - - #ifdef saveStreamOpenReadPlatform - errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot); - SAVE.available = errorIsOk(openRet); - if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; } - #endif - - if(!stream.found) { onComplete(errorOkImpl(), user); return; } - - errorret_t ret = saveFileLoad(&stream, file); - - #ifdef saveStreamClosePlatform - saveStreamClosePlatform(&stream); - #endif - - if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot); - file->exists = errorIsOk(ret); - onComplete(ret, user); } -void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) { - assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); +void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) { + assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX"); assertNotNull(onComplete, "onComplete cannot be NULL"); - savefile_t *file = &SAVE.files[slot]; - // These are metadata about the file itself, not game data - always stamp - // the current magic/version on every write rather than relying on - // whatever happened to already be in memory (zeroed at saveInit, or - // whatever version an old loaded file had), otherwise the written file - // fails its own header check the next time it's loaded. - memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE); - file->version = SAVE_FILE_VERSION; - - // See saveLoad() - some platforms take over and complete later. - #ifdef saveAsyncWritePlatform - saveAsyncWritePlatform(slot, onComplete, user); - return; + // See saveLoadSlot() - some platforms take over and complete later. + #ifdef saveSlotAsyncWritePlatform + saveSlotAsyncWritePlatform(slot, onComplete, user); + #else + errorret_t ret = saveSlotWritePlatform(slot, &SAVE.slots[slot]); + SAVE.available = errorIsOk(ret); + onComplete(ret, user); #endif - - savestream_t stream; - memoryZero(&stream, sizeof(savestream_t)); - - #ifdef saveStreamOpenWritePlatform - errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot); - SAVE.available = errorIsOk(openRet); - if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; } - #endif - - errorret_t ret = saveFileWrite(&stream, file); - - if(errorIsOk(ret)) { - ret = saveStreamFinalizeWriteImpl(&stream); - } - - #ifdef saveStreamClosePlatform - saveStreamClosePlatform(&stream); - #endif - - file->exists = errorIsOk(ret); - onComplete(ret, user); } -errorret_t saveDelete(const uint8_t slot) { - assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); +errorret_t saveDeleteSlot(const uint8_t slot) { + assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX"); - #ifdef saveDeletePlatform - errorret_t deleteRet = saveDeletePlatform(slot); + #ifdef saveSlotDeletePlatform + errorret_t deleteRet = saveSlotDeletePlatform(slot); SAVE.available = errorIsOk(deleteRet); errorChain(deleteRet); #endif - SAVE.files[slot].exists = false; + SAVE.slots[slot].exists = false; errorOk(); } -bool_t saveExists(const uint8_t slot) { - assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); - return SAVE.files[slot].exists; +bool_t saveSlotExists(const uint8_t slot) { + assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX"); + return SAVE.slots[slot].exists; } -savefile_t * saveGet(const uint8_t slot) { - assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); - return &SAVE.files[slot]; +saveslot_t * saveGetSlot(const uint8_t slot) { + assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX"); + return &SAVE.slots[slot]; +} + +void saveLoadMeta(savecallback_t onComplete, void *user) { + assertNotNull(onComplete, "onComplete cannot be NULL"); + + SAVE.meta.exists = false; + + #ifdef saveMetaAsyncLoadPlatform + saveMetaAsyncLoadPlatform(onComplete, user); + #else + errorret_t ret = saveMetaLoadPlatform(&SAVE.meta); + SAVE.available = errorIsOk(ret); + onComplete(ret, user); + #endif +} + +void saveWriteMeta(savecallback_t onComplete, void *user) { + assertNotNull(onComplete, "onComplete cannot be NULL"); + + #ifdef saveMetaAsyncWritePlatform + saveMetaAsyncWritePlatform(onComplete, user); + #else + errorret_t ret = saveMetaWritePlatform(&SAVE.meta); + SAVE.available = errorIsOk(ret); + onComplete(ret, user); + #endif +} + +savemeta_t * saveGetMeta(void) { + return &SAVE.meta; } diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h index d3662168..144ff587 100644 --- a/src/dusk/save/save.h +++ b/src/dusk/save/save.h @@ -7,20 +7,23 @@ #pragma once #include "error/error.h" -#include "savefile.h" +#include "saveslot.h" +#include "savemeta.h" #include "save/saveplatform.h" typedef struct { - /** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */ - savefile_t files[SAVE_FILE_COUNT_MAX]; + /** Per-slot save data; indexed 0 to SAVE_SLOT_COUNT_MAX - 1. */ + saveslot_t slots[SAVE_SLOT_COUNT_MAX]; + /** Device-wide preferences - see savemeta.h. */ + savemeta_t meta; /** Platform-specific save system state (paths, card handles, etc.). */ saveplatform_t platform; /** * True if the save medium (memory card/stick/disk) was reachable the * last time it was checked - at saveInit(), and refreshed by every - * subsequent saveLoad()/saveWrite() attempt. Starting the game with no - * card/stick inserted, or one being removed mid-session, are both - * expected conditions here, not fatal errors - see saveIsAvailable(). + * subsequent load/write attempt. Starting the game with no card/stick + * inserted, or one being removed mid-session, are both expected + * conditions here, not fatal errors - see saveIsAvailable(). */ bool_t available; /** @@ -35,18 +38,28 @@ typedef struct { extern save_t SAVE; /** - * Initializes the save system. Never fails the way saveWrite/saveLoad can - - * if the platform's save medium isn't reachable (e.g. no memory card/stick - * inserted), that's logged and reflected in saveIsAvailable() rather than - * treated as fatal, since the game should still be playable without save - * support. + * Initializes the save system. Never fails the way saveWriteSlot()/ + * saveWriteMeta() can - if the platform's save medium isn't reachable + * (e.g. no memory card/stick inserted), that's logged and reflected in + * saveIsAvailable() rather than treated as fatal, since the game should + * still be playable without save support. + * + * On most platforms, this also eagerly loads meta and every slot from + * disk immediately - cheap and harmless, since nothing consumes the + * loaded slot data automatically (there's no "continue game" flow yet; + * rpgInit() always hardcodes a fresh game regardless of what's loaded). + * PSP skips this (see saveSkipEagerLoadPlatform) - its only read path is + * the native sceUtilitySavedata dialog now that meta lives inside the + * same payload as the save slot, and running that multi-frame dialog on + * every single boot would reintroduce the exact UX problem a lightweight + * settings-only file used to avoid. * * @return An error code only for unexpected platform failures. */ errorret_t saveInit(void); /** - * Checks whether the save medium was reachable as of the last save/load + * Checks whether the save medium was reachable as of the last load/write * attempt (or saveInit(), if none has been attempted yet). Intended for UI * to decide whether to offer saving/loading at all, or to explain why it * isn't available right now - e.g. "No memory card inserted". @@ -64,69 +77,95 @@ errorret_t saveDispose(void); /** * Updates the save manager, pumping any in-progress async save/load and - * dispatching its callback once complete. No-op on platforms where - * saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()). - * Must be called every engine frame for platforms that need it (PSP's - * native save dialog spans multiple frames). + * dispatching its callback once complete. No-op on platforms where every + * operation always completes synchronously (see saveIsBusy()). Must be + * called every engine frame for platforms that need it (PSP's native save + * dialog spans multiple frames). * * @return An error code indicating success or failure. */ errorret_t saveUpdate(void); /** - * True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's - * native save dialog is open). Calling saveWrite()/saveLoad() again while - * this is true is undefined behavior - wait for the previous call's - * callback first. + * True while an async save/load is in progress (e.g. PSP's native save + * dialog is open). Calling any save/load function again while this is true + * is undefined behavior - wait for the previous call's callback first. * * @return True if a save/load request is currently in progress. */ bool_t saveIsBusy(void); /** - * Loads the save file for a given slot from persistent storage. Slow/async - * on some platforms (PSP's native save dialog spans multiple frames) - on - * others (Linux, Dolphin) onComplete is invoked before this call returns. - * See saveIsBusy(). + * Loads the save slot for a given index from persistent storage. Slow/ + * async on some platforms (PSP's native save dialog spans multiple + * frames) - on others (Linux, Dolphin) onComplete is invoked before this + * call returns. See saveIsBusy(). * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). + * @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1). * @param onComplete Callback invoked with the result once loading finishes. * @param user User data passed through to onComplete. */ -void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user); +void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user); /** - * Writes the save file for a given slot to persistent storage. Slow/async - * on some platforms (PSP's native save dialog spans multiple frames) - on - * others (Linux, Dolphin) onComplete is invoked before this call returns. - * See saveIsBusy(). + * Writes the save slot for a given index to persistent storage. Slow/ + * async on some platforms (PSP's native save dialog spans multiple + * frames) - on others (Linux, Dolphin) onComplete is invoked before this + * call returns. See saveIsBusy(). * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). + * @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1). * @param onComplete Callback invoked with the result once writing finishes. * @param user User data passed through to onComplete. */ -void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user); +void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user); /** - * Deletes the save file for a given slot from persistent storage. + * Deletes the save slot for a given index from persistent storage. * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). + * @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1). * @return An error code if the delete fails. */ -errorret_t saveDelete(const uint8_t slot); +errorret_t saveDeleteSlot(const uint8_t slot); /** - * Checks whether a save file exists for a given slot. + * Checks whether a save slot has data for a given index. * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return true if a save file exists for the slot, false otherwise. + * @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1). + * @return true if the slot has data, false otherwise. */ -bool_t saveExists(const uint8_t slot); +bool_t saveSlotExists(const uint8_t slot); /** - * Gets a pointer to the save file data for a given slot. + * Gets a pointer to the save slot data for a given index. * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return A pointer to the savefile_t for the given slot. + * @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1). + * @return A pointer to the saveslot_t for the given slot. */ -savefile_t * saveGet(const uint8_t slot); +saveslot_t * saveGetSlot(const uint8_t slot); + +/** + * Loads device-wide meta (preferences) from persistent storage. Callback- + * based on every platform, same as saveLoadSlot() - on PSP specifically, + * loading meta means loading the same combined savedata payload as the + * active slot, which is unavoidably async there. + * + * @param onComplete Callback invoked with the result once loading finishes. + * @param user User data passed through to onComplete. + */ +void saveLoadMeta(savecallback_t onComplete, void *user); + +/** + * Writes device-wide meta (preferences) to persistent storage. + * + * @param onComplete Callback invoked with the result once writing finishes. + * @param user User data passed through to onComplete. + */ +void saveWriteMeta(savecallback_t onComplete, void *user); + +/** + * Gets a pointer to the live meta data. Modify fields directly, then call + * saveWriteMeta() to persist them. + * + * @return A pointer to the meta. + */ +savemeta_t * saveGetMeta(void); diff --git a/src/dusk/save/savemeta.h b/src/dusk/save/savemeta.h new file mode 100644 index 00000000..82ecef5d --- /dev/null +++ b/src/dusk/save/savemeta.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +/** Save meta format version. Increment on breaking change. */ +#define SAVE_META_VERSION 1 + +/** Magic bytes that identify a Dusk save meta blob. */ +#define SAVE_META_HEADER "DSM" + +/** Byte length of the magic header (excludes the null terminator). */ +#define SAVE_META_HEADER_SIZE (sizeof(SAVE_META_HEADER) - 1) + +/** + * Default gamepad deadzone for meta that's never actually been loaded from + * disk yet (see saveInit(), which stamps this on first boot) - the save + * meta is the single source of truth for this value (see + * savemeta_t.deadzone); nothing else stores or defaults it. + */ +#define SAVE_META_DEADZONE_DEFAULT 0.1f + +/** + * Device/user-wide preferences, independent of any individual game save + * slot (see saveslot.h) - there's exactly one of these, not one per slot, + * since a setting like gamepad deadzone shouldn't reset or diverge just + * because the player started a new game in a different slot. + */ +typedef struct { + /** Magic header bytes read from the blob; must equal SAVE_META_HEADER. */ + char_t header[SAVE_META_HEADER_SIZE]; + /** Format version read from the blob; used to branch on older layouts. */ + uint32_t version; + /** Runtime flag - true if meta was successfully loaded or written. */ + bool_t exists; + /** + * User-configured gamepad deadzone (0.0f-1.0f) - the save meta is the + * only place this lives; read it directly via saveGetMeta()->deadzone + * rather than caching it anywhere else. + */ + float_t deadzone; +} savemeta_t; diff --git a/src/dusk/save/savefile.h b/src/dusk/save/saveslot.h similarity index 54% rename from src/dusk/save/savefile.h rename to src/dusk/save/saveslot.h index 5d4b74c4..8d0b9246 100644 --- a/src/dusk/save/savefile.h +++ b/src/dusk/save/saveslot.h @@ -8,23 +8,30 @@ #pragma once #include "dusk.h" -/** Save file format version. Increment on breaking change. */ -#define SAVE_FILE_VERSION 1 +/** Save slot format version. Increment on breaking change. */ +#define SAVE_SLOT_VERSION 1 -/** Magic bytes that identify a Dusk save file. */ -#define SAVE_FILE_HEADER "DSK" +/** Magic bytes that identify a Dusk save slot. */ +#define SAVE_SLOT_HEADER "DSK" /** Byte length of the magic header (excludes the null terminator). */ -#define SAVE_FILE_HEADER_SIZE (sizeof(SAVE_FILE_HEADER) - 1) +#define SAVE_SLOT_HEADER_SIZE (sizeof(SAVE_SLOT_HEADER) - 1) -/** Maximum number of independent save slots supported. */ -#define SAVE_FILE_COUNT_MAX 3 +/** + * Maximum number of independent save slots supported. Platform-overridable + * via a compiler define (not a header #ifndef alone, since this header is + * included before any platform header gets a chance to react) - see PSP's + * CMakeLists.txt, which overrides this to 1. + */ +#ifndef SAVE_SLOT_COUNT_MAX + #define SAVE_SLOT_COUNT_MAX 3 +#endif /** * The save slot actually used for gameplay right now - there's no slot - * select/multi-save UX yet (SAVE_FILE_COUNT_MAX > 1 exists for later), so - * every part of the game that needs "the" save file (settings, the game - * menu's Save button, etc.) reads/writes this one slot. + * select/multi-save UX yet (SAVE_SLOT_COUNT_MAX > 1 exists for later), so + * every part of the game that needs "the" save slot (the game menu's Save + * button, etc.) reads/writes this one slot. */ #define SAVE_ACTIVE_SLOT 0 @@ -34,7 +41,7 @@ /** * Maximum number of global entities whose "collected" state can be * tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here - * rather than tied to ENTITY_GLOBAL_LIST_COUNT, since savefile.h is a + * rather than tied to ENTITY_GLOBAL_LIST_COUNT, since saveslot.h is a * leaf header with no dependency on the entity system (and no reason to * take one just for a size constant). */ @@ -44,15 +51,16 @@ * Maximum number of story flags the save format can hold - see * rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the * current flag count) rather than tied to STORY_FLAG_COUNT, since - * savefile.h is a leaf header with no dependency on generated story + * saveslot.h is a leaf header with no dependency on generated story * content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning. */ #define SAVE_STORY_FLAG_COUNT_MAX 128 +/** Per-slot game progress - the state a "save file" traditionally means. */ typedef struct { - /** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */ - char_t header[SAVE_FILE_HEADER_SIZE]; - /** Format version read from the file; used to branch on older layouts. */ + /** Magic header bytes read from the slot; must equal SAVE_SLOT_HEADER. */ + char_t header[SAVE_SLOT_HEADER_SIZE]; + /** Format version read from the slot; used to branch on older layouts. */ uint32_t version; /** Runtime flag - true if this slot was successfully loaded or written. */ bool_t exists; @@ -61,18 +69,19 @@ typedef struct { /** Per-global-ID "already collected" flags - see globalitemstore.h. */ bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX]; /** - * Story flag values, indexed by storyflag_t - the save file is the only + * Story flag values, indexed by storyflag_t - the save slot is the only * place these live; read/write via storyFlagGet()/storyFlagSet() (see * rpg/story/storyflag.h), not directly. */ uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX]; -} savefile_t; +} saveslot_t; /** - * Callback invoked when an async saveWrite()/saveLoad() request completes. - * Declared here (rather than save.h) so platform save headers - which - * save.h's platform indirection pulls in before save.h finishes defining - * anything else - can reference it without a circular include. + * Callback invoked when an async saveWriteSlot()/saveLoadSlot()/ + * saveWriteMeta()/saveLoadMeta() request completes. Declared here (rather + * than save.h) so platform save headers - which save.h's platform + * indirection pulls in before save.h finishes defining anything else - can + * reference it without a circular include. * * @param result Whether the request succeeded. * @param user User data passed through from the original call. diff --git a/src/dusk/save/savestream.c b/src/dusk/save/savestream.c index 938766e9..6f652e2d 100644 --- a/src/dusk/save/savestream.c +++ b/src/dusk/save/savestream.c @@ -45,12 +45,23 @@ errorret_t saveStreamWriteBytesImpl( errorOk(); } -errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) { +errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out) { + #ifdef saveStreamTellPlatform + errorChain(saveStreamTellPlatform(stream, out)); + #else + *out = 0; + #endif + errorOk(); +} + +errorret_t saveStreamFinalizeWriteImpl( + savestream_t *stream, const size_t headerPosition, const size_t headerSize +) { uint32_t finalCRC = cryptCRC32End(stream->checksum); uint32_t leChecksum = endianLittleToHost32(finalCRC); #ifdef saveStreamSeekPlatform - errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE)); + errorChain(saveStreamSeekPlatform(stream, headerPosition + headerSize)); #endif errorChain(saveStreamWriteBytesRawImpl( @@ -60,27 +71,25 @@ errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) { } errorret_t saveStreamVerifyChecksumImpl( - savestream_t *stream, const uint8_t slot + savestream_t *stream, const char_t *sectionLabel ) { uint32_t computed = cryptCRC32End(stream->checksum); if(computed != stream->expectedChecksum) { - errorThrow("Save slot %u has invalid checksum", (uint32_t)slot); + errorThrow("%s has invalid checksum", sectionLabel); } errorOk(); } - errorret_t saveStreamReadHeaderImpl( - savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE] + savestream_t *stream, char_t *header, const char_t *expectedHeader, + const size_t headerSize ) { - errorChain(saveStreamReadBytesRawImpl(stream, header, SAVE_FILE_HEADER_SIZE)); + errorChain(saveStreamReadBytesRawImpl(stream, header, headerSize)); - if( - header[0] != SAVE_FILE_HEADER[0] || - header[1] != SAVE_FILE_HEADER[1] || - header[2] != SAVE_FILE_HEADER[2] - ) { - errorThrow("Save file has invalid header"); + for(size_t i = 0; i < headerSize; i++) { + if(header[i] != expectedHeader[i]) { + errorThrow("Save data has invalid header"); + } } uint32_t leChecksum; @@ -91,11 +100,9 @@ errorret_t saveStreamReadHeaderImpl( } errorret_t saveStreamWriteHeaderImpl( - savestream_t *stream, const char_t header[SAVE_FILE_HEADER_SIZE] + savestream_t *stream, const char_t *header, const size_t headerSize ) { - errorChain(saveStreamWriteBytesRawImpl( - stream, header, SAVE_FILE_HEADER_SIZE - )); + errorChain(saveStreamWriteBytesRawImpl(stream, header, headerSize)); uint32_t placeholder = 0; errorChain(saveStreamWriteBytesRawImpl( @@ -327,28 +334,68 @@ errorret_t saveStreamWriteDateImpl( errorOk(); } -errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) { - saveFileReadHeader(stream, file->header); - saveFileReadVersion(stream, &file->version); - saveFileReadString(stream, file->playerName, SAVE_PLAYER_NAME_MAX); - for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { - saveFileReadBool(stream, &file->globalItemCollected[i]); - } - for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { - saveFileReadUInt8(stream, &file->storyFlags[i]); - } +errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta) { + saveFileReadHeader( + stream, meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE + ); + saveFileReadVersion(stream, &meta->version); + saveFileReadFloat(stream, &meta->deadzone); + errorChain(saveStreamVerifyChecksumImpl(stream, "Save meta")); + meta->exists = true; errorOk(); } -errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) { - saveFileWriteHeader(stream, file->header); - saveFileWriteVersion(stream, &file->version); - saveFileWriteString(stream, file->playerName, SAVE_PLAYER_NAME_MAX); - for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { - saveFileWriteBool(stream, &file->globalItemCollected[i]); - } - for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { - saveFileWriteUInt8(stream, &file->storyFlags[i]); - } +errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta) { + memoryCopy(meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE); + meta->version = SAVE_META_VERSION; + + size_t headerPosition; + errorChain(saveStreamTellImpl(stream, &headerPosition)); + saveFileWriteHeader(stream, meta->header, SAVE_META_HEADER_SIZE); + saveFileWriteVersion(stream, &meta->version); + saveFileWriteFloat(stream, &meta->deadzone); + errorChain(saveStreamFinalizeWriteImpl( + stream, headerPosition, SAVE_META_HEADER_SIZE + )); + meta->exists = true; + errorOk(); +} + +errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot) { + saveFileReadHeader( + stream, slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE + ); + saveFileReadVersion(stream, &slot->version); + saveFileReadString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX); + for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { + saveFileReadBool(stream, &slot->globalItemCollected[i]); + } + for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { + saveFileReadUInt8(stream, &slot->storyFlags[i]); + } + errorChain(saveStreamVerifyChecksumImpl(stream, "Save slot")); + slot->exists = true; + errorOk(); +} + +errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot) { + memoryCopy(slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE); + slot->version = SAVE_SLOT_VERSION; + + size_t headerPosition; + errorChain(saveStreamTellImpl(stream, &headerPosition)); + saveFileWriteHeader(stream, slot->header, SAVE_SLOT_HEADER_SIZE); + saveFileWriteVersion(stream, &slot->version); + saveFileWriteString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX); + for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) { + saveFileWriteBool(stream, &slot->globalItemCollected[i]); + } + for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) { + saveFileWriteUInt8(stream, &slot->storyFlags[i]); + } + errorChain(saveStreamFinalizeWriteImpl( + stream, headerPosition, SAVE_SLOT_HEADER_SIZE + )); + slot->exists = true; errorOk(); } diff --git a/src/dusk/save/savestream.h b/src/dusk/save/savestream.h index 4eb6c540..2c26e18b 100644 --- a/src/dusk/save/savestream.h +++ b/src/dusk/save/savestream.h @@ -7,7 +7,8 @@ #pragma once #include "error/error.h" -#include "savefile.h" +#include "saveslot.h" +#include "savemeta.h" #include "save/saveplatform.h" #include "time/timeepoch.h" @@ -67,49 +68,72 @@ errorret_t saveStreamWriteBytesImpl( ); /** - * Finalizes a write stream: computes the final CRC32, seeks to the - * checksum field in the header, and writes it in little-endian order. + * Gets the current read/write position within the stream. Used to capture + * a section's start position before writing its header, so its checksum + * can be backfilled at the right offset once the section's body is known - + * required now that a single stream can hold multiple self-contained + * sections back-to-back (meta + N save slots), not just one. + * + * @param stream Active stream. + * @param out Receives the current position. + * @return An error if the platform can't report a position. + */ +errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out); + +/** + * Finalizes a write stream: computes the final CRC32, seeks back to the + * checksum field just after this section's header, and writes it in + * little-endian order. * * @param stream Active write stream. + * @param headerPosition Byte offset where this section's header started + * (see saveStreamTellImpl), captured before writing the header. + * @param headerSize Byte length of this section's magic header. * @return An error if the seek or write fails. */ -errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream); +errorret_t saveStreamFinalizeWriteImpl( + savestream_t *stream, const size_t headerPosition, const size_t headerSize +); /** * Verifies that the CRC32 accumulated during loading matches the value - * stored in the file header. + * stored in this section's header. * * @param stream Active read stream (loading must be complete). - * @param slot Slot index used in the error message on mismatch. + * @param sectionLabel Human-readable label used in the error message on + * mismatch (e.g. "save meta", "save slot"). * @return An error if the checksum does not match. */ errorret_t saveStreamVerifyChecksumImpl( - savestream_t *stream, const uint8_t slot + savestream_t *stream, const char_t *sectionLabel ); /** - * Reads and validates the magic header, then reads the stored CRC32 and - * resets the running accumulator. + * Reads and validates a section's magic header, then reads its stored + * CRC32 and resets the running accumulator. * * @param stream Active read stream. - * @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header. + * @param header Buffer of headerSize bytes to receive the header. + * @param expectedHeader The magic bytes this section must match. + * @param headerSize Byte length of the magic header. * @return An error if the header is missing or invalid. */ errorret_t saveStreamReadHeaderImpl( - savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE] + savestream_t *stream, char_t *header, const char_t *expectedHeader, + const size_t headerSize ); /** - * Writes the magic header and a zero CRC32 placeholder, then resets the - * running accumulator. + * Writes a section's magic header and a zero CRC32 placeholder, then + * resets the running accumulator. * * @param stream Active write stream. - * @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write. + * @param header Buffer of headerSize bytes to write. + * @param headerSize Byte length of the magic header. * @return An error if the write fails. */ errorret_t saveStreamWriteHeaderImpl( - savestream_t *stream, - const char_t header[SAVE_FILE_HEADER_SIZE] + savestream_t *stream, const char_t *header, const size_t headerSize ); /** @@ -377,29 +401,49 @@ errorret_t saveStreamWriteDateImpl( ); /** - * Reads the contents of a save slot from the stream into the save file - * struct. Use saveFileRead* macros to deserialize fields one at a time. + * Reads a self-contained save meta section (header, version, fields, + * checksum verification) from the stream. * - * @param stream Active read stream for this slot. - * @param file Save file struct to populate. + * @param stream Active read stream, positioned at the section's start. + * @param meta Meta struct to populate. * @return An error code if loading fails. */ -errorret_t saveFileLoad(savestream_t *stream, savefile_t *file); +errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta); /** - * Writes the contents of the save file struct into the stream. - * Use saveFileWrite* macros to serialize fields one at a time. + * Writes a self-contained save meta section (header, version, fields, + * checksum) to the stream. * - * @param stream Active write stream for this slot. - * @param file Save file struct to serialize. + * @param stream Active write stream, positioned at the section's start. + * @param meta Meta struct to serialize. * @return An error code if writing fails. */ -errorret_t saveFileWrite(savestream_t *stream, savefile_t *file); +errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta); -#define saveFileReadHeader(stream, header) \ - errorChain(saveStreamReadHeaderImpl(stream, header)) -#define saveFileWriteHeader(stream, header) \ - errorChain(saveStreamWriteHeaderImpl(stream, header)) +/** + * Reads a self-contained save slot section (header, version, fields, + * checksum verification) from the stream. + * + * @param stream Active read stream, positioned at the section's start. + * @param slot Slot struct to populate. + * @return An error code if loading fails. + */ +errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot); + +/** + * Writes a self-contained save slot section (header, version, fields, + * checksum) to the stream. + * + * @param stream Active write stream, positioned at the section's start. + * @param slot Slot struct to serialize. + * @return An error code if writing fails. + */ +errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot); + +#define saveFileReadHeader(stream, header, expected, size) \ + errorChain(saveStreamReadHeaderImpl(stream, header, expected, size)) +#define saveFileWriteHeader(stream, header, size) \ + errorChain(saveStreamWriteHeaderImpl(stream, header, size)) #define saveFileReadVersion(stream, out) \ errorChain(saveStreamReadVersionImpl(stream, out)) @@ -465,4 +509,3 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file); errorChain(saveStreamReadDateImpl(stream, out)) #define saveFileWriteDate(stream, input) \ errorChain(saveStreamWriteDateImpl(stream, input)) - diff --git a/src/dusk/save/settings.c b/src/dusk/save/settings.c deleted file mode 100644 index 6abf0299..00000000 --- a/src/dusk/save/settings.c +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settings.h" -#include "save/settingsstream.h" -#include "util/memory.h" -#include "error/error.h" - -settings_t SETTINGS; - -static errorret_t settingsLoad(void) { - settingsstream_t stream; - memoryZero(&stream, sizeof(settingsstream_t)); - - #ifdef settingsStreamOpenReadPlatform - errorret_t openRet = settingsStreamOpenReadPlatform(&stream); - SETTINGS.available = errorIsOk(openRet); - errorChain(openRet); - #endif - - if(!stream.found) errorOk(); - - errorret_t ret = settingsFileLoad(&stream, &SETTINGS.file); - - #ifdef settingsStreamClosePlatform - settingsStreamClosePlatform(&stream); - #endif - - if(errorIsOk(ret)) ret = settingsStreamVerifyChecksumImpl(&stream); - SETTINGS.file.exists = errorIsOk(ret); - errorChain(ret); - errorOk(); -} - -errorret_t settingsInit(void) { - memoryZero(&SETTINGS, sizeof(settings_t)); - SETTINGS.file.deadzone = SETTINGS_DEADZONE_DEFAULT; - - #ifdef settingsInitPlatform - // A missing/unreachable storage medium is expected, recoverable state, - // not a reason to fail booting the whole game - log it and carry on - // with SETTINGS.available false instead of chaining the error upward. - errorret_t result = settingsInitPlatform(); - SETTINGS.available = errorIsOk(result); - if(!SETTINGS.available) errorCatch(errorPrint(result)); - #else - SETTINGS.available = false; - #endif - - if(SETTINGS.available) { - errorret_t loadRet = settingsLoad(); - if(errorIsNotOk(loadRet)) errorCatch(errorPrint(loadRet)); - } - - errorOk(); -} - -bool_t settingsIsAvailable(void) { - return SETTINGS.available; -} - -errorret_t settingsDispose(void) { - #ifdef settingsDisposePlatform - errorChain(settingsDisposePlatform()); - #endif - errorOk(); -} - -errorret_t settingsSave(void) { - memoryCopy( - SETTINGS.file.header, SETTINGS_FILE_HEADER, SETTINGS_FILE_HEADER_SIZE - ); - SETTINGS.file.version = SETTINGS_FILE_VERSION; - - settingsstream_t stream; - memoryZero(&stream, sizeof(settingsstream_t)); - - #ifdef settingsStreamOpenWritePlatform - errorret_t openRet = settingsStreamOpenWritePlatform(&stream); - SETTINGS.available = errorIsOk(openRet); - errorChain(openRet); - #endif - - errorret_t ret = settingsFileWrite(&stream, &SETTINGS.file); - - if(errorIsOk(ret)) { - ret = settingsStreamFinalizeWriteImpl(&stream); - } - - #ifdef settingsStreamClosePlatform - settingsStreamClosePlatform(&stream); - #endif - - SETTINGS.file.exists = errorIsOk(ret); - errorChain(ret); - errorOk(); -} - -settingsfile_t * settingsGet(void) { - return &SETTINGS.file; -} diff --git a/src/dusk/save/settings.h b/src/dusk/save/settings.h deleted file mode 100644 index 31fe40d8..00000000 --- a/src/dusk/save/settings.h +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "settingsfile.h" -#include "save/settingsplatform.h" - -typedef struct { - /** The single settings file - see settingsfile.h. */ - settingsfile_t file; - /** Platform-specific settings storage state (paths, card handles, etc.). */ - settingsplatform_t platform; - /** - * True if the settings storage medium was reachable the last time it - * was checked - at settingsInit(), and refreshed by every subsequent - * settingsSave() attempt. Missing storage is expected (e.g. no memory - * card/stick inserted), not fatal - see settingsIsAvailable(). - */ - bool_t available; -} settings_t; - -extern settings_t SETTINGS; - -/** - * Initializes the settings system and immediately loads the settings file - * if one exists, so SETTINGS.file reflects the last-saved values as soon - * as this returns - unlike save.h's per-slot game saves (which need an - * explicit "new game vs continue" choice before loading), there's no such - * ambiguity for device-wide settings, so this always loads eagerly. - * - * Never fails the way settingsSave() can - if the storage medium isn't - * reachable, that's logged and reflected in settingsIsAvailable() rather - * than treated as fatal, since the game should still be playable with - * just the built-in defaults. - * - * @return An error code only for unexpected platform failures. - */ -errorret_t settingsInit(void); - -/** - * Checks whether the settings storage medium was reachable as of the last - * load/save attempt. - * - * @return true if the settings medium was available last time it was - * checked. - */ -bool_t settingsIsAvailable(void); - -/** - * Disposes of the settings system. - * - * @return An error code if disposal fails. - */ -errorret_t settingsDispose(void); - -/** - * Writes the current settings to persistent storage. Always synchronous - - * unlike save.h's game saves, no platform needs a multi-frame native - * dialog for this, so there's no callback/busy-polling to deal with. - * - * @return An error code if the write fails. - */ -errorret_t settingsSave(void); - -/** - * Gets a pointer to the live settings data. Modify fields directly, then - * call settingsSave() to persist them. - * - * @return A pointer to the settings file. - */ -settingsfile_t * settingsGet(void); diff --git a/src/dusk/save/settingsfile.h b/src/dusk/save/settingsfile.h deleted file mode 100644 index 0537c81e..00000000 --- a/src/dusk/save/settingsfile.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** Settings file format version. Increment on breaking change. */ -#define SETTINGS_FILE_VERSION 1 - -/** Magic bytes that identify a Dusk settings file. */ -#define SETTINGS_FILE_HEADER "DST" - -/** Byte length of the magic header (excludes the null terminator). */ -#define SETTINGS_FILE_HEADER_SIZE (sizeof(SETTINGS_FILE_HEADER) - 1) - -/** - * Default gamepad deadzone for a settings file that's never actually been - * loaded from disk yet (see settingsInit(), which stamps this on first - * boot) - the settings file is the single source of truth for this value - * (see settingsfile_t.deadzone); nothing else stores or defaults it. - */ -#define SETTINGS_DEADZONE_DEFAULT 0.1f - -/** - * Device/user-wide preferences, independent of any individual game save - * slot (see savefile.h) - there's exactly one of these, not one per slot, - * since a setting like gamepad deadzone shouldn't reset or diverge just - * because the player started a new game in a different slot. - */ -typedef struct { - /** Magic header bytes read from the file; must equal SETTINGS_FILE_HEADER. */ - char_t header[SETTINGS_FILE_HEADER_SIZE]; - /** Format version read from the file; used to branch on older layouts. */ - uint32_t version; - /** Runtime flag - true if settings were successfully loaded or written. */ - bool_t exists; - /** - * User-configured gamepad deadzone (0.0f-1.0f) - the settings file is - * the only place this lives; read it directly via settingsGet()->deadzone - * rather than caching it anywhere else. - */ - float_t deadzone; -} settingsfile_t; diff --git a/src/dusk/save/settingsstream.c b/src/dusk/save/settingsstream.c deleted file mode 100644 index 857357b8..00000000 --- a/src/dusk/save/settingsstream.c +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settingsstream.h" -#include "util/crypt.h" -#include "util/endian.h" - -errorret_t settingsStreamReadBytesRawImpl( - settingsstream_t *stream, void *buf, const size_t len -) { - #ifdef settingsStreamReadBytesPlatform - errorChain(settingsStreamReadBytesPlatform(stream, buf, len)); - #endif - errorOk(); -} - -errorret_t settingsStreamWriteBytesRawImpl( - settingsstream_t *stream, const void *buf, const size_t len -) { - #ifdef settingsStreamWriteBytesPlatform - errorChain(settingsStreamWriteBytesPlatform(stream, buf, len)); - #endif - errorOk(); -} - -errorret_t settingsStreamReadBytesImpl( - settingsstream_t *stream, void *buf, const size_t len -) { - errorChain(settingsStreamReadBytesRawImpl(stream, buf, len)); - cryptCRC32Update(&stream->checksum, buf, len); - errorOk(); -} - -errorret_t settingsStreamWriteBytesImpl( - settingsstream_t *stream, const void *buf, const size_t len -) { - cryptCRC32Update(&stream->checksum, buf, len); - errorChain(settingsStreamWriteBytesRawImpl(stream, buf, len)); - errorOk(); -} - -errorret_t settingsStreamFinalizeWriteImpl(settingsstream_t *stream) { - uint32_t finalCRC = cryptCRC32End(stream->checksum); - uint32_t leChecksum = endianLittleToHost32(finalCRC); - - #ifdef settingsStreamSeekPlatform - errorChain(settingsStreamSeekPlatform(stream, SETTINGS_FILE_HEADER_SIZE)); - #endif - - errorChain(settingsStreamWriteBytesRawImpl( - stream, &leChecksum, sizeof(uint32_t) - )); - errorOk(); -} - -errorret_t settingsStreamVerifyChecksumImpl(settingsstream_t *stream) { - uint32_t computed = cryptCRC32End(stream->checksum); - if(computed != stream->expectedChecksum) { - errorThrow("Settings file has invalid checksum"); - } - errorOk(); -} - -errorret_t settingsStreamReadHeaderImpl( - settingsstream_t *stream, char_t header[SETTINGS_FILE_HEADER_SIZE] -) { - errorChain(settingsStreamReadBytesRawImpl( - stream, header, SETTINGS_FILE_HEADER_SIZE - )); - - if( - header[0] != SETTINGS_FILE_HEADER[0] || - header[1] != SETTINGS_FILE_HEADER[1] || - header[2] != SETTINGS_FILE_HEADER[2] - ) { - errorThrow("Settings file has invalid header"); - } - - uint32_t leChecksum; - errorChain(settingsStreamReadBytesRawImpl( - stream, &leChecksum, sizeof(uint32_t) - )); - stream->expectedChecksum = endianLittleToHost32(leChecksum); - stream->checksum = cryptCRC32Begin(); - errorOk(); -} - -errorret_t settingsStreamWriteHeaderImpl( - settingsstream_t *stream, const char_t header[SETTINGS_FILE_HEADER_SIZE] -) { - errorChain(settingsStreamWriteBytesRawImpl( - stream, header, SETTINGS_FILE_HEADER_SIZE - )); - - uint32_t placeholder = 0; - errorChain(settingsStreamWriteBytesRawImpl( - stream, &placeholder, sizeof(uint32_t) - )); - stream->checksum = cryptCRC32Begin(); - errorOk(); -} - -errorret_t settingsStreamReadVersionImpl(settingsstream_t *stream, uint32_t *out) { - uint32_t raw; - errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(uint32_t))); - *out = endianLittleToHost32(raw); - errorOk(); -} - -errorret_t settingsStreamWriteVersionImpl( - settingsstream_t *stream, const uint32_t *input -) { - uint32_t raw = endianLittleToHost32(*input); - errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t))); - errorOk(); -} - -errorret_t settingsStreamReadBoolImpl(settingsstream_t *stream, bool_t *out) { - uint8_t raw; - errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(uint8_t))); - *out = (bool_t)(raw != 0); - errorOk(); -} - -errorret_t settingsStreamWriteBoolImpl( - settingsstream_t *stream, const bool_t *input -) { - uint8_t raw = *input ? 1 : 0; - errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t))); - errorOk(); -} - -errorret_t settingsStreamReadFloatImpl(settingsstream_t *stream, float_t *out) { - float_t raw; - errorChain(settingsStreamReadBytesImpl(stream, &raw, sizeof(float_t))); - *out = endianLittleToHostFloat(raw); - errorOk(); -} - -errorret_t settingsStreamWriteFloatImpl( - settingsstream_t *stream, const float_t *input -) { - float_t raw = endianLittleToHostFloat(*input); - errorChain(settingsStreamWriteBytesImpl(stream, &raw, sizeof(float_t))); - errorOk(); -} - -errorret_t settingsFileLoad(settingsstream_t *stream, settingsfile_t *file) { - settingsFileReadHeader(stream, file->header); - settingsFileReadVersion(stream, &file->version); - settingsFileReadFloat(stream, &file->deadzone); - errorOk(); -} - -errorret_t settingsFileWrite(settingsstream_t *stream, settingsfile_t *file) { - settingsFileWriteHeader(stream, file->header); - settingsFileWriteVersion(stream, &file->version); - settingsFileWriteFloat(stream, &file->deadzone); - errorOk(); -} diff --git a/src/dusk/save/settingsstream.h b/src/dusk/save/settingsstream.h deleted file mode 100644 index 890e400b..00000000 --- a/src/dusk/save/settingsstream.h +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "settingsfile.h" -#include "save/settingsplatform.h" - -typedef struct { - bool_t found; - uint32_t checksum; - uint32_t expectedChecksum; - settingsplatformstream_t platform; -} settingsstream_t; - -/** - * Reads bytes from the platform stream without updating the CRC. - * - * @param stream Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if the read fails. - */ -errorret_t settingsStreamReadBytesRawImpl( - settingsstream_t *stream, void *buf, const size_t len -); - -/** - * Writes bytes to the platform stream without updating the CRC. - * - * @param stream Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteBytesRawImpl( - settingsstream_t *stream, const void *buf, const size_t len -); - -/** - * Reads bytes from the platform stream and accumulates them into the CRC. - * - * @param stream Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if the read fails. - */ -errorret_t settingsStreamReadBytesImpl( - settingsstream_t *stream, void *buf, const size_t len -); - -/** - * Updates the CRC then writes bytes to the platform stream. - * - * @param stream Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteBytesImpl( - settingsstream_t *stream, const void *buf, const size_t len -); - -/** - * Finalizes a write stream: computes the final CRC32, seeks to the - * checksum field in the header, and writes it in little-endian order. - * - * @param stream Active write stream. - * @return An error if the seek or write fails. - */ -errorret_t settingsStreamFinalizeWriteImpl(settingsstream_t *stream); - -/** - * Verifies that the CRC32 accumulated during loading matches the value - * stored in the file header. - * - * @param stream Active read stream (loading must be complete). - * @return An error if the checksum does not match. - */ -errorret_t settingsStreamVerifyChecksumImpl(settingsstream_t *stream); - -/** - * Reads and validates the magic header, then reads the stored CRC32 and - * resets the running accumulator. - * - * @param stream Active read stream. - * @param header Buffer of SETTINGS_FILE_HEADER_SIZE bytes to receive the - * header. - * @return An error if the header is missing or invalid. - */ -errorret_t settingsStreamReadHeaderImpl( - settingsstream_t *stream, char_t header[SETTINGS_FILE_HEADER_SIZE] -); - -/** - * Writes the magic header and a zero CRC32 placeholder, then resets the - * running accumulator. - * - * @param stream Active write stream. - * @param header Buffer of SETTINGS_FILE_HEADER_SIZE bytes to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteHeaderImpl( - settingsstream_t *stream, - const char_t header[SETTINGS_FILE_HEADER_SIZE] -); - -/** - * Reads a little-endian uint32 version field from the stream. - * - * @param stream Active read stream. - * @param out Receives the host-order value. - * @return An error if the read fails. - */ -errorret_t settingsStreamReadVersionImpl(settingsstream_t *stream, uint32_t *out); - -/** - * Writes a uint32 version field to the stream in little-endian order. - * - * @param stream Active write stream. - * @param input Value to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteVersionImpl( - settingsstream_t *stream, const uint32_t *input -); - -/** - * Reads a single byte as a boolean (0 = false, non-zero = true). - * - * @param stream Active read stream. - * @param out Receives the boolean value. - * @return An error if the read fails. - */ -errorret_t settingsStreamReadBoolImpl(settingsstream_t *stream, bool_t *out); - -/** - * Writes a boolean as a single byte (true = 1, false = 0). - * - * @param stream Active write stream. - * @param input Value to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteBoolImpl( - settingsstream_t *stream, const bool_t *input -); - -/** - * Reads a little-endian float from the stream. - * - * @param stream Active read stream. - * @param out Receives the host-order value. - * @return An error if the read fails. - */ -errorret_t settingsStreamReadFloatImpl(settingsstream_t *stream, float_t *out); - -/** - * Writes a float to the stream in little-endian order. - * - * @param stream Active write stream. - * @param input Value to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteFloatImpl( - settingsstream_t *stream, const float_t *input -); - -/** - * Reads the contents of the settings file from the stream. - * - * @param stream Active read stream. - * @param file Settings file struct to populate. - * @return An error code if loading fails. - */ -errorret_t settingsFileLoad(settingsstream_t *stream, settingsfile_t *file); - -/** - * Writes the contents of the settings file struct into the stream. - * - * @param stream Active write stream. - * @param file Settings file struct to serialize. - * @return An error code if writing fails. - */ -errorret_t settingsFileWrite(settingsstream_t *stream, settingsfile_t *file); - -#define settingsFileReadHeader(stream, header) \ - errorChain(settingsStreamReadHeaderImpl(stream, header)) -#define settingsFileWriteHeader(stream, header) \ - errorChain(settingsStreamWriteHeaderImpl(stream, header)) - -#define settingsFileReadVersion(stream, out) \ - errorChain(settingsStreamReadVersionImpl(stream, out)) -#define settingsFileWriteVersion(stream, input) \ - errorChain(settingsStreamWriteVersionImpl(stream, input)) - -#define settingsFileReadBool(stream, out) \ - errorChain(settingsStreamReadBoolImpl(stream, out)) -#define settingsFileWriteBool(stream, input) \ - errorChain(settingsStreamWriteBoolImpl(stream, input)) - -#define settingsFileReadFloat(stream, out) \ - errorChain(settingsStreamReadFloatImpl(stream, out)) -#define settingsFileWriteFloat(stream, input) \ - errorChain(settingsStreamWriteFloatImpl(stream, input)) diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index d3fbd6fb..21582071 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -49,7 +49,7 @@ static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) { return; } - saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL); + saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL); } // Determines whether there's actually save data to overwrite (not just @@ -57,7 +57,7 @@ static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) { // what lets a fresh memory card/stick, with no prior save on it yet, be // told apart from one that already has our data on it. Cheap either way // (a single sector/file read), and correct on every platform without any -// platform-specific UI code - saveExists() already reflects each +// platform-specific UI code - saveSlotExists() already reflects each // platform's own notion of "found something." static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) { if(errorIsNotOk(result)) { @@ -68,8 +68,8 @@ static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) { return; } - if(saveExists(SAVE_ACTIVE_SLOT)) { - saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL); + if(saveSlotExists(SAVE_ACTIVE_SLOT)) { + saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL); return; } @@ -87,7 +87,7 @@ static void uiGameMenuSave(void) { } if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up. - saveLoad(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL); + saveLoadSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL); } uigamemenu_t UI_GAME_MENU; diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/src/dusk/ui/frame/settings/uisettingsinput.c index 7a9edcd4..f56d0f2b 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.c +++ b/src/dusk/ui/frame/settings/uisettingsinput.c @@ -11,7 +11,11 @@ #include "util/memory.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" -#include "save/settings.h" +#include "save/save.h" + +static void uiSettingsInputSaveComplete(errorret_t result, void *user) { + if(errorIsNotOk(result)) errorCatch(errorPrint(result)); +} void uiSettingsInputSelected( const uimenu_t *menu, @@ -39,7 +43,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) { UI_SETTINGS_INPUT_LABEL_MAX )); MENU_SLIDER_FLOAT( - input->deadzoneLabel, SETTINGS_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f + input->deadzoneLabel, SAVE_META_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f ); #else MENU_LABEL("No input settings yet"); @@ -55,17 +59,17 @@ void uiSettingsInputLoad(void) { #ifdef DUSK_INPUT_GAMEPAD uiSliderSetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, - settingsGet()->deadzone + saveGetMeta()->deadzone ); #endif } void uiSettingsInputApply(void) { #ifdef DUSK_INPUT_GAMEPAD - settingsGet()->deadzone = uiSliderGetFloat( + saveGetMeta()->deadzone = uiSliderGetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider ); - errorCatch(errorPrint(settingsSave())); + saveWriteMeta(uiSettingsInputSaveComplete, NULL); #endif uiMenuClose(&UI_SETTINGS.data.input.menu); } @@ -74,7 +78,7 @@ bool_t uiSettingsInputHasChanges(void) { #ifdef DUSK_INPUT_GAMEPAD if(uiSliderGetFloat( &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider - ) != settingsGet()->deadzone) return true; + ) != saveGetMeta()->deadzone) return true; #endif return false; diff --git a/src/duskdolphin/input/inputdolphin.c b/src/duskdolphin/input/inputdolphin.c index c1f6d436..366fb5e8 100644 --- a/src/duskdolphin/input/inputdolphin.c +++ b/src/duskdolphin/input/inputdolphin.c @@ -9,7 +9,7 @@ #include "assert/assert.h" #include "log/log.h" #include "util/string.h" -#include "save/settings.h" +#include "save/save.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { #ifdef DUSK_INPUT_GAMEPAD @@ -188,5 +188,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) { } float_t inputGetDeadzoneDolphin(const inputbutton_t button) { - return settingsGet()->deadzone; + return saveGetMeta()->deadzone; } \ No newline at end of file diff --git a/src/duskdolphin/save/CMakeLists.txt b/src/duskdolphin/save/CMakeLists.txt index 46683ef0..95b853df 100644 --- a/src/duskdolphin/save/CMakeLists.txt +++ b/src/duskdolphin/save/CMakeLists.txt @@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savedolphin.c savestreamdolphin.c - settingsdolphin.c - settingsstreamdolphin.c ) diff --git a/src/duskdolphin/save/savedolphin.c b/src/duskdolphin/save/savedolphin.c index 464f0ad5..f8660138 100644 --- a/src/duskdolphin/save/savedolphin.c +++ b/src/duskdolphin/save/savedolphin.c @@ -6,6 +6,7 @@ */ #include "save/save.h" +#include "save/savestream.h" #include "util/memory.h" #include "util/string.h" @@ -65,129 +66,76 @@ errorret_t saveDisposeDolphin(void) { errorOk(); } -errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) { - char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; - saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX); +errorret_t saveCombinedLoadDolphin(void) { + savestream_t stream; + memoryZero(&stream, sizeof(savestream_t)); - int32_t result; - do { - result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile - ); - } while(result == CARD_ERROR_BUSY); - if(result == CARD_ERROR_NOFILE) { - file->exists = false; - errorOk(); - } - if(result < 0) { - file->exists = false; - errorThrow("Failed to open memory card file for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result - ); + errorret_t openRet = saveStreamOpenReadPlatform(&stream); + SAVE.available = errorIsOk(openRet); + errorChain(openRet); + + if(!stream.found) errorOk(); + + errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta); + for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) { + ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]); } - void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE); - if(!buffer) { - CARD_Close(&SAVE.platform.cardFile); - errorThrow("Failed to allocate memory card read buffer"); - } + #ifdef saveStreamClosePlatform + saveStreamClosePlatform(&stream); + #endif - do { - result = CARD_Read( - &SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0 - ); - } while(result == CARD_ERROR_BUSY); - CARD_Close(&SAVE.platform.cardFile); - - if(result < 0) { - memoryFree(buffer); - file->exists = false; - errorThrow("Failed to read memory card data for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result - ); - } - - memoryCopy(file, buffer, sizeof(savefile_t)); - memoryFree(buffer); - - file->exists = true; + errorChain(ret); errorOk(); } -errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) { - char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; - saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX); +errorret_t saveCombinedWriteDolphin(void) { + savestream_t stream; + memoryZero(&stream, sizeof(savestream_t)); - void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE); - if(!buffer) { - errorThrow("Failed to allocate memory card write buffer"); - } - memoryZero(buffer, SAVE_DOLPHIN_SECTOR_SIZE); - memoryCopy(buffer, file, sizeof(savefile_t)); + errorret_t openRet = saveStreamOpenWritePlatform(&stream); + SAVE.available = errorIsOk(openRet); + errorChain(openRet); - // Try open existing file first; create if absent. - int32_t result; - do { - result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile - ); - } while(result == CARD_ERROR_BUSY); - if(result == CARD_ERROR_NOFILE) { - do { - result = CARD_Create( - SAVE_DOLPHIN_CHANNEL, - fileName, - SAVE_DOLPHIN_SECTOR_SIZE, - &SAVE.platform.cardFile - ); - } while(result == CARD_ERROR_BUSY); + errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta); + for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) { + ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]); } - if(result < 0) { - memoryFree(buffer); - errorThrow("Failed to open/create memory card file for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result - ); - } - - do { - result = CARD_Write( - &SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0 - ); - } while(result == CARD_ERROR_BUSY); - CARD_Close(&SAVE.platform.cardFile); - memoryFree(buffer); - - if(result < 0) { - errorThrow("Failed to write memory card data for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result - ); - } + #ifdef saveStreamClosePlatform + saveStreamClosePlatform(&stream); + #endif + errorChain(ret); errorOk(); } -errorret_t saveDeleteDolphin(const uint8_t slot) { - char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; - saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX); - - int32_t result; - do { - result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName); - } while(result == CARD_ERROR_BUSY); - if(result < 0 && result != CARD_ERROR_NOFILE) { - errorThrow("Failed to delete memory card file for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result - ); - } - - errorOk(); +errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) { + (void)slot; + (void)out; + return saveCombinedLoadDolphin(); } -void saveGetFileNameDolphin( - const uint8_t slot, char_t *out, const size_t max -) { - snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot); +errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) { + (void)slot; + (void)slotData; + return saveCombinedWriteDolphin(); +} + +errorret_t saveSlotDeleteDolphin(const uint8_t slot) { + memoryZero(&SAVE.slots[slot], sizeof(saveslot_t)); + SAVE.slots[slot].exists = false; + return saveCombinedWriteDolphin(); +} + +errorret_t saveMetaLoadDolphin(savemeta_t *out) { + (void)out; + return saveCombinedLoadDolphin(); +} + +errorret_t saveMetaWriteDolphin(savemeta_t *meta) { + (void)meta; + return saveCombinedWriteDolphin(); } const char_t *saveCardErrorStringDolphin(const int32_t result) { diff --git a/src/duskdolphin/save/savedolphin.h b/src/duskdolphin/save/savedolphin.h index c6a72aa0..96180e67 100644 --- a/src/duskdolphin/save/savedolphin.h +++ b/src/duskdolphin/save/savedolphin.h @@ -7,10 +7,10 @@ #pragma once #include "error/error.h" -#include "save/savefile.h" +#include "save/saveslot.h" +#include "save/savemeta.h" #include -#define SAVE_DOLPHIN_FILE_NAME_MAX 32 #define SAVE_DOLPHIN_SECTOR_SIZE 8192 #ifndef SAVE_DOLPHIN_GAME_CODE @@ -21,6 +21,17 @@ #define SAVE_DOLPHIN_CHANNEL CARD_SLOTA #endif +/** + * Fixed memory card file name holding meta + every save slot, back to + * back, in one file - GameCube memory cards are small enough that one + * consolidated file (rather than one per slot, plus a separate one for + * meta) meaningfully saves card space, and nothing needs true random + * access into just one section (see saveCombinedLoadDolphin()). + */ +#ifndef SAVE_DOLPHIN_FILE_NAME + #define SAVE_DOLPHIN_FILE_NAME "DUSK_SAVE" +#endif + typedef struct { card_file cardFile; uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32))); @@ -42,42 +53,60 @@ errorret_t saveInitDolphin(void); errorret_t saveDisposeDolphin(void); /** - * Loads a save file from the memory card for the given slot. + * Reads the one consolidated card file (SAVE_DOLPHIN_FILE_NAME) into + * SAVE.meta and every SAVE.slots[i], in order. Not finding the file is + * not an error - SAVE.meta/SAVE.slots simply keep their compiled + * defaults. * - * @param slot The save slot index. - * @param file Output save file data. - * @return An error code if the load fails. + * @return An error code if the card is mounted but the read/parse fails. */ -errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file); +errorret_t saveCombinedLoadDolphin(void); /** - * Writes a save file to the memory card for the given slot. + * Writes SAVE.meta and every SAVE.slots[i], in order, into the one + * consolidated card file (SAVE_DOLPHIN_FILE_NAME), creating it if needed. * - * @param slot The save slot index. - * @param file Save file data to write. * @return An error code if the write fails. */ -errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file); +errorret_t saveCombinedWriteDolphin(void); /** - * Deletes the save file for the given slot from the memory card. + * Save-slot platform entry point - always (re)reads the whole + * consolidated file (see saveCombinedLoadDolphin()); slot/out are unused + * since every slot is populated in the same pass. + */ +errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out); + +/** + * Save-slot platform entry point - always (re)writes the whole + * consolidated file (see saveCombinedWriteDolphin()); slot/slotData are + * unused since every slot is written in the same pass. + */ +errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData); + +/** + * Deletes a single save slot's data (zeroes it in memory) and re-writes + * the consolidated file - the file itself always exists as long as any + * slot or meta does, so "delete" can't remove the file wholesale. * * @param slot The save slot index. * @return An error code if the delete fails. */ -errorret_t saveDeleteDolphin(const uint8_t slot); +errorret_t saveSlotDeleteDolphin(const uint8_t slot); /** - * Builds the memory card file name for a given save slot, from - * SAVE_DOLPHIN_GAME_CODE and the slot index. - * - * @param slot The save slot index. - * @param out Destination buffer for the file name. - * @param max Size of out, in bytes. + * Meta platform entry point - always (re)reads the whole consolidated + * file (see saveCombinedLoadDolphin()); out is unused since meta is + * populated in the same pass. */ -void saveGetFileNameDolphin( - const uint8_t slot, char_t *out, const size_t max -); +errorret_t saveMetaLoadDolphin(savemeta_t *out); + +/** + * Meta platform entry point - always (re)writes the whole consolidated + * file (see saveCombinedWriteDolphin()); meta is unused since it's + * written in the same pass. + */ +errorret_t saveMetaWriteDolphin(savemeta_t *meta); /** * Describes a libogc CARD_ERROR_* result code (see diff --git a/src/duskdolphin/save/saveplatform.h b/src/duskdolphin/save/saveplatform.h index 510b5d26..dc8144ae 100644 --- a/src/duskdolphin/save/saveplatform.h +++ b/src/duskdolphin/save/saveplatform.h @@ -14,12 +14,17 @@ typedef savestreamdolphin_t saveplatformstream_t; #define saveInitPlatform saveInitDolphin #define saveDisposePlatform saveDisposeDolphin -#define saveDeletePlatform saveDeleteDolphin -#define saveStreamOpenReadPlatform(stream, slot) \ - saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found, slot) -#define saveStreamOpenWritePlatform(stream, slot) \ - saveStreamOpenWriteDolphin(&(stream)->platform, slot) +#define saveSlotDeletePlatform saveSlotDeleteDolphin +#define saveSlotLoadPlatform saveSlotLoadDolphin +#define saveSlotWritePlatform saveSlotWriteDolphin +#define saveMetaLoadPlatform saveMetaLoadDolphin +#define saveMetaWritePlatform saveMetaWriteDolphin + +#define saveStreamOpenReadPlatform(stream) \ + saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found) +#define saveStreamOpenWritePlatform(stream) \ + saveStreamOpenWriteDolphin(&(stream)->platform) #define saveStreamClosePlatform(stream) \ saveStreamCloseDolphin(&(stream)->platform) #define saveStreamReadBytesPlatform(stream, buf, len) \ @@ -28,3 +33,5 @@ typedef savestreamdolphin_t saveplatformstream_t; saveStreamWriteBytesDolphin(&(stream)->platform, buf, len) #define saveStreamSeekPlatform(stream, pos) \ saveStreamSeekDolphin(&(stream)->platform, pos) +#define saveStreamTellPlatform(stream, out) \ + saveStreamTellDolphin(&(stream)->platform, out) diff --git a/src/duskdolphin/save/savestreamdolphin.c b/src/duskdolphin/save/savestreamdolphin.c index 1240ebb6..d79418af 100644 --- a/src/duskdolphin/save/savestreamdolphin.c +++ b/src/duskdolphin/save/savestreamdolphin.c @@ -8,29 +8,17 @@ #include "save/save.h" #include "save/savestreamdolphin.h" #include "util/memory.h" -#include "util/string.h" -static void _saveStreamGetFileName( - char_t *out, const size_t max, const uint8_t slot -) { - snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot); -} - -errorret_t saveStreamOpenReadDolphin( - savestreamdolphin_t *p, bool_t *found, const uint8_t slot -) { +errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found) { if(!SAVE.platform.mounted) { *found = false; errorThrow("No memory card mounted"); } - char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; - _saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot); - int32_t result; do { result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile + SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile ); } while(result == CARD_ERROR_BUSY); if(result == CARD_ERROR_NOFILE) { @@ -41,8 +29,8 @@ errorret_t saveStreamOpenReadDolphin( } if(result < 0) { *found = false; - errorThrow("Failed to open memory card file for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result + errorThrow("Failed to open memory card file: %s (%d)", + saveCardErrorStringDolphin(result), result ); } @@ -52,44 +40,40 @@ errorret_t saveStreamOpenReadDolphin( CARD_Close(&p->cardFile); if(result < 0) { *found = false; - errorThrow("Failed to read memory card data for slot %u: %s (%d)", - (uint32_t)slot, saveCardErrorStringDolphin(result), result + errorThrow("Failed to read memory card data: %s (%d)", + saveCardErrorStringDolphin(result), result ); } *found = true; p->position = 0; p->writing = false; - p->slot = slot; errorOk(); } -errorret_t saveStreamOpenWriteDolphin( - savestreamdolphin_t *p, const uint8_t slot -) { +errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p) { if(!SAVE.platform.mounted) errorThrow("No memory card mounted"); memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE); p->position = 0; p->writing = true; - p->slot = slot; errorOk(); } void saveStreamCloseDolphin(savestreamdolphin_t *p) { if(!p->writing) return; - char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; - _saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot); - int32_t result; do { - result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile); + result = CARD_Open( + SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile + ); } while(result == CARD_ERROR_BUSY); if(result == CARD_ERROR_NOFILE) { do { result = CARD_Create( - SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile + SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, + SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile ); } while(result == CARD_ERROR_BUSY); } @@ -129,3 +113,8 @@ errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos) { p->position = pos; errorOk(); } + +errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out) { + *out = p->position; + errorOk(); +} diff --git a/src/duskdolphin/save/savestreamdolphin.h b/src/duskdolphin/save/savestreamdolphin.h index 2b351fe6..e84cbf61 100644 --- a/src/duskdolphin/save/savestreamdolphin.h +++ b/src/duskdolphin/save/savestreamdolphin.h @@ -20,34 +20,28 @@ typedef struct { size_t position; /** True when opened for writing; flushes buffer to card on close. */ bool_t writing; - /** Slot index stored at open time so Close can derive the filename. */ - uint8_t slot; } savestreamdolphin_t; /** - * Opens a memory card slot for reading by loading its sector into buffer. + * Opens the consolidated memory card file for reading by loading its + * sector into buffer. * * @param p Stream to initialize. * @param found Set to true if the file exists, false if it does not. - * @param slot Save slot index. * @return An error if reading the card fails for a reason other than * missing file. */ -errorret_t saveStreamOpenReadDolphin( - savestreamdolphin_t *p, bool_t *found, const uint8_t slot -); +errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found); /** - * Opens a memory card slot for writing by zeroing the sector buffer. - * The buffer is flushed to the card when savestreamCloseDolphin is called. + * Opens the consolidated memory card file for writing by zeroing the + * sector buffer. The buffer is flushed to the card when + * saveStreamCloseDolphin is called. * - * @param p Stream to initialize. - * @param slot Save slot index. + * @param p Stream to initialize. * @return An error if initialization fails. */ -errorret_t saveStreamOpenWriteDolphin( - savestreamdolphin_t *p, const uint8_t slot -); +errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p); /** * Flushes the sector buffer to the memory card (write mode only) and @@ -89,3 +83,12 @@ errorret_t saveStreamWriteBytesDolphin( * @return An error if pos is out of range. */ errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos); + +/** + * Gets the current read/write position within the sector buffer. + * + * @param p Active stream. + * @param out Receives the current position. + * @return An error - always succeeds, matches saveStreamTellImpl's shape. + */ +errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out); diff --git a/src/duskdolphin/save/settingsdolphin.c b/src/duskdolphin/save/settingsdolphin.c deleted file mode 100644 index 14a505f0..00000000 --- a/src/duskdolphin/save/settingsdolphin.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/save.h" -#include "save/settings.h" - -errorret_t settingsInitDolphin(void) { - if(!SAVE.platform.mounted) { - errorThrow("No memory card mounted"); - } - errorOk(); -} - -errorret_t settingsDisposeDolphin(void) { - errorOk(); -} diff --git a/src/duskdolphin/save/settingsdolphin.h b/src/duskdolphin/save/settingsdolphin.h deleted file mode 100644 index c95ed07f..00000000 --- a/src/duskdolphin/save/settingsdolphin.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/settingsfile.h" - -#ifndef SETTINGS_DOLPHIN_FILE_NAME - #define SETTINGS_DOLPHIN_FILE_NAME "DUSK_CFG" -#endif - -typedef struct { - /** No dedicated state - settings reuse the memory card mount that the - * game-save system (see savedolphin.h) already establishes. */ - uint8_t reserved; -} settingsdolphin_t; - -/** - * Initializes the settings system on GameCube. Does not mount the memory - * card itself - reuses the mount already established by the game-save - * system (see saveInitDolphin()), since there's only one memory card - * subsystem to go around. settingsInit() must therefore run after - * saveInit() on this platform. - * - * @return An error code if no memory card is currently mounted. - */ -errorret_t settingsInitDolphin(void); - -/** - * Disposes of the settings system on GameCube. Does not unmount the - * memory card - that's owned by the game-save system. - * - * @return An error code if disposal fails. - */ -errorret_t settingsDisposeDolphin(void); diff --git a/src/duskdolphin/save/settingsplatform.h b/src/duskdolphin/save/settingsplatform.h deleted file mode 100644 index 2216a5f5..00000000 --- a/src/duskdolphin/save/settingsplatform.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/settingsdolphin.h" -#include "save/settingsstreamdolphin.h" - -typedef settingsdolphin_t settingsplatform_t; -typedef settingsstreamdolphin_t settingsplatformstream_t; - -#define settingsInitPlatform settingsInitDolphin -#define settingsDisposePlatform settingsDisposeDolphin - -#define settingsStreamOpenReadPlatform(stream) \ - settingsStreamOpenReadDolphin(&(stream)->platform, &(stream)->found) -#define settingsStreamOpenWritePlatform(stream) \ - settingsStreamOpenWriteDolphin(&(stream)->platform) -#define settingsStreamClosePlatform(stream) \ - settingsStreamCloseDolphin(&(stream)->platform) -#define settingsStreamReadBytesPlatform(stream, buf, len) \ - settingsStreamReadBytesDolphin(&(stream)->platform, buf, len) -#define settingsStreamWriteBytesPlatform(stream, buf, len) \ - settingsStreamWriteBytesDolphin(&(stream)->platform, buf, len) -#define settingsStreamSeekPlatform(stream, pos) \ - settingsStreamSeekDolphin(&(stream)->platform, pos) diff --git a/src/duskdolphin/save/settingsstreamdolphin.c b/src/duskdolphin/save/settingsstreamdolphin.c deleted file mode 100644 index 95848c97..00000000 --- a/src/duskdolphin/save/settingsstreamdolphin.c +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/save.h" -#include "save/settingsdolphin.h" -#include "save/settingsstreamdolphin.h" -#include "util/memory.h" - -errorret_t settingsStreamOpenReadDolphin( - settingsstreamdolphin_t *p, bool_t *found -) { - if(!SAVE.platform.mounted) { - *found = false; - errorThrow("No memory card mounted"); - } - - int32_t result; - do { - result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, &p->cardFile - ); - } while(result == CARD_ERROR_BUSY); - if(result == CARD_ERROR_NOFILE) { - *found = false; - p->position = 0; - p->writing = false; - errorOk(); - } - if(result < 0) { - *found = false; - errorThrow("Failed to open memory card settings file: %s (%d)", - saveCardErrorStringDolphin(result), result - ); - } - - do { - result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); - } while(result == CARD_ERROR_BUSY); - CARD_Close(&p->cardFile); - if(result < 0) { - *found = false; - errorThrow("Failed to read memory card settings data: %s (%d)", - saveCardErrorStringDolphin(result), result - ); - } - - *found = true; - p->position = 0; - p->writing = false; - errorOk(); -} - -errorret_t settingsStreamOpenWriteDolphin(settingsstreamdolphin_t *p) { - if(!SAVE.platform.mounted) errorThrow("No memory card mounted"); - - memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE); - p->position = 0; - p->writing = true; - errorOk(); -} - -void settingsStreamCloseDolphin(settingsstreamdolphin_t *p) { - if(!p->writing) return; - - int32_t result; - do { - result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, &p->cardFile - ); - } while(result == CARD_ERROR_BUSY); - if(result == CARD_ERROR_NOFILE) { - do { - result = CARD_Create( - SAVE_DOLPHIN_CHANNEL, SETTINGS_DOLPHIN_FILE_NAME, - SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile - ); - } while(result == CARD_ERROR_BUSY); - } - - do { - result = CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); - } while(result == CARD_ERROR_BUSY); - CARD_Close(&p->cardFile); -} - -errorret_t settingsStreamReadBytesDolphin( - settingsstreamdolphin_t *p, void *buf, const size_t len -) { - if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) { - errorThrow("Settings stream read exceeds sector size"); - } - memoryCopy(buf, p->buffer + p->position, len); - p->position += len; - errorOk(); -} - -errorret_t settingsStreamWriteBytesDolphin( - settingsstreamdolphin_t *p, const void *buf, const size_t len -) { - if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) { - errorThrow("Settings stream write exceeds sector size"); - } - memoryCopy(p->buffer + p->position, buf, len); - p->position += len; - errorOk(); -} - -errorret_t settingsStreamSeekDolphin( - settingsstreamdolphin_t *p, const size_t pos -) { - if(pos >= SAVE_DOLPHIN_SECTOR_SIZE) { - errorThrow("Settings stream seek out of range"); - } - p->position = pos; - errorOk(); -} diff --git a/src/duskdolphin/save/settingsstreamdolphin.h b/src/duskdolphin/save/settingsstreamdolphin.h deleted file mode 100644 index 59dbbd8b..00000000 --- a/src/duskdolphin/save/settingsstreamdolphin.h +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/savedolphin.h" -#include -#include - -typedef struct { - /** libogc memory card file handle. */ - card_file cardFile; - /** In-memory sector buffer; all reads and writes operate on this. */ - uint8_t buffer[SAVE_DOLPHIN_SECTOR_SIZE] __attribute__((aligned(32))); - /** Current read/write position within buffer. */ - size_t position; - /** True when opened for writing; flushes buffer to card on close. */ - bool_t writing; -} settingsstreamdolphin_t; - -/** - * Opens the settings memory card file for reading by loading its sector - * into buffer. - * - * @param p Stream to initialize. - * @param found Set to true if the file exists, false if it does not. - * @return An error if reading the card fails for a reason other than - * missing file. - */ -errorret_t settingsStreamOpenReadDolphin( - settingsstreamdolphin_t *p, bool_t *found -); - -/** - * Opens the settings memory card file for writing by zeroing the sector - * buffer. The buffer is flushed to the card when - * settingsStreamCloseDolphin is called. - * - * @param p Stream to initialize. - * @return An error if initialization fails. - */ -errorret_t settingsStreamOpenWriteDolphin(settingsstreamdolphin_t *p); - -/** - * Flushes the sector buffer to the memory card (write mode only) and - * releases the card file handle. - * - * @param p Stream to close. - */ -void settingsStreamCloseDolphin(settingsstreamdolphin_t *p); - -/** - * Copies len bytes from the sector buffer at the current position into buf. - * - * @param p Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if the read would exceed the sector size. - */ -errorret_t settingsStreamReadBytesDolphin( - settingsstreamdolphin_t *p, void *buf, const size_t len -); - -/** - * Copies len bytes from buf into the sector buffer at the current position. - * - * @param p Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write would exceed the sector size. - */ -errorret_t settingsStreamWriteBytesDolphin( - settingsstreamdolphin_t *p, const void *buf, const size_t len -); - -/** - * Sets the current read/write position within the sector buffer. - * - * @param p Active stream. - * @param pos Target byte offset from the start of the sector. - * @return An error if pos is out of range. - */ -errorret_t settingsStreamSeekDolphin( - settingsstreamdolphin_t *p, const size_t pos -); diff --git a/src/dusklinux/input/inputlinux.c b/src/dusklinux/input/inputlinux.c index 0455ebd0..5cf39fb0 100644 --- a/src/dusklinux/input/inputlinux.c +++ b/src/dusklinux/input/inputlinux.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/settings.h" +#include "save/save.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { #ifdef DUSK_INPUT_GAMEPAD @@ -548,5 +548,5 @@ errorret_t inputInitLinux(void) { } float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return settingsGet()->deadzone; + return saveGetMeta()->deadzone; } \ No newline at end of file diff --git a/src/dusklinux/save/CMakeLists.txt b/src/dusklinux/save/CMakeLists.txt index 99edc030..5714936f 100644 --- a/src/dusklinux/save/CMakeLists.txt +++ b/src/dusklinux/save/CMakeLists.txt @@ -7,7 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savelinux.c - savestreamlinux.c - settingslinux.c - settingsstreamlinux.c + savejsonlinux.c ) diff --git a/src/dusklinux/save/savejsonlinux.c b/src/dusklinux/save/savejsonlinux.c new file mode 100644 index 00000000..080b24a3 --- /dev/null +++ b/src/dusklinux/save/savejsonlinux.c @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/savejsonlinux.h" +#include "util/string.h" +#include + +errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer) { + writer->doc = yyjson_mut_doc_new(NULL); + if(!writer->doc) { + errorThrow("Failed to allocate JSON document"); + } + writer->root = yyjson_mut_obj(writer->doc); + yyjson_mut_doc_set_root(writer->doc, writer->root); + errorOk(); +} + +void saveJsonWriterAddUInt32Linux( + savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value +) { + yyjson_mut_obj_add_uint(writer->doc, writer->root, key, (uint64_t)value); +} + +void saveJsonWriterAddFloatLinux( + savejsonwriterlinux_t *writer, const char_t *key, const float_t value +) { + yyjson_mut_obj_add_real(writer->doc, writer->root, key, (double)value); +} + +void saveJsonWriterAddStringLinux( + savejsonwriterlinux_t *writer, const char_t *key, const char_t *value +) { + yyjson_mut_obj_add_strcpy(writer->doc, writer->root, key, value); +} + +void saveJsonWriterAddBoolArrayLinux( + savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values, + const size_t count +) { + yyjson_mut_val *arr = yyjson_mut_arr(writer->doc); + for(size_t i = 0; i < count; i++) { + yyjson_mut_arr_add_bool(writer->doc, arr, values[i]); + } + yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr); +} + +void saveJsonWriterAddUInt8ArrayLinux( + savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values, + const size_t count +) { + yyjson_mut_val *arr = yyjson_mut_arr(writer->doc); + for(size_t i = 0; i < count; i++) { + yyjson_mut_arr_add_uint(writer->doc, arr, (uint64_t)values[i]); + } + yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr); +} + +errorret_t saveJsonWriterSaveLinux( + savejsonwriterlinux_t *writer, const char_t *path +) { + yyjson_write_err err; + if(!yyjson_mut_write_file( + path, writer->doc, YYJSON_WRITE_PRETTY, NULL, &err + )) { + errorThrow("Failed to write %s: %s", path, err.msg); + } + errorOk(); +} + +void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer) { + if(writer->doc) { + yyjson_mut_doc_free(writer->doc); + writer->doc = NULL; + } +} + +errorret_t saveJsonReaderOpenLinux( + const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot, + bool_t *found +) { + *outDoc = NULL; + *outRoot = NULL; + + struct stat st; + if(stat(path, &st) != 0) { + *found = false; + errorOk(); + } + + yyjson_read_err err; + *outDoc = yyjson_read_file( + path, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, + NULL, &err + ); + if(!*outDoc) { + *found = false; + errorThrow("Failed to parse %s: %s", path, err.msg); + } + + *outRoot = yyjson_doc_get_root(*outDoc); + *found = true; + errorOk(); +} + +uint32_t saveJsonReadUInt32Linux( + yyjson_val *root, const char_t *key, const uint32_t defaultValue +) { + yyjson_val *val = yyjson_obj_get(root, key); + if(!val || !yyjson_is_num(val)) return defaultValue; + return (uint32_t)yyjson_get_uint(val); +} + +float_t saveJsonReadFloatLinux( + yyjson_val *root, const char_t *key, const float_t defaultValue +) { + yyjson_val *val = yyjson_obj_get(root, key); + if(!val || !yyjson_is_num(val)) return defaultValue; + return (float_t)yyjson_get_num(val); +} + +void saveJsonReadStringLinux( + yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen, + const char_t *defaultValue +) { + yyjson_val *val = yyjson_obj_get(root, key); + const char_t *src = defaultValue; + if(val && yyjson_is_str(val)) src = yyjson_get_str(val); + stringCopy(out, src, maxLen); +} + +void saveJsonReadBoolArrayLinux( + yyjson_val *root, const char_t *key, bool_t *out, const size_t count +) { + yyjson_val *arr = yyjson_obj_get(root, key); + if(!arr || !yyjson_is_arr(arr)) return; + + size_t idx, len; + yyjson_val *elem; + yyjson_arr_foreach(arr, idx, len, elem) { + if(idx >= count) break; + if(yyjson_is_bool(elem)) out[idx] = yyjson_get_bool(elem); + } +} + +void saveJsonReadUInt8ArrayLinux( + yyjson_val *root, const char_t *key, uint8_t *out, const size_t count +) { + yyjson_val *arr = yyjson_obj_get(root, key); + if(!arr || !yyjson_is_arr(arr)) return; + + size_t idx, len; + yyjson_val *elem; + yyjson_arr_foreach(arr, idx, len, elem) { + if(idx >= count) break; + if(yyjson_is_int(elem)) out[idx] = (uint8_t)yyjson_get_int(elem); + } +} diff --git a/src/dusklinux/save/savejsonlinux.h b/src/dusklinux/save/savejsonlinux.h new file mode 100644 index 00000000..01a08180 --- /dev/null +++ b/src/dusklinux/save/savejsonlinux.h @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "yyjson.h" + +/** + * Small helper around a yyjson mutable document, used to build up a save + * file's fields one at a time before writing it out. Schema-agnostic - + * knows nothing about saveslot_t/savemeta_t; the caller supplies field + * names and values. + */ +typedef struct { + yyjson_mut_doc *doc; + yyjson_mut_val *root; +} savejsonwriterlinux_t; + +/** + * Creates a new mutable JSON document with an empty root object. + * + * @param writer Writer to initialize. + * @return An error if the document can't be allocated. + */ +errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer); + +/** + * Adds a "version": field to the root object. + */ +void saveJsonWriterAddUInt32Linux( + savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value +); + +/** + * Adds a float field to the root object (written as a JSON number). + */ +void saveJsonWriterAddFloatLinux( + savejsonwriterlinux_t *writer, const char_t *key, const float_t value +); + +/** + * Adds a string field to the root object. The value is copied into the + * document, so the caller's buffer doesn't need to outlive the call. + */ +void saveJsonWriterAddStringLinux( + savejsonwriterlinux_t *writer, const char_t *key, const char_t *value +); + +/** + * Adds a JSON array of booleans as a field on the root object. + */ +void saveJsonWriterAddBoolArrayLinux( + savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values, + const size_t count +); + +/** + * Adds a JSON array of unsigned 8-bit integers as a field on the root + * object. + */ +void saveJsonWriterAddUInt8ArrayLinux( + savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values, + const size_t count +); + +/** + * Pretty-prints the document to the given file path, creating or + * truncating it. + * + * @param writer Writer holding the document to write. + * @param path Destination file path. + * @return An error if the write fails. + */ +errorret_t saveJsonWriterSaveLinux( + savejsonwriterlinux_t *writer, const char_t *path +); + +/** + * Frees the document. Safe to call even if saveJsonWriterInitLinux() + * failed partway. + * + * @param writer Writer to dispose. + */ +void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer); + +/** + * Reads and parses a JSON file, returning its root object. + * + * @param path File path to read. + * @param outDoc Receives the parsed document (must be freed via + * yyjson_doc_free() once done, regardless of found/error outcome). + * @param outRoot Receives the root object, or NULL if not found. + * @param found Set to true if the file exists, false if it does not + * (not finding the file is not an error). + * @return An error if the file exists but fails to parse. + */ +errorret_t saveJsonReaderOpenLinux( + const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot, + bool_t *found +); + +/** + * Reads a uint32 field, falling back to defaultValue if the key is + * missing or not a number - a hand-edited file shouldn't hard-fail the + * whole load over one bad/missing field. + */ +uint32_t saveJsonReadUInt32Linux( + yyjson_val *root, const char_t *key, const uint32_t defaultValue +); + +/** + * Reads a float field, falling back to defaultValue if the key is + * missing or not a number. + */ +float_t saveJsonReadFloatLinux( + yyjson_val *root, const char_t *key, const float_t defaultValue +); + +/** + * Reads a string field into out, falling back to defaultValue if the key + * is missing or not a string. Always null-terminates. + */ +void saveJsonReadStringLinux( + yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen, + const char_t *defaultValue +); + +/** + * Reads a JSON array of booleans into out, up to count entries. Missing + * key, non-array value, or a shorter array all leave the remaining/all + * entries untouched (caller should zero the buffer first). + */ +void saveJsonReadBoolArrayLinux( + yyjson_val *root, const char_t *key, bool_t *out, const size_t count +); + +/** + * Reads a JSON array of unsigned 8-bit integers into out, up to count + * entries. Same forgiving semantics as saveJsonReadBoolArrayLinux(). + */ +void saveJsonReadUInt8ArrayLinux( + yyjson_val *root, const char_t *key, uint8_t *out, const size_t count +); diff --git a/src/dusklinux/save/savelinux.c b/src/dusklinux/save/savelinux.c index 0b672464..465bdf94 100644 --- a/src/dusklinux/save/savelinux.c +++ b/src/dusklinux/save/savelinux.c @@ -6,8 +6,9 @@ */ #include "save/save.h" +#include "save/savejsonlinux.h" #include "util/string.h" -#include +#include "util/memory.h" #include #include @@ -25,60 +26,121 @@ errorret_t saveDisposeLinux(void) { errorOk(); } -errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file) { - char_t path[SAVE_LINUX_PATH_MAX]; - snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT, - SAVE.platform.savePath, (uint32_t)slot +static void _saveSlotPathLinux( + char_t *out, const size_t max, const uint8_t slot +) { + snprintf( + out, max, SAVE_LINUX_SLOT_FILE_FORMAT, SAVE.platform.savePath, + (uint32_t)slot ); +} - FILE *f = fopen(path, "rb"); - if(!f) { - file->exists = false; - errorOk(); - } +static void _saveMetaPathLinux(char_t *out, const size_t max) { + snprintf(out, max, SAVE_LINUX_META_FILE_FORMAT, SAVE.platform.savePath); +} - size_t read = fread(file, sizeof(savefile_t), 1, f); - fclose(f); +errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out) { + char_t path[SAVE_LINUX_PATH_MAX]; + _saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot); - if(read != 1) { - file->exists = false; - errorThrow("Failed to read save data for slot %u", (uint32_t)slot); - } + yyjson_doc *doc; + yyjson_val *root; + bool_t found; + errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found); + if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); } + if(!found) errorOk(); - file->exists = true; + memoryZero(out, sizeof(saveslot_t)); + out->version = saveJsonReadUInt32Linux(root, "version", SAVE_SLOT_VERSION); + saveJsonReadStringLinux( + root, "playerName", out->playerName, SAVE_PLAYER_NAME_MAX, "" + ); + saveJsonReadBoolArrayLinux( + root, "globalItemCollected", out->globalItemCollected, + SAVE_GLOBAL_ITEM_COUNT_MAX + ); + saveJsonReadUInt8ArrayLinux( + root, "storyFlags", out->storyFlags, SAVE_STORY_FLAG_COUNT_MAX + ); + out->exists = true; + + yyjson_doc_free(doc); errorOk(); } -errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file) { +errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData) { char_t path[SAVE_LINUX_PATH_MAX]; - snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT, - SAVE.platform.savePath, (uint32_t)slot + _saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot); + + slotData->version = SAVE_SLOT_VERSION; + + savejsonwriterlinux_t writer; + errorChain(saveJsonWriterInitLinux(&writer)); + saveJsonWriterAddUInt32Linux(&writer, "version", slotData->version); + saveJsonWriterAddStringLinux(&writer, "playerName", slotData->playerName); + saveJsonWriterAddBoolArrayLinux( + &writer, "globalItemCollected", slotData->globalItemCollected, + SAVE_GLOBAL_ITEM_COUNT_MAX + ); + saveJsonWriterAddUInt8ArrayLinux( + &writer, "storyFlags", slotData->storyFlags, SAVE_STORY_FLAG_COUNT_MAX ); - FILE *f = fopen(path, "wb"); - if(!f) { - errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot); - } - - size_t written = fwrite(file, sizeof(savefile_t), 1, f); - fclose(f); - - if(written != 1) { - errorThrow("Failed to write save data for slot %u", (uint32_t)slot); - } + errorret_t ret = saveJsonWriterSaveLinux(&writer, path); + saveJsonWriterDisposeLinux(&writer); + errorChain(ret); + slotData->exists = true; errorOk(); } -errorret_t saveDeleteLinux(const uint8_t slot) { +errorret_t saveDeleteSlotLinux(const uint8_t slot) { char_t path[SAVE_LINUX_PATH_MAX]; - snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT, - SAVE.platform.savePath, (uint32_t)slot - ); + _saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot); if(remove(path) != 0 && errno != ENOENT) { - errorThrow("Failed to delete save file for slot %u", (uint32_t)slot); + errorThrow("Failed to delete save slot %u: %s", (uint32_t)slot, path); } errorOk(); } + +errorret_t saveMetaLoadLinux(savemeta_t *out) { + char_t path[SAVE_LINUX_PATH_MAX]; + _saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX); + + yyjson_doc *doc; + yyjson_val *root; + bool_t found; + errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found); + if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); } + if(!found) errorOk(); + + out->version = saveJsonReadUInt32Linux(root, "version", SAVE_META_VERSION); + out->deadzone = saveJsonReadFloatLinux( + root, "deadzone", SAVE_META_DEADZONE_DEFAULT + ); + out->exists = true; + + yyjson_doc_free(doc); + errorOk(); +} + +errorret_t saveMetaWriteLinux(savemeta_t *meta) { + char_t path[SAVE_LINUX_PATH_MAX]; + _saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX); + + meta->version = SAVE_META_VERSION; + + savejsonwriterlinux_t writer; + errorChain(saveJsonWriterInitLinux(&writer)); + saveJsonWriterAddUInt32Linux(&writer, "version", meta->version); + saveJsonWriterAddFloatLinux(&writer, "deadzone", meta->deadzone); + + errorret_t ret = saveJsonWriterSaveLinux(&writer, path); + saveJsonWriterDisposeLinux(&writer); + errorChain(ret); + + meta->exists = true; + errorOk(); +} diff --git a/src/dusklinux/save/savelinux.h b/src/dusklinux/save/savelinux.h index 1f99066e..86f9d801 100644 --- a/src/dusklinux/save/savelinux.h +++ b/src/dusklinux/save/savelinux.h @@ -7,10 +7,12 @@ #pragma once #include "error/error.h" -#include "save/savefile.h" +#include "save/saveslot.h" +#include "save/savemeta.h" #define SAVE_LINUX_PATH_MAX FILENAME_MAX -#define SAVE_LINUX_FILE_FORMAT "%s/save_%u.dat" +#define SAVE_LINUX_SLOT_FILE_FORMAT "%s/slot%u.json" +#define SAVE_LINUX_META_FILE_FORMAT "%s/settings.json" #ifndef SAVE_LINUX_PATH #define SAVE_LINUX_PATH "./saves" @@ -21,7 +23,8 @@ typedef struct { } savelinux_t; /** - * Initializes the save system on Linux. + * Initializes the save system on Linux - ensures the save directory + * exists (shared by both save slots and meta). * * @return An error code if initialization fails. */ @@ -35,27 +38,43 @@ errorret_t saveInitLinux(void); errorret_t saveDisposeLinux(void); /** - * Loads a save file from disk for the given slot. + * Loads a save slot as JSON (slotN.json) from disk, if it exists. * * @param slot The save slot index. - * @param file Output save file data. - * @return An error code if the load fails. + * @param out Output slot data. + * @return An error code if the slot exists but fails to parse. */ -errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file); +errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out); /** - * Writes a save file to disk for the given slot. + * Writes a save slot as JSON (slotN.json) to disk. * * @param slot The save slot index. - * @param file Save file data to write. + * @param slotData Slot data to write. * @return An error code if the write fails. */ -errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file); +errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData); /** - * Deletes the save file for the given slot from disk. + * Deletes the save slot JSON file for the given index. * * @param slot The save slot index. * @return An error code if the delete fails. */ -errorret_t saveDeleteLinux(const uint8_t slot); +errorret_t saveDeleteSlotLinux(const uint8_t slot); + +/** + * Loads save meta as JSON (settings.json) from disk, if it exists. + * + * @param out Output meta data. + * @return An error code if the file exists but fails to parse. + */ +errorret_t saveMetaLoadLinux(savemeta_t *out); + +/** + * Writes save meta as JSON (settings.json) to disk. + * + * @param meta Meta data to write. + * @return An error code if the write fails. + */ +errorret_t saveMetaWriteLinux(savemeta_t *meta); diff --git a/src/dusklinux/save/saveplatform.h b/src/dusklinux/save/saveplatform.h index 24f80972..a122bb17 100644 --- a/src/dusklinux/save/saveplatform.h +++ b/src/dusklinux/save/saveplatform.h @@ -7,24 +7,21 @@ #pragma once #include "save/savelinux.h" -#include "save/savestreamlinux.h" typedef savelinux_t saveplatform_t; -typedef savestreamlinux_t saveplatformstream_t; +// Linux fully overrides every save operation with JSON I/O (see +// savelinux.c/savejsonlinux.c) - nothing generic ever opens a +// savestream_t here, so this only needs to exist for that shared type to +// compile. +typedef struct { + uint8_t reserved; +} saveplatformstream_t; #define saveInitPlatform saveInitLinux #define saveDisposePlatform saveDisposeLinux -#define saveDeletePlatform saveDeleteLinux -#define saveStreamOpenReadPlatform(stream, slot) \ - saveStreamOpenReadLinux(&(stream)->platform, &(stream)->found, slot) -#define saveStreamOpenWritePlatform(stream, slot) \ - saveStreamOpenWriteLinux(&(stream)->platform, slot) -#define saveStreamClosePlatform(stream) \ - saveStreamCloseLinux(&(stream)->platform) -#define saveStreamReadBytesPlatform(stream, buf, len) \ - saveStreamReadBytesLinux(&(stream)->platform, buf, len) -#define saveStreamWriteBytesPlatform(stream, buf, len) \ - saveStreamWriteBytesLinux(&(stream)->platform, buf, len) -#define saveStreamSeekPlatform(stream, pos) \ - saveStreamSeekLinux(&(stream)->platform, pos) +#define saveSlotDeletePlatform saveDeleteSlotLinux +#define saveSlotLoadPlatform saveSlotLoadLinux +#define saveSlotWritePlatform saveSlotWriteLinux +#define saveMetaLoadPlatform saveMetaLoadLinux +#define saveMetaWritePlatform saveMetaWriteLinux diff --git a/src/dusklinux/save/savestreamlinux.c b/src/dusklinux/save/savestreamlinux.c deleted file mode 100644 index e4879ad4..00000000 --- a/src/dusklinux/save/savestreamlinux.c +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/save.h" -#include "save/savestreamlinux.h" -#include "util/string.h" -#include -#include - -static void _saveStreamGetPath( - char_t *out, const size_t max, const uint8_t slot -) { - snprintf( - out, max, SAVE_LINUX_FILE_FORMAT, - SAVE.platform.savePath, (uint32_t)slot - ); -} - -errorret_t saveStreamOpenReadLinux( - savestreamlinux_t *p, bool_t *found, const uint8_t slot -) { - char_t path[SAVE_LINUX_PATH_MAX]; - _saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot); - - p->file = fopen(path, "rb"); - *found = (p->file != NULL); - errorOk(); -} - -errorret_t saveStreamOpenWriteLinux(savestreamlinux_t *p, const uint8_t slot) { - char_t path[SAVE_LINUX_PATH_MAX]; - _saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot); - - p->file = fopen(path, "wb"); - if(!p->file) { - errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot); - } - errorOk(); -} - -void saveStreamCloseLinux(savestreamlinux_t *p) { - if(p->file) { - fclose(p->file); - p->file = NULL; - } -} - -errorret_t saveStreamReadBytesLinux( - savestreamlinux_t *p, void *buf, const size_t len -) { - if(fread(buf, 1, len, p->file) != len) { - errorThrow("Unexpected end of save file"); - } - errorOk(); -} - -errorret_t saveStreamWriteBytesLinux( - savestreamlinux_t *p, const void *buf, const size_t len -) { - if(fwrite(buf, 1, len, p->file) != len) { - errorThrow("Failed to write save data"); - } - errorOk(); -} - -errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos) { - if(fseek(p->file, (long)pos, SEEK_SET) != 0) { - errorThrow("Failed to seek in save file"); - } - errorOk(); -} diff --git a/src/dusklinux/save/savestreamlinux.h b/src/dusklinux/save/savestreamlinux.h deleted file mode 100644 index 3d8a1f58..00000000 --- a/src/dusklinux/save/savestreamlinux.h +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include -#include - -typedef struct { - FILE *file; -} savestreamlinux_t; - -/** - * Opens a save slot file for reading. - * - * @param p Stream to initialize. - * @param found Set to true if the file exists, false if it does not. - * @param slot Save slot index. - * @return An error if the open fails for a reason other than missing file. - */ -errorret_t saveStreamOpenReadLinux( - savestreamlinux_t *p, bool_t *found, const uint8_t slot -); - -/** - * Opens a save slot file for writing, creating or truncating it. - * - * @param p Stream to initialize. - * @param slot Save slot index. - * @return An error if the file cannot be opened for writing. - */ -errorret_t saveStreamOpenWriteLinux( - savestreamlinux_t *p, const uint8_t slot -); - -/** - * Closes the file handle held by the stream. - * - * @param p Stream to close. - */ -void saveStreamCloseLinux(savestreamlinux_t *p); - -/** - * Reads len bytes from the stream into buf. - * - * @param p Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if fewer than len bytes are available. - */ -errorret_t saveStreamReadBytesLinux( - savestreamlinux_t *p, void *buf, const size_t len -); - -/** - * Writes len bytes from buf into the stream. - * - * @param p Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write fails. - */ -errorret_t saveStreamWriteBytesLinux( - savestreamlinux_t *p, const void *buf, const size_t len -); - -/** - * Seeks to an absolute byte position within the stream. - * - * @param p Active stream. - * @param pos Target byte offset from the start of the file. - * @return An error if the seek fails. - */ -errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos); diff --git a/src/dusklinux/save/settingslinux.c b/src/dusklinux/save/settingslinux.c deleted file mode 100644 index 0200378a..00000000 --- a/src/dusklinux/save/settingslinux.c +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settings.h" -#include "util/string.h" -#include -#include - -errorret_t settingsInitLinux(void) { - stringCopy( - SETTINGS.platform.path, SETTINGS_LINUX_PATH, SETTINGS_LINUX_PATH_MAX - ); - - if(mkdir(SETTINGS.platform.path, 0755) != 0 && errno != EEXIST) { - errorThrow( - "Failed to create settings directory: %s", SETTINGS.platform.path - ); - } - - errorOk(); -} - -errorret_t settingsDisposeLinux(void) { - errorOk(); -} diff --git a/src/dusklinux/save/settingslinux.h b/src/dusklinux/save/settingslinux.h deleted file mode 100644 index da1655c3..00000000 --- a/src/dusklinux/save/settingslinux.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/settingsfile.h" - -#define SETTINGS_LINUX_PATH_MAX FILENAME_MAX -#define SETTINGS_LINUX_FILE_FORMAT "%s/settings.dat" - -#ifndef SETTINGS_LINUX_PATH - #define SETTINGS_LINUX_PATH "./saves" -#endif - -typedef struct { - char_t path[SETTINGS_LINUX_PATH_MAX]; -} settingslinux_t; - -/** - * Initializes the settings system on Linux - ensures the settings - * directory exists, independent of whether the game-save system (see - * savelinux.h) has been initialized. - * - * @return An error code if initialization fails. - */ -errorret_t settingsInitLinux(void); - -/** - * Disposes of the settings system on Linux. - * - * @return An error code if disposal fails. - */ -errorret_t settingsDisposeLinux(void); diff --git a/src/dusklinux/save/settingsplatform.h b/src/dusklinux/save/settingsplatform.h deleted file mode 100644 index 1e1ab163..00000000 --- a/src/dusklinux/save/settingsplatform.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/settingslinux.h" -#include "save/settingsstreamlinux.h" - -typedef settingslinux_t settingsplatform_t; -typedef settingsstreamlinux_t settingsplatformstream_t; - -#define settingsInitPlatform settingsInitLinux -#define settingsDisposePlatform settingsDisposeLinux - -#define settingsStreamOpenReadPlatform(stream) \ - settingsStreamOpenReadLinux(&(stream)->platform, &(stream)->found) -#define settingsStreamOpenWritePlatform(stream) \ - settingsStreamOpenWriteLinux(&(stream)->platform) -#define settingsStreamClosePlatform(stream) \ - settingsStreamCloseLinux(&(stream)->platform) -#define settingsStreamReadBytesPlatform(stream, buf, len) \ - settingsStreamReadBytesLinux(&(stream)->platform, buf, len) -#define settingsStreamWriteBytesPlatform(stream, buf, len) \ - settingsStreamWriteBytesLinux(&(stream)->platform, buf, len) -#define settingsStreamSeekPlatform(stream, pos) \ - settingsStreamSeekLinux(&(stream)->platform, pos) diff --git a/src/dusklinux/save/settingsstreamlinux.c b/src/dusklinux/save/settingsstreamlinux.c deleted file mode 100644 index 702c53d1..00000000 --- a/src/dusklinux/save/settingsstreamlinux.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settings.h" -#include "save/settingsstreamlinux.h" -#include "util/string.h" - -static void _settingsStreamGetPath(char_t *out, const size_t max) { - snprintf(out, max, SETTINGS_LINUX_FILE_FORMAT, SETTINGS.platform.path); -} - -errorret_t settingsStreamOpenReadLinux( - settingsstreamlinux_t *p, bool_t *found -) { - char_t path[SETTINGS_LINUX_PATH_MAX]; - _settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX); - - p->file = fopen(path, "rb"); - *found = (p->file != NULL); - errorOk(); -} - -errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p) { - char_t path[SETTINGS_LINUX_PATH_MAX]; - _settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX); - - p->file = fopen(path, "wb"); - if(!p->file) { - errorThrow("Failed to open settings file for writing"); - } - errorOk(); -} - -void settingsStreamCloseLinux(settingsstreamlinux_t *p) { - if(p->file) { - fclose(p->file); - p->file = NULL; - } -} - -errorret_t settingsStreamReadBytesLinux( - settingsstreamlinux_t *p, void *buf, const size_t len -) { - if(fread(buf, 1, len, p->file) != len) { - errorThrow("Unexpected end of settings file"); - } - errorOk(); -} - -errorret_t settingsStreamWriteBytesLinux( - settingsstreamlinux_t *p, const void *buf, const size_t len -) { - if(fwrite(buf, 1, len, p->file) != len) { - errorThrow("Failed to write settings data"); - } - errorOk(); -} - -errorret_t settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos) { - if(fseek(p->file, (long)pos, SEEK_SET) != 0) { - errorThrow("Failed to seek in settings file"); - } - errorOk(); -} diff --git a/src/dusklinux/save/settingsstreamlinux.h b/src/dusklinux/save/settingsstreamlinux.h deleted file mode 100644 index 2110d170..00000000 --- a/src/dusklinux/save/settingsstreamlinux.h +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include -#include - -typedef struct { - FILE *file; -} settingsstreamlinux_t; - -/** - * Opens the settings file for reading. - * - * @param p Stream to initialize. - * @param found Set to true if the file exists, false if it does not. - * @return An error if the open fails for a reason other than missing file. - */ -errorret_t settingsStreamOpenReadLinux( - settingsstreamlinux_t *p, bool_t *found -); - -/** - * Opens the settings file for writing, creating or truncating it. - * - * @param p Stream to initialize. - * @return An error if the file cannot be opened for writing. - */ -errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p); - -/** - * Closes the file handle held by the stream. - * - * @param p Stream to close. - */ -void settingsStreamCloseLinux(settingsstreamlinux_t *p); - -/** - * Reads len bytes from the stream into buf. - * - * @param p Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if fewer than len bytes are available. - */ -errorret_t settingsStreamReadBytesLinux( - settingsstreamlinux_t *p, void *buf, const size_t len -); - -/** - * Writes len bytes from buf into the stream. - * - * @param p Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteBytesLinux( - settingsstreamlinux_t *p, const void *buf, const size_t len -); - -/** - * Seeks to an absolute byte position within the stream. - * - * @param p Active stream. - * @param pos Target byte offset from the start of the file. - * @return An error if the seek fails. - */ -errorret_t settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos); diff --git a/src/duskpsp/input/inputpsp.c b/src/duskpsp/input/inputpsp.c index 4a01f8b7..4132c924 100644 --- a/src/duskpsp/input/inputpsp.c +++ b/src/duskpsp/input/inputpsp.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/settings.h" +#include "save/save.h" // #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM // #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM @@ -95,5 +95,5 @@ errorret_t inputInitPSP(void) { } float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return settingsGet()->deadzone; + return saveGetMeta()->deadzone; } \ No newline at end of file diff --git a/src/duskpsp/save/CMakeLists.txt b/src/duskpsp/save/CMakeLists.txt index 6708114a..76c70403 100644 --- a/src/duskpsp/save/CMakeLists.txt +++ b/src/duskpsp/save/CMakeLists.txt @@ -8,6 +8,12 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savepsp.c savestreampsp.c - settingspsp.c - settingsstreampsp.c +) + +# PSP only needs one Dusk-side save slot - a future main-menu save picker +# will let players manage multiple named saves through the OS's own +# sceUtilitySavedata browser instead of Dusk maintaining its own numbered +# slots (see save/saveslot.h for the default used by every other platform). +target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + SAVE_SLOT_COUNT_MAX=1 ) diff --git a/src/duskpsp/save/saveplatform.h b/src/duskpsp/save/saveplatform.h index c63c5d50..6f5ea474 100644 --- a/src/duskpsp/save/saveplatform.h +++ b/src/duskpsp/save/saveplatform.h @@ -14,7 +14,7 @@ typedef savestreampsp_t saveplatformstream_t; #define saveInitPlatform saveInitPSP #define saveDisposePlatform saveDisposePSP -#define saveDeletePlatform saveDeletePSP +#define saveSlotDeletePlatform saveDeleteSlotPSP #define saveStreamReadBytesPlatform(stream, buf, len) \ saveStreamReadBytesPSP(&(stream)->platform, buf, len) @@ -22,16 +22,32 @@ typedef savestreampsp_t saveplatformstream_t; saveStreamWriteBytesPSP(&(stream)->platform, buf, len) #define saveStreamSeekPlatform(stream, pos) \ saveStreamSeekPSP(&(stream)->platform, pos) +#define saveStreamTellPlatform(stream, out) \ + saveStreamTellPSP(&(stream)->platform, out) // Save/load go entirely through the native sceUtilitySavedata dialog // (savePSPBeginSave/Load), which spans multiple frames - these bypass // save.c's normal synchronous open/write-fields/close flow above (that's // still used internally, just against an in-memory buffer, from within -// savePSPBeginSave/Load themselves) and are what save.c's saveWrite()/ -// saveLoad() actually call on this platform. -#define saveAsyncWritePlatform(slot, onComplete, user) \ +// savePSPBeginSave/Load themselves) and are what save.c's saveWriteSlot()/ +// saveLoadSlot()/saveWriteMeta()/saveLoadMeta() actually call on this +// platform. Meta and the (one) slot are serialized together into the same +// buffer - there is no separate meta-only path on PSP - so the meta +// variants just drive the same dialog against SAVE_ACTIVE_SLOT. +#define saveSlotAsyncWritePlatform(slot, onComplete, user) \ savePSPBeginSave(slot, onComplete, user) -#define saveAsyncLoadPlatform(slot, onComplete, user) \ +#define saveSlotAsyncLoadPlatform(slot, onComplete, user) \ savePSPBeginLoad(slot, onComplete, user) +#define saveMetaAsyncWritePlatform(onComplete, user) \ + savePSPBeginSave(SAVE_ACTIVE_SLOT, onComplete, user) +#define saveMetaAsyncLoadPlatform(onComplete, user) \ + savePSPBeginLoad(SAVE_ACTIVE_SLOT, onComplete, user) #define saveIsBusyPlatform() savePSPIsBusy() #define savePlatformUpdate() savePSPUpdate() + +// Meta only reaches memory via the native dialog now (folded into the same +// payload as the save slot) - running that multi-frame dialog on every +// single boot just to eagerly populate SAVE.meta would reintroduce the +// exact UX problem a lightweight settings-only file used to avoid, so +// saveInit() skips eager loading entirely on this platform. +#define saveSkipEagerLoadPlatform diff --git a/src/duskpsp/save/savepsp.c b/src/duskpsp/save/savepsp.c index d1367d85..4fde8bd6 100644 --- a/src/duskpsp/save/savepsp.c +++ b/src/duskpsp/save/savepsp.c @@ -45,7 +45,7 @@ errorret_t saveDisposePSP(void) { errorOk(); } -errorret_t saveDeletePSP(const uint8_t slot) { +errorret_t saveDeleteSlotPSP(const uint8_t slot) { char_t path[SAVE_PSP_PATH_MAX]; stringFormat( path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME, @@ -54,7 +54,7 @@ errorret_t saveDeletePSP(const uint8_t slot) { int32_t result = sceIoRemove(path); if(result < 0 && result != (int32_t)0x80010002) { - errorThrow("Failed to delete save file for slot %u", (uint32_t)slot); + errorThrow("Failed to delete save data for slot %u", (uint32_t)slot); } char_t dir[SAVE_PSP_PATH_MAX]; @@ -79,19 +79,19 @@ void savePSPBeginSave( assertNotNull(onComplete, "onComplete cannot be NULL"); assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress"); - savefile_t *file = &SAVE.files[slot]; + saveslot_t *slotData = &SAVE.slots[slot]; - // Serialize into the buffer synchronously (plain memory writes, same - // header/version/CRC framing as every other platform) before the dialog - // ever starts - only the actual commit-to-storage step needs to wait on - // the dialog. + // Serialize meta then the slot into the buffer synchronously (plain + // memory writes, same header/version/CRC framing as every other + // platform) before the dialog ever starts - only the actual commit-to- + // storage step needs to wait on the dialog. savestream_t stream; memoryZero(&stream, sizeof(savestream_t)); stream.platform.buffer = SAVE.platform.dataBuffer; stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer); - errorret_t ret = saveFileWrite(&stream, file); - if(errorIsOk(ret)) ret = saveStreamFinalizeWriteImpl(&stream); + errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta); + if(errorIsOk(ret)) ret = saveSlotSerializeWrite(&stream, slotData); if(errorIsNotOk(ret)) { onComplete(ret, user); return; @@ -121,7 +121,7 @@ void savePSPBeginSave( // save browser entry. stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title)); stringCopy( - param->sfoParam.savedataTitle, file->playerName, + param->sfoParam.savedataTitle, slotData->playerName, sizeof(param->sfoParam.savedataTitle) ); stringCopy( @@ -157,10 +157,10 @@ void savePSPBeginLoad( SceIoStat stat; if(sceIoGetstat(path, &stat) < 0) { - // No save data for this slot yet - not an error (matches every other - // platform's "nothing to load yet" behavior), and deliberately skips - // showing the dialog at all rather than surfacing an empty "no data" - // native screen for a slot the player has never saved to. + // No save data yet - not an error (matches every other platform's + // "nothing to load yet" behavior), and deliberately skips showing the + // dialog at all rather than surfacing an empty "no data" native + // screen for data the player has never saved. onComplete(errorOkImpl(), user); return; } @@ -264,19 +264,20 @@ errorret_t savePSPUpdate(void) { SAVE.available = true; if(op == SAVE_PSP_OP_LOAD) { - savefile_t *file = &SAVE.files[slot]; savestream_t stream; memoryZero(&stream, sizeof(savestream_t)); stream.platform.buffer = SAVE.platform.dataBuffer; stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer); stream.platform.length = SAVE.platform.param.dataSize; - errorret_t ret = saveFileLoad(&stream, file); - if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot); - file->exists = errorIsOk(ret); + errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta); + if(errorIsOk(ret)) { + ret = saveSlotSerializeRead(&stream, &SAVE.slots[slot]); + } cb(ret, user); } else { - SAVE.files[slot].exists = true; + SAVE.meta.exists = true; + SAVE.slots[slot].exists = true; cb(errorOkImpl(), user); } break; diff --git a/src/duskpsp/save/savepsp.h b/src/duskpsp/save/savepsp.h index 5779ae52..14d55153 100644 --- a/src/duskpsp/save/savepsp.h +++ b/src/duskpsp/save/savepsp.h @@ -7,7 +7,8 @@ #pragma once #include "error/error.h" -#include "save/savefile.h" +#include "save/saveslot.h" +#include "save/savemeta.h" #include #include @@ -30,8 +31,13 @@ typedef enum { typedef struct { SceUtilitySavedataParam param; // Raw buffer sceUtilitySavedata reads/writes the whole save into/from - + // holds BOTH save meta and the (single) save slot back-to-back, // populated by our own savestream_t serialization (see savestreampsp.h) // before a save starts, and deserialized from after a load finishes. + // Meta lives in here rather than its own lightweight file specifically + // because a device-wide preference change is meant to feel like a real + // save on this platform (a brief native icon flash), not need its own + // separate storage mechanism. uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64))); size_t dataLength; @@ -66,24 +72,24 @@ errorret_t saveInitPSP(void); errorret_t saveDisposePSP(void); /** - * Deletes the save data folder for the given slot from the memory stick. + * Deletes the (one) save data folder from the memory stick. * - * @param slot The save slot index. + * @param slot The save slot index (always 0 on PSP - see + * SAVE_SLOT_COUNT_MAX's override in this platform's CMakeLists.txt). * @return An error code if the delete fails. */ -errorret_t saveDeletePSP(const uint8_t slot); +errorret_t saveDeleteSlotPSP(const uint8_t slot); /** * Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE - * writes silently with just a brief icon flash, no confirm screen, since * SAVE mode shows one even for a slot with no existing data - but * PARAM.SFO/title/description are generated identically regardless of - * mode, and the OS handles the save browser entry either way) for the - * given slot. Serializes SAVE.files[slot] into SAVE.platform.dataBuffer - * first, synchronously, then kicks off the dialog and returns - completion - * is reported later via onComplete, driven by savePSPUpdate() each frame. - * If no save data exists yet for this slot, sceUtilitySavedataInitStart() - * creates it. + * mode, and the OS handles the save browser entry either way). Serializes + * SAVE.meta then SAVE.slots[slot] into SAVE.platform.dataBuffer first, + * synchronously, then kicks off the dialog and returns - completion is + * reported later via onComplete, driven by savePSPUpdate() each frame. + * If no save data exists yet, sceUtilitySavedataInitStart() creates it. * * @param slot The save slot index. * @param onComplete Callback invoked once the dialog finishes. @@ -95,11 +101,11 @@ void savePSPBeginSave( /** * Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD - - * see savePSPBeginSave() for why not the plain LOAD mode) for the given - * slot, unless a quick sceIoGetstat check finds no save data for this slot - * yet - in which case onComplete is invoked immediately with - * SAVE.files[slot].exists left false, matching the other platforms' - * "no file yet" semantics, and no dialog is shown at all. + * see savePSPBeginSave() for why not the plain LOAD mode) unless a quick + * sceIoGetstat check finds no save data yet - in which case onComplete is + * invoked immediately with SAVE.meta/SAVE.slots[slot].exists left false, + * matching the other platforms' "no file yet" semantics, and no dialog is + * shown at all. * * @param slot The save slot index. * @param onComplete Callback invoked once the dialog (or immediate diff --git a/src/duskpsp/save/savestreampsp.c b/src/duskpsp/save/savestreampsp.c index 623683fd..74da9162 100644 --- a/src/duskpsp/save/savestreampsp.c +++ b/src/duskpsp/save/savestreampsp.c @@ -39,3 +39,8 @@ errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) { p->position = pos; errorOk(); } + +errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out) { + *out = p->position; + errorOk(); +} diff --git a/src/duskpsp/save/savestreampsp.h b/src/duskpsp/save/savestreampsp.h index b14fc266..4fb5978d 100644 --- a/src/duskpsp/save/savestreampsp.h +++ b/src/duskpsp/save/savestreampsp.h @@ -53,3 +53,12 @@ errorret_t saveStreamWriteBytesPSP( * @return An error if pos is out of range. */ errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos); + +/** + * Gets the current read/write position within the buffer. + * + * @param p Active stream. + * @param out Receives the current position. + * @return An error - always succeeds, matches saveStreamTellImpl's shape. + */ +errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out); diff --git a/src/duskpsp/save/settingsplatform.h b/src/duskpsp/save/settingsplatform.h deleted file mode 100644 index 3bf3999d..00000000 --- a/src/duskpsp/save/settingsplatform.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/settingspsp.h" -#include "save/settingsstreampsp.h" - -typedef settingspsp_t settingsplatform_t; -typedef settingsstreampsp_t settingsplatformstream_t; - -#define settingsInitPlatform settingsInitPSP -#define settingsDisposePlatform settingsDisposePSP - -#define settingsStreamOpenReadPlatform(stream) \ - settingsStreamOpenReadPSP(&(stream)->platform, &(stream)->found) -#define settingsStreamOpenWritePlatform(stream) \ - settingsStreamOpenWritePSP(&(stream)->platform) -#define settingsStreamClosePlatform(stream) \ - settingsStreamClosePSP(&(stream)->platform) -#define settingsStreamReadBytesPlatform(stream, buf, len) \ - settingsStreamReadBytesPSP(&(stream)->platform, buf, len) -#define settingsStreamWriteBytesPlatform(stream, buf, len) \ - settingsStreamWriteBytesPSP(&(stream)->platform, buf, len) -#define settingsStreamSeekPlatform(stream, pos) \ - settingsStreamSeekPSP(&(stream)->platform, pos) diff --git a/src/duskpsp/save/settingspsp.c b/src/duskpsp/save/settingspsp.c deleted file mode 100644 index 501fc4c7..00000000 --- a/src/duskpsp/save/settingspsp.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settingspsp.h" - -errorret_t settingsInitPSP(void) { - SceIoStat stat; - if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) { - errorThrow("No memory stick detected"); - } - errorOk(); -} - -errorret_t settingsDisposePSP(void) { - errorOk(); -} diff --git a/src/duskpsp/save/settingspsp.h b/src/duskpsp/save/settingspsp.h deleted file mode 100644 index 7ec8af97..00000000 --- a/src/duskpsp/save/settingspsp.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/settingsfile.h" -#include "save/savepsp.h" -#include - -#define SETTINGS_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%sCFG" -#define SETTINGS_PSP_FILE_FORMAT SETTINGS_PSP_DIR_FORMAT "/settings.bin" -#define SETTINGS_PSP_PATH_MAX 256 - -typedef struct { - uint8_t reserved; -} settingspsp_t; - -/** - * Initializes the settings system on PSP. Confirms the memory stick is - * reachable (sceIoGetstat on SAVE_PSP_ROOT), same check as saveInitPSP(). - * Deliberately does not go through sceUtilitySavedata (see savepsp.h) - - * that dialog is built for the "game save" browser/PARAM.SFO experience - * and shows a visible native icon flash on every write, which is fine for - * an explicit menu Save but not for a settings file that can be written - * every time the player nudges a slider. Plain sceIo file I/O, in its own - * directory outside the per-slot savedata folders, avoids that entirely. - * - * @return An error code if no memory stick is reachable. - */ -errorret_t settingsInitPSP(void); - -/** - * Disposes of the settings system on PSP. - * - * @return An error code if disposal fails. - */ -errorret_t settingsDisposePSP(void); diff --git a/src/duskpsp/save/settingsstreampsp.c b/src/duskpsp/save/settingsstreampsp.c deleted file mode 100644 index 809d4ffa..00000000 --- a/src/duskpsp/save/settingsstreampsp.c +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/settingspsp.h" -#include "save/settingsstreampsp.h" -#include "util/string.h" - -errorret_t settingsStreamOpenReadPSP(settingsstreampsp_t *p, bool_t *found) { - char_t path[SETTINGS_PSP_PATH_MAX]; - stringFormat( - path, sizeof(path), SETTINGS_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME - ); - - p->fd = sceIoOpen(path, PSP_O_RDONLY, 0); - *found = (p->fd >= 0); - errorOk(); -} - -errorret_t settingsStreamOpenWritePSP(settingsstreampsp_t *p) { - char_t dir[SETTINGS_PSP_PATH_MAX]; - stringFormat(dir, sizeof(dir), SETTINGS_PSP_DIR_FORMAT, SAVE_PSP_GAME_NAME); - sceIoMkdir(dir, 0777); - - char_t path[SETTINGS_PSP_PATH_MAX]; - stringFormat( - path, sizeof(path), SETTINGS_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME - ); - - p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777); - if(p->fd < 0) { - errorThrow("Failed to open settings file for writing"); - } - errorOk(); -} - -void settingsStreamClosePSP(settingsstreampsp_t *p) { - if(p->fd >= 0) { - sceIoClose(p->fd); - p->fd = -1; - } -} - -errorret_t settingsStreamReadBytesPSP( - settingsstreampsp_t *p, void *buf, const size_t len -) { - int32_t read = sceIoRead(p->fd, buf, len); - if(read != (int32_t)len) { - errorThrow("Unexpected end of settings file"); - } - errorOk(); -} - -errorret_t settingsStreamWriteBytesPSP( - settingsstreampsp_t *p, const void *buf, const size_t len -) { - int32_t written = sceIoWrite(p->fd, buf, len); - if(written != (int32_t)len) { - errorThrow("Failed to write settings data"); - } - errorOk(); -} - -errorret_t settingsStreamSeekPSP(settingsstreampsp_t *p, const size_t pos) { - if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) { - errorThrow("Failed to seek in settings file"); - } - errorOk(); -} diff --git a/src/duskpsp/save/settingsstreampsp.h b/src/duskpsp/save/settingsstreampsp.h deleted file mode 100644 index 2922349e..00000000 --- a/src/duskpsp/save/settingsstreampsp.h +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include -#include - -typedef struct { - SceUID fd; -} settingsstreampsp_t; - -/** - * Opens the settings file for reading via plain sceIo (not the - * sceUtilitySavedata dialog - see settingspsp.h). - * - * @param p Stream to initialize. - * @param found Set to true if the file exists, false if it does not. - * @return An error if the open fails for a reason other than missing file. - */ -errorret_t settingsStreamOpenReadPSP(settingsstreampsp_t *p, bool_t *found); - -/** - * Opens the settings file for writing, creating its directory and - * truncating the file if needed. - * - * @param p Stream to initialize. - * @return An error if the file cannot be opened for writing. - */ -errorret_t settingsStreamOpenWritePSP(settingsstreampsp_t *p); - -/** - * Closes the file descriptor held by the stream. - * - * @param p Stream to close. - */ -void settingsStreamClosePSP(settingsstreampsp_t *p); - -/** - * Reads len bytes from the stream into buf. - * - * @param p Active stream. - * @param buf Destination buffer. - * @param len Number of bytes to read. - * @return An error if fewer than len bytes are available. - */ -errorret_t settingsStreamReadBytesPSP( - settingsstreampsp_t *p, void *buf, const size_t len -); - -/** - * Writes len bytes from buf into the stream. - * - * @param p Active stream. - * @param buf Source buffer. - * @param len Number of bytes to write. - * @return An error if the write fails. - */ -errorret_t settingsStreamWriteBytesPSP( - settingsstreampsp_t *p, const void *buf, const size_t len -); - -/** - * Seeks to an absolute byte position within the stream. - * - * @param p Active stream. - * @param pos Target byte offset from the start of the file. - * @return An error if the seek fails. - */ -errorret_t settingsStreamSeekPSP(settingsstreampsp_t *p, const size_t pos); diff --git a/src/duskvita/input/inputvita.c b/src/duskvita/input/inputvita.c index 15a6461e..b3b8f427 100644 --- a/src/duskvita/input/inputvita.c +++ b/src/duskvita/input/inputvita.c @@ -6,7 +6,7 @@ */ #include "input/input.h" -#include "save/settings.h" +#include "save/save.h" inputbuttondata_t INPUT_BUTTON_DATA[] = { { .name = "triangle", { @@ -84,5 +84,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = { }; float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return settingsGet()->deadzone; + return saveGetMeta()->deadzone; }