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 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 18:35:06 -05:00
parent 7f7be39230
commit aa0180571e
59 changed files with 1142 additions and 2071 deletions
-6
View File
@@ -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());
+4 -4
View File
@@ -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;
}
+8 -8
View File
@@ -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
);
+13 -6
View File
@@ -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();
+3 -3
View File
@@ -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,
+7 -7
View File
@@ -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);
-2
View File
@@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
save.c
savestream.c
settings.c
settingsstream.c
)
+82 -77
View File
@@ -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;
}
+82 -43
View File
@@ -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);
+47
View File
@@ -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;
@@ -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.
+84 -37
View File
@@ -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();
}
+74 -31
View File
@@ -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))
-105
View File
@@ -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;
}
-76
View File
@@ -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);
-47
View File
@@ -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;
-164
View File
@@ -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();
}
-208
View File
@@ -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))
+5 -5
View File
@@ -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;
+10 -6
View File
@@ -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;
+2 -2
View File
@@ -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;
}
-2
View File
@@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savedolphin.c
savestreamdolphin.c
settingsdolphin.c
settingsstreamdolphin.c
)
+54 -106
View File
@@ -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) {
+51 -22
View File
@@ -7,10 +7,10 @@
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#include "save/saveslot.h"
#include "save/savemeta.h"
#include <gccore.h>
#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
+12 -5
View File
@@ -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)
+17 -28
View File
@@ -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();
}
+17 -14
View File
@@ -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);
-20
View File
@@ -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();
}
-39
View File
@@ -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);
-29
View File
@@ -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)
@@ -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();
}
@@ -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 <gccore.h>
#include <stddef.h>
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
);
+2 -2
View File
@@ -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;
}
+1 -3
View File
@@ -7,7 +7,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savelinux.c
savestreamlinux.c
settingslinux.c
settingsstreamlinux.c
savejsonlinux.c
)
+161
View File
@@ -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 <sys/stat.h>
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);
}
}
+147
View File
@@ -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": <uint> 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
);
+98 -36
View File
@@ -6,8 +6,9 @@
*/
#include "save/save.h"
#include "save/savejsonlinux.h"
#include "util/string.h"
#include <stdio.h>
#include "util/memory.h"
#include <sys/stat.h>
#include <errno.h>
@@ -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();
}
+31 -12
View File
@@ -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);
+12 -15
View File
@@ -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
-75
View File
@@ -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 <sys/stat.h>
#include <errno.h>
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();
}
-78
View File
@@ -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 <stdio.h>
#include <stddef.h>
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);
-29
View File
@@ -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 <sys/stat.h>
#include <errno.h>
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();
}
-37
View File
@@ -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);
-29
View File
@@ -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)
-68
View File
@@ -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();
}
-74
View File
@@ -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 <stdio.h>
#include <stddef.h>
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);
+2 -2
View File
@@ -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;
}
+8 -2
View File
@@ -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
)
+21 -5
View File
@@ -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
+20 -19
View File
@@ -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;
+21 -15
View File
@@ -7,7 +7,8 @@
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#include "save/saveslot.h"
#include "save/savemeta.h"
#include <pspiofilemgr.h>
#include <psputility.h>
@@ -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
+5
View File
@@ -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();
}
+9
View File
@@ -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);
-29
View File
@@ -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)
-20
View File
@@ -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();
}
-41
View File
@@ -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 <pspiofilemgr.h>
#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);
-72
View File
@@ -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();
}
-74
View File
@@ -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 <pspiofilemgr.h>
#include <stddef.h>
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);
+2 -2
View File
@@ -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;
}