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:
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
save.c
|
||||
savestream.c
|
||||
settings.c
|
||||
settingsstream.c
|
||||
)
|
||||
|
||||
+82
-77
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -8,6 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
savedolphin.c
|
||||
savestreamdolphin.c
|
||||
settingsdolphin.c
|
||||
settingsstreamdolphin.c
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savestream.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
@@ -65,129 +66,76 @@ errorret_t saveDisposeDolphin(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
errorret_t saveCombinedLoadDolphin(void) {
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
file->exists = false;
|
||||
errorOk();
|
||||
}
|
||||
if(result < 0) {
|
||||
file->exists = false;
|
||||
errorThrow("Failed to open memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
errorret_t openRet = saveStreamOpenReadPlatform(&stream);
|
||||
SAVE.available = errorIsOk(openRet);
|
||||
errorChain(openRet);
|
||||
|
||||
if(!stream.found) errorOk();
|
||||
|
||||
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
|
||||
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||
ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]);
|
||||
}
|
||||
|
||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||
if(!buffer) {
|
||||
CARD_Close(&SAVE.platform.cardFile);
|
||||
errorThrow("Failed to allocate memory card read buffer");
|
||||
}
|
||||
#ifdef saveStreamClosePlatform
|
||||
saveStreamClosePlatform(&stream);
|
||||
#endif
|
||||
|
||||
do {
|
||||
result = CARD_Read(
|
||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&SAVE.platform.cardFile);
|
||||
|
||||
if(result < 0) {
|
||||
memoryFree(buffer);
|
||||
file->exists = false;
|
||||
errorThrow("Failed to read memory card data for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
memoryCopy(file, buffer, sizeof(savefile_t));
|
||||
memoryFree(buffer);
|
||||
|
||||
file->exists = true;
|
||||
errorChain(ret);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
errorret_t saveCombinedWriteDolphin(void) {
|
||||
savestream_t stream;
|
||||
memoryZero(&stream, sizeof(savestream_t));
|
||||
|
||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||
if(!buffer) {
|
||||
errorThrow("Failed to allocate memory card write buffer");
|
||||
}
|
||||
memoryZero(buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||
memoryCopy(buffer, file, sizeof(savefile_t));
|
||||
errorret_t openRet = saveStreamOpenWritePlatform(&stream);
|
||||
SAVE.available = errorIsOk(openRet);
|
||||
errorChain(openRet);
|
||||
|
||||
// Try open existing file first; create if absent.
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Open(
|
||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result == CARD_ERROR_NOFILE) {
|
||||
do {
|
||||
result = CARD_Create(
|
||||
SAVE_DOLPHIN_CHANNEL,
|
||||
fileName,
|
||||
SAVE_DOLPHIN_SECTOR_SIZE,
|
||||
&SAVE.platform.cardFile
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
|
||||
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||
ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]);
|
||||
}
|
||||
|
||||
if(result < 0) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to open/create memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
do {
|
||||
result = CARD_Write(
|
||||
&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0
|
||||
);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
CARD_Close(&SAVE.platform.cardFile);
|
||||
memoryFree(buffer);
|
||||
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to write memory card data for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
#ifdef saveStreamClosePlatform
|
||||
saveStreamClosePlatform(&stream);
|
||||
#endif
|
||||
|
||||
errorChain(ret);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeleteDolphin(const uint8_t slot) {
|
||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
||||
|
||||
int32_t result;
|
||||
do {
|
||||
result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName);
|
||||
} while(result == CARD_ERROR_BUSY);
|
||||
if(result < 0 && result != CARD_ERROR_NOFILE) {
|
||||
errorThrow("Failed to delete memory card file for slot %u: %s (%d)",
|
||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
||||
);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) {
|
||||
(void)slot;
|
||||
(void)out;
|
||||
return saveCombinedLoadDolphin();
|
||||
}
|
||||
|
||||
void saveGetFileNameDolphin(
|
||||
const uint8_t slot, char_t *out, const size_t max
|
||||
) {
|
||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
||||
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) {
|
||||
(void)slot;
|
||||
(void)slotData;
|
||||
return saveCombinedWriteDolphin();
|
||||
}
|
||||
|
||||
errorret_t saveSlotDeleteDolphin(const uint8_t slot) {
|
||||
memoryZero(&SAVE.slots[slot], sizeof(saveslot_t));
|
||||
SAVE.slots[slot].exists = false;
|
||||
return saveCombinedWriteDolphin();
|
||||
}
|
||||
|
||||
errorret_t saveMetaLoadDolphin(savemeta_t *out) {
|
||||
(void)out;
|
||||
return saveCombinedLoadDolphin();
|
||||
}
|
||||
|
||||
errorret_t saveMetaWriteDolphin(savemeta_t *meta) {
|
||||
(void)meta;
|
||||
return saveCombinedWriteDolphin();
|
||||
}
|
||||
|
||||
const char_t *saveCardErrorStringDolphin(const int32_t result) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,7 +7,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
savelinux.c
|
||||
savestreamlinux.c
|
||||
settingslinux.c
|
||||
settingsstreamlinux.c
|
||||
savejsonlinux.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savejsonlinux.h"
|
||||
#include "util/string.h"
|
||||
#include <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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user