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 "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/settings.h"
|
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -39,10 +38,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
errorChain(saveInit());
|
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(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
@@ -94,7 +89,6 @@ errorret_t engineDispose(void) {
|
|||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
errorChain(settingsDispose());
|
|
||||||
errorChain(saveDispose());
|
errorChain(saveDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
bool_t globalItemStoreIsCollected(
|
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");
|
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||||
return file->globalItemCollected[id];
|
return file->globalItemCollected[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
void globalItemStoreSetCollected(
|
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");
|
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||||
file->globalItemCollected[id] = collected;
|
file->globalItemCollected[id] = collected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,34 +7,34 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the global entity with the given ID has already been
|
* 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
|
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
|
||||||
* skip spawning itself if the player already picked it up in a prior
|
* skip spawning itself if the player already picked it up in a prior
|
||||||
* session, without needing to keep the entity itself alive to remember
|
* session, without needing to keep the entity itself alive to remember
|
||||||
* that (which would need render/collision special-casing - this doesn't).
|
* 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.
|
* @param id The global entity ID to check.
|
||||||
* @return True if already marked collected.
|
* @return True if already marked collected.
|
||||||
*/
|
*/
|
||||||
bool_t globalItemStoreIsCollected(
|
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
|
* 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
|
* given save slot's data. Does not itself write the save to disk - call
|
||||||
* saveWrite() separately once ready to persist it.
|
* 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 id The global entity ID to mark.
|
||||||
* @param collected The new collected state.
|
* @param collected The new collected state.
|
||||||
*/
|
*/
|
||||||
void globalItemStoreSetCollected(
|
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(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
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
|
// Must run before any code reads a story flag - stamps CSV-defined
|
||||||
// defaults onto the active save slot if it's never actually been
|
// defaults onto the active save slot since it's now forced to look
|
||||||
// loaded from disk yet.
|
// unloaded.
|
||||||
storyFlagInitDefaults(saveGet(SAVE_ACTIVE_SLOT));
|
storyFlagInitDefaults(saveSlot);
|
||||||
|
|
||||||
backpackInit();
|
backpackInit();
|
||||||
partyInit();
|
partyInit();
|
||||||
@@ -67,9 +75,8 @@ errorret_t rpgInit(void) {
|
|||||||
// header/version. Remove once there's an actual name-entry flow. On PSP
|
// header/version. Remove once there's an actual name-entry flow. On PSP
|
||||||
// this shows the real native save dialog every boot - expected while
|
// this shows the real native save dialog every boot - expected while
|
||||||
// testing that path, not something to ship as-is.
|
// testing that path, not something to ship as-is.
|
||||||
savefile_t *saveFile = saveGet(SAVE_ACTIVE_SLOT);
|
stringCopy(saveSlot->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
|
||||||
stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
|
saveWriteSlot(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL);
|
||||||
saveWrite(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL);
|
|
||||||
|
|
||||||
// All Good!
|
// All Good!
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
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) {
|
void storyFlagInitDefaults(saveslot_t *file) {
|
||||||
assertNotNull(file, "Save file cannot be NULL");
|
assertNotNull(file, "Save slot cannot be NULL");
|
||||||
if(file->exists) return;
|
if(file->exists) return;
|
||||||
assertTrue(
|
assertTrue(
|
||||||
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
|
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
|
* 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.
|
* @param flag The story flag to get.
|
||||||
* @return The value of the story flag.
|
* @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
|
* 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 flag The story flag to set.
|
||||||
* @param value The value to set the story flag to.
|
* @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
|
* 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
|
* disk yet (file->exists is false) - otherwise leaves already-played
|
||||||
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
|
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
|
||||||
* code reads a story flag.
|
* 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
|
PUBLIC
|
||||||
save.c
|
save.c
|
||||||
savestream.c
|
savestream.c
|
||||||
settings.c
|
|
||||||
settingsstream.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
+82
-77
@@ -13,8 +13,13 @@
|
|||||||
|
|
||||||
save_t SAVE;
|
save_t SAVE;
|
||||||
|
|
||||||
|
static void _saveEagerLoadComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t saveInit(void) {
|
errorret_t saveInit(void) {
|
||||||
memoryZero(&SAVE, sizeof(save_t));
|
memoryZero(&SAVE, sizeof(save_t));
|
||||||
|
SAVE.meta.deadzone = SAVE_META_DEADZONE_DEFAULT;
|
||||||
|
|
||||||
#ifdef saveInitPlatform
|
#ifdef saveInitPlatform
|
||||||
// A missing/unreachable save medium is expected, recoverable state,
|
// A missing/unreachable save medium is expected, recoverable state,
|
||||||
@@ -27,6 +32,21 @@ errorret_t saveInit(void) {
|
|||||||
SAVE.available = false;
|
SAVE.available = false;
|
||||||
#endif
|
#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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,104 +76,89 @@ bool_t saveIsBusy(void) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) {
|
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
savefile_t *file = &SAVE.files[slot];
|
SAVE.slots[slot].exists = false;
|
||||||
file->exists = false;
|
|
||||||
|
|
||||||
// Some platforms (PSP's native save dialog) can't complete within this
|
// Some platforms (PSP's native save dialog) can't complete within this
|
||||||
// call - they take over entirely and invoke onComplete later, from
|
// call - they take over entirely and invoke onComplete later, from
|
||||||
// saveUpdate(), once their own multi-frame flow finishes.
|
// saveUpdate(), once their own multi-frame flow finishes. Those
|
||||||
#ifdef saveAsyncLoadPlatform
|
// platforms never define the sync saveSlotLoadPlatform() at all, so the
|
||||||
saveAsyncLoadPlatform(slot, onComplete, user);
|
// fallback below must live in the #else, not just after an early return.
|
||||||
return;
|
#ifdef saveSlotAsyncLoadPlatform
|
||||||
#endif
|
saveSlotAsyncLoadPlatform(slot, onComplete, user);
|
||||||
|
#else
|
||||||
savestream_t stream;
|
errorret_t ret = saveSlotLoadPlatform(slot, &SAVE.slots[slot]);
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
SAVE.available = errorIsOk(ret);
|
||||||
|
|
||||||
#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);
|
onComplete(ret, user);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) {
|
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
savefile_t *file = &SAVE.files[slot];
|
// See saveLoadSlot() - some platforms take over and complete later.
|
||||||
// These are metadata about the file itself, not game data - always stamp
|
#ifdef saveSlotAsyncWritePlatform
|
||||||
// the current magic/version on every write rather than relying on
|
saveSlotAsyncWritePlatform(slot, onComplete, user);
|
||||||
// whatever happened to already be in memory (zeroed at saveInit, or
|
#else
|
||||||
// whatever version an old loaded file had), otherwise the written file
|
errorret_t ret = saveSlotWritePlatform(slot, &SAVE.slots[slot]);
|
||||||
// fails its own header check the next time it's loaded.
|
SAVE.available = errorIsOk(ret);
|
||||||
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;
|
|
||||||
#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);
|
onComplete(ret, user);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDelete(const uint8_t slot) {
|
errorret_t saveDeleteSlot(const uint8_t slot) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
|
|
||||||
#ifdef saveDeletePlatform
|
#ifdef saveSlotDeletePlatform
|
||||||
errorret_t deleteRet = saveDeletePlatform(slot);
|
errorret_t deleteRet = saveSlotDeletePlatform(slot);
|
||||||
SAVE.available = errorIsOk(deleteRet);
|
SAVE.available = errorIsOk(deleteRet);
|
||||||
errorChain(deleteRet);
|
errorChain(deleteRet);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SAVE.files[slot].exists = false;
|
SAVE.slots[slot].exists = false;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t saveExists(const uint8_t slot) {
|
bool_t saveSlotExists(const uint8_t slot) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
return SAVE.files[slot].exists;
|
return SAVE.slots[slot].exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
savefile_t * saveGet(const uint8_t slot) {
|
saveslot_t * saveGetSlot(const uint8_t slot) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||||
return &SAVE.files[slot];
|
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
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "savefile.h"
|
#include "saveslot.h"
|
||||||
|
#include "savemeta.h"
|
||||||
#include "save/saveplatform.h"
|
#include "save/saveplatform.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
|
/** Per-slot save data; indexed 0 to SAVE_SLOT_COUNT_MAX - 1. */
|
||||||
savefile_t files[SAVE_FILE_COUNT_MAX];
|
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.). */
|
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||||
saveplatform_t platform;
|
saveplatform_t platform;
|
||||||
/**
|
/**
|
||||||
* True if the save medium (memory card/stick/disk) was reachable the
|
* True if the save medium (memory card/stick/disk) was reachable the
|
||||||
* last time it was checked - at saveInit(), and refreshed by every
|
* last time it was checked - at saveInit(), and refreshed by every
|
||||||
* subsequent saveLoad()/saveWrite() attempt. Starting the game with no
|
* subsequent load/write attempt. Starting the game with no card/stick
|
||||||
* card/stick inserted, or one being removed mid-session, are both
|
* inserted, or one being removed mid-session, are both expected
|
||||||
* expected conditions here, not fatal errors - see saveIsAvailable().
|
* conditions here, not fatal errors - see saveIsAvailable().
|
||||||
*/
|
*/
|
||||||
bool_t available;
|
bool_t available;
|
||||||
/**
|
/**
|
||||||
@@ -35,18 +38,28 @@ typedef struct {
|
|||||||
extern save_t SAVE;
|
extern save_t SAVE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the save system. Never fails the way saveWrite/saveLoad can -
|
* Initializes the save system. Never fails the way saveWriteSlot()/
|
||||||
* if the platform's save medium isn't reachable (e.g. no memory card/stick
|
* saveWriteMeta() can - if the platform's save medium isn't reachable
|
||||||
* inserted), that's logged and reflected in saveIsAvailable() rather than
|
* (e.g. no memory card/stick inserted), that's logged and reflected in
|
||||||
* treated as fatal, since the game should still be playable without save
|
* saveIsAvailable() rather than treated as fatal, since the game should
|
||||||
* support.
|
* 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.
|
* @return An error code only for unexpected platform failures.
|
||||||
*/
|
*/
|
||||||
errorret_t saveInit(void);
|
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
|
* 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
|
* 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".
|
* 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
|
* Updates the save manager, pumping any in-progress async save/load and
|
||||||
* dispatching its callback once complete. No-op on platforms where
|
* dispatching its callback once complete. No-op on platforms where every
|
||||||
* saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()).
|
* operation always completes synchronously (see saveIsBusy()). Must be
|
||||||
* Must be called every engine frame for platforms that need it (PSP's
|
* called every engine frame for platforms that need it (PSP's native save
|
||||||
* native save dialog spans multiple frames).
|
* dialog spans multiple frames).
|
||||||
*
|
*
|
||||||
* @return An error code indicating success or failure.
|
* @return An error code indicating success or failure.
|
||||||
*/
|
*/
|
||||||
errorret_t saveUpdate(void);
|
errorret_t saveUpdate(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's
|
* True while an async save/load is in progress (e.g. PSP's native save
|
||||||
* native save dialog is open). Calling saveWrite()/saveLoad() again while
|
* dialog is open). Calling any save/load function again while this is true
|
||||||
* this is true is undefined behavior - wait for the previous call's
|
* is undefined behavior - wait for the previous call's callback first.
|
||||||
* callback first.
|
|
||||||
*
|
*
|
||||||
* @return True if a save/load request is currently in progress.
|
* @return True if a save/load request is currently in progress.
|
||||||
*/
|
*/
|
||||||
bool_t saveIsBusy(void);
|
bool_t saveIsBusy(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the save file for a given slot from persistent storage. Slow/async
|
* Loads the save slot for a given index from persistent storage. Slow/
|
||||||
* on some platforms (PSP's native save dialog spans multiple frames) - on
|
* async on some platforms (PSP's native save dialog spans multiple
|
||||||
* others (Linux, Dolphin) onComplete is invoked before this call returns.
|
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||||
* See saveIsBusy().
|
* 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 onComplete Callback invoked with the result once loading finishes.
|
||||||
* @param user User data passed through to onComplete.
|
* @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
|
* Writes the save slot for a given index to persistent storage. Slow/
|
||||||
* on some platforms (PSP's native save dialog spans multiple frames) - on
|
* async on some platforms (PSP's native save dialog spans multiple
|
||||||
* others (Linux, Dolphin) onComplete is invoked before this call returns.
|
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||||
* See saveIsBusy().
|
* 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 onComplete Callback invoked with the result once writing finishes.
|
||||||
* @param user User data passed through to onComplete.
|
* @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.
|
* @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).
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
* @return true if a save file exists for the slot, false otherwise.
|
* @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).
|
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||||
* @return A pointer to the savefile_t for the given slot.
|
* @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
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
|
||||||
/** Save file format version. Increment on breaking change. */
|
/** Save slot format version. Increment on breaking change. */
|
||||||
#define SAVE_FILE_VERSION 1
|
#define SAVE_SLOT_VERSION 1
|
||||||
|
|
||||||
/** Magic bytes that identify a Dusk save file. */
|
/** Magic bytes that identify a Dusk save slot. */
|
||||||
#define SAVE_FILE_HEADER "DSK"
|
#define SAVE_SLOT_HEADER "DSK"
|
||||||
|
|
||||||
/** Byte length of the magic header (excludes the null terminator). */
|
/** 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
|
* 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
|
* select/multi-save UX yet (SAVE_SLOT_COUNT_MAX > 1 exists for later), so
|
||||||
* every part of the game that needs "the" save file (settings, the game
|
* every part of the game that needs "the" save slot (the game menu's Save
|
||||||
* menu's Save button, etc.) reads/writes this one slot.
|
* button, etc.) reads/writes this one slot.
|
||||||
*/
|
*/
|
||||||
#define SAVE_ACTIVE_SLOT 0
|
#define SAVE_ACTIVE_SLOT 0
|
||||||
|
|
||||||
@@ -34,7 +41,7 @@
|
|||||||
/**
|
/**
|
||||||
* Maximum number of global entities whose "collected" state can be
|
* Maximum number of global entities whose "collected" state can be
|
||||||
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
|
* 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
|
* leaf header with no dependency on the entity system (and no reason to
|
||||||
* take one just for a size constant).
|
* take one just for a size constant).
|
||||||
*/
|
*/
|
||||||
@@ -44,15 +51,16 @@
|
|||||||
* Maximum number of story flags the save format can hold - see
|
* Maximum number of story flags the save format can hold - see
|
||||||
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
|
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
|
||||||
* current flag count) rather than tied to STORY_FLAG_COUNT, since
|
* 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.
|
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
|
||||||
*/
|
*/
|
||||||
#define SAVE_STORY_FLAG_COUNT_MAX 128
|
#define SAVE_STORY_FLAG_COUNT_MAX 128
|
||||||
|
|
||||||
|
/** Per-slot game progress - the state a "save file" traditionally means. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
|
/** Magic header bytes read from the slot; must equal SAVE_SLOT_HEADER. */
|
||||||
char_t header[SAVE_FILE_HEADER_SIZE];
|
char_t header[SAVE_SLOT_HEADER_SIZE];
|
||||||
/** Format version read from the file; used to branch on older layouts. */
|
/** Format version read from the slot; used to branch on older layouts. */
|
||||||
uint32_t version;
|
uint32_t version;
|
||||||
/** Runtime flag - true if this slot was successfully loaded or written. */
|
/** Runtime flag - true if this slot was successfully loaded or written. */
|
||||||
bool_t exists;
|
bool_t exists;
|
||||||
@@ -61,18 +69,19 @@ typedef struct {
|
|||||||
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
|
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
|
||||||
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
|
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
|
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
|
||||||
* rpg/story/storyflag.h), not directly.
|
* rpg/story/storyflag.h), not directly.
|
||||||
*/
|
*/
|
||||||
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
|
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
|
||||||
} savefile_t;
|
} saveslot_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback invoked when an async saveWrite()/saveLoad() request completes.
|
* Callback invoked when an async saveWriteSlot()/saveLoadSlot()/
|
||||||
* Declared here (rather than save.h) so platform save headers - which
|
* saveWriteMeta()/saveLoadMeta() request completes. Declared here (rather
|
||||||
* save.h's platform indirection pulls in before save.h finishes defining
|
* than save.h) so platform save headers - which save.h's platform
|
||||||
* anything else - can reference it without a circular include.
|
* indirection pulls in before save.h finishes defining anything else - can
|
||||||
|
* reference it without a circular include.
|
||||||
*
|
*
|
||||||
* @param result Whether the request succeeded.
|
* @param result Whether the request succeeded.
|
||||||
* @param user User data passed through from the original call.
|
* @param user User data passed through from the original call.
|
||||||
+84
-37
@@ -45,12 +45,23 @@ errorret_t saveStreamWriteBytesImpl(
|
|||||||
errorOk();
|
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 finalCRC = cryptCRC32End(stream->checksum);
|
||||||
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
||||||
|
|
||||||
#ifdef saveStreamSeekPlatform
|
#ifdef saveStreamSeekPlatform
|
||||||
errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE));
|
errorChain(saveStreamSeekPlatform(stream, headerPosition + headerSize));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
errorChain(saveStreamWriteBytesRawImpl(
|
errorChain(saveStreamWriteBytesRawImpl(
|
||||||
@@ -60,27 +71,25 @@ errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamVerifyChecksumImpl(
|
errorret_t saveStreamVerifyChecksumImpl(
|
||||||
savestream_t *stream, const uint8_t slot
|
savestream_t *stream, const char_t *sectionLabel
|
||||||
) {
|
) {
|
||||||
uint32_t computed = cryptCRC32End(stream->checksum);
|
uint32_t computed = cryptCRC32End(stream->checksum);
|
||||||
if(computed != stream->expectedChecksum) {
|
if(computed != stream->expectedChecksum) {
|
||||||
errorThrow("Save slot %u has invalid checksum", (uint32_t)slot);
|
errorThrow("%s has invalid checksum", sectionLabel);
|
||||||
}
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
errorret_t saveStreamReadHeaderImpl(
|
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(
|
for(size_t i = 0; i < headerSize; i++) {
|
||||||
header[0] != SAVE_FILE_HEADER[0] ||
|
if(header[i] != expectedHeader[i]) {
|
||||||
header[1] != SAVE_FILE_HEADER[1] ||
|
errorThrow("Save data has invalid header");
|
||||||
header[2] != SAVE_FILE_HEADER[2]
|
}
|
||||||
) {
|
|
||||||
errorThrow("Save file has invalid header");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t leChecksum;
|
uint32_t leChecksum;
|
||||||
@@ -91,11 +100,9 @@ errorret_t saveStreamReadHeaderImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamWriteHeaderImpl(
|
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(
|
errorChain(saveStreamWriteBytesRawImpl(stream, header, headerSize));
|
||||||
stream, header, SAVE_FILE_HEADER_SIZE
|
|
||||||
));
|
|
||||||
|
|
||||||
uint32_t placeholder = 0;
|
uint32_t placeholder = 0;
|
||||||
errorChain(saveStreamWriteBytesRawImpl(
|
errorChain(saveStreamWriteBytesRawImpl(
|
||||||
@@ -327,28 +334,68 @@ errorret_t saveStreamWriteDateImpl(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
|
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta) {
|
||||||
saveFileReadHeader(stream, file->header);
|
saveFileReadHeader(
|
||||||
saveFileReadVersion(stream, &file->version);
|
stream, meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE
|
||||||
saveFileReadString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
|
);
|
||||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
saveFileReadVersion(stream, &meta->version);
|
||||||
saveFileReadBool(stream, &file->globalItemCollected[i]);
|
saveFileReadFloat(stream, &meta->deadzone);
|
||||||
}
|
errorChain(saveStreamVerifyChecksumImpl(stream, "Save meta"));
|
||||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
meta->exists = true;
|
||||||
saveFileReadUInt8(stream, &file->storyFlags[i]);
|
|
||||||
}
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
|
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta) {
|
||||||
saveFileWriteHeader(stream, file->header);
|
memoryCopy(meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE);
|
||||||
saveFileWriteVersion(stream, &file->version);
|
meta->version = SAVE_META_VERSION;
|
||||||
saveFileWriteString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
|
|
||||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
size_t headerPosition;
|
||||||
saveFileWriteBool(stream, &file->globalItemCollected[i]);
|
errorChain(saveStreamTellImpl(stream, &headerPosition));
|
||||||
}
|
saveFileWriteHeader(stream, meta->header, SAVE_META_HEADER_SIZE);
|
||||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
saveFileWriteVersion(stream, &meta->version);
|
||||||
saveFileWriteUInt8(stream, &file->storyFlags[i]);
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-31
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "savefile.h"
|
#include "saveslot.h"
|
||||||
|
#include "savemeta.h"
|
||||||
#include "save/saveplatform.h"
|
#include "save/saveplatform.h"
|
||||||
#include "time/timeepoch.h"
|
#include "time/timeepoch.h"
|
||||||
|
|
||||||
@@ -67,49 +68,72 @@ errorret_t saveStreamWriteBytesImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalizes a write stream: computes the final CRC32, seeks to the
|
* Gets the current read/write position within the stream. Used to capture
|
||||||
* checksum field in the header, and writes it in little-endian order.
|
* 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 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.
|
* @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
|
* 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 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.
|
* @return An error if the checksum does not match.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamVerifyChecksumImpl(
|
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
|
* Reads and validates a section's magic header, then reads its stored
|
||||||
* resets the running accumulator.
|
* CRC32 and resets the running accumulator.
|
||||||
*
|
*
|
||||||
* @param stream Active read stream.
|
* @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.
|
* @return An error if the header is missing or invalid.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamReadHeaderImpl(
|
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
|
* Writes a section's magic header and a zero CRC32 placeholder, then
|
||||||
* running accumulator.
|
* resets the running accumulator.
|
||||||
*
|
*
|
||||||
* @param stream Active write stream.
|
* @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.
|
* @return An error if the write fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamWriteHeaderImpl(
|
errorret_t saveStreamWriteHeaderImpl(
|
||||||
savestream_t *stream,
|
savestream_t *stream, const char_t *header, const size_t headerSize
|
||||||
const char_t header[SAVE_FILE_HEADER_SIZE]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -377,29 +401,49 @@ errorret_t saveStreamWriteDateImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the contents of a save slot from the stream into the save file
|
* Reads a self-contained save meta section (header, version, fields,
|
||||||
* struct. Use saveFileRead* macros to deserialize fields one at a time.
|
* checksum verification) from the stream.
|
||||||
*
|
*
|
||||||
* @param stream Active read stream for this slot.
|
* @param stream Active read stream, positioned at the section's start.
|
||||||
* @param file Save file struct to populate.
|
* @param meta Meta struct to populate.
|
||||||
* @return An error code if loading fails.
|
* @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.
|
* Writes a self-contained save meta section (header, version, fields,
|
||||||
* Use saveFileWrite* macros to serialize fields one at a time.
|
* checksum) to the stream.
|
||||||
*
|
*
|
||||||
* @param stream Active write stream for this slot.
|
* @param stream Active write stream, positioned at the section's start.
|
||||||
* @param file Save file struct to serialize.
|
* @param meta Meta struct to serialize.
|
||||||
* @return An error code if writing fails.
|
* @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))
|
* Reads a self-contained save slot section (header, version, fields,
|
||||||
#define saveFileWriteHeader(stream, header) \
|
* checksum verification) from the stream.
|
||||||
errorChain(saveStreamWriteHeaderImpl(stream, header))
|
*
|
||||||
|
* @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) \
|
#define saveFileReadVersion(stream, out) \
|
||||||
errorChain(saveStreamReadVersionImpl(stream, out))
|
errorChain(saveStreamReadVersionImpl(stream, out))
|
||||||
@@ -465,4 +509,3 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
|||||||
errorChain(saveStreamReadDateImpl(stream, out))
|
errorChain(saveStreamReadDateImpl(stream, out))
|
||||||
#define saveFileWriteDate(stream, input) \
|
#define saveFileWriteDate(stream, input) \
|
||||||
errorChain(saveStreamWriteDateImpl(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines whether there's actually save data to overwrite (not just
|
// 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
|
// 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
|
// 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
|
// (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."
|
// platform's own notion of "found something."
|
||||||
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
|
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
|
||||||
if(errorIsNotOk(result)) {
|
if(errorIsNotOk(result)) {
|
||||||
@@ -68,8 +68,8 @@ static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(saveExists(SAVE_ACTIVE_SLOT)) {
|
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
|
||||||
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ static void uiGameMenuSave(void) {
|
|||||||
}
|
}
|
||||||
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
|
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;
|
uigamemenu_t UI_GAME_MENU;
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.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(
|
void uiSettingsInputSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
@@ -39,7 +43,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
|
|||||||
UI_SETTINGS_INPUT_LABEL_MAX
|
UI_SETTINGS_INPUT_LABEL_MAX
|
||||||
));
|
));
|
||||||
MENU_SLIDER_FLOAT(
|
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
|
#else
|
||||||
MENU_LABEL("No input settings yet");
|
MENU_LABEL("No input settings yet");
|
||||||
@@ -55,17 +59,17 @@ void uiSettingsInputLoad(void) {
|
|||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
uiSliderSetFloat(
|
uiSliderSetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
||||||
settingsGet()->deadzone
|
saveGetMeta()->deadzone
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSettingsInputApply(void) {
|
void uiSettingsInputApply(void) {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
settingsGet()->deadzone = uiSliderGetFloat(
|
saveGetMeta()->deadzone = uiSliderGetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||||
);
|
);
|
||||||
errorCatch(errorPrint(settingsSave()));
|
saveWriteMeta(uiSettingsInputSaveComplete, NULL);
|
||||||
#endif
|
#endif
|
||||||
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
||||||
}
|
}
|
||||||
@@ -74,7 +78,7 @@ bool_t uiSettingsInputHasChanges(void) {
|
|||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
if(uiSliderGetFloat(
|
if(uiSliderGetFloat(
|
||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||||
) != settingsGet()->deadzone) return true;
|
) != saveGetMeta()->deadzone) return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "save/settings.h"
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -188,5 +188,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneDolphin(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
|
PUBLIC
|
||||||
savedolphin.c
|
savedolphin.c
|
||||||
savestreamdolphin.c
|
savestreamdolphin.c
|
||||||
settingsdolphin.c
|
|
||||||
settingsstreamdolphin.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savestream.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
@@ -65,129 +66,76 @@ errorret_t saveDisposeDolphin(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
|
errorret_t saveCombinedLoadDolphin(void) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
savestream_t stream;
|
||||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
int32_t result;
|
errorret_t openRet = saveStreamOpenReadPlatform(&stream);
|
||||||
do {
|
SAVE.available = errorIsOk(openRet);
|
||||||
result = CARD_Open(
|
errorChain(openRet);
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile
|
|
||||||
);
|
if(!stream.found) errorOk();
|
||||||
} while(result == CARD_ERROR_BUSY);
|
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
|
||||||
file->exists = false;
|
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||||
errorOk();
|
ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]);
|
||||||
}
|
|
||||||
if(result < 0) {
|
|
||||||
file->exists = false;
|
|
||||||
errorThrow("Failed to open memory card file for slot %u: %s (%d)",
|
|
||||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
#ifdef saveStreamClosePlatform
|
||||||
if(!buffer) {
|
saveStreamClosePlatform(&stream);
|
||||||
CARD_Close(&SAVE.platform.cardFile);
|
#endif
|
||||||
errorThrow("Failed to allocate memory card read buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
errorChain(ret);
|
||||||
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;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
|
errorret_t saveCombinedWriteDolphin(void) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
savestream_t stream;
|
||||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
void *buffer = memoryAlign(32, SAVE_DOLPHIN_SECTOR_SIZE);
|
errorret_t openRet = saveStreamOpenWritePlatform(&stream);
|
||||||
if(!buffer) {
|
SAVE.available = errorIsOk(openRet);
|
||||||
errorThrow("Failed to allocate memory card write buffer");
|
errorChain(openRet);
|
||||||
}
|
|
||||||
memoryZero(buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
|
||||||
memoryCopy(buffer, file, sizeof(savefile_t));
|
|
||||||
|
|
||||||
// Try open existing file first; create if absent.
|
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
|
||||||
int32_t result;
|
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||||
do {
|
ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(result < 0) {
|
#ifdef saveStreamClosePlatform
|
||||||
memoryFree(buffer);
|
saveStreamClosePlatform(&stream);
|
||||||
errorThrow("Failed to open/create memory card file for slot %u: %s (%d)",
|
#endif
|
||||||
(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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
errorChain(ret);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDeleteDolphin(const uint8_t slot) {
|
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) {
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
(void)slot;
|
||||||
saveGetFileNameDolphin(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
|
(void)out;
|
||||||
|
return saveCombinedLoadDolphin();
|
||||||
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 saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) {
|
||||||
|
(void)slot;
|
||||||
|
(void)slotData;
|
||||||
|
return saveCombinedWriteDolphin();
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveGetFileNameDolphin(
|
errorret_t saveSlotDeleteDolphin(const uint8_t slot) {
|
||||||
const uint8_t slot, char_t *out, const size_t max
|
memoryZero(&SAVE.slots[slot], sizeof(saveslot_t));
|
||||||
) {
|
SAVE.slots[slot].exists = false;
|
||||||
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
|
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) {
|
const char_t *saveCardErrorStringDolphin(const int32_t result) {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savemeta.h"
|
||||||
#include <gccore.h>
|
#include <gccore.h>
|
||||||
|
|
||||||
#define SAVE_DOLPHIN_FILE_NAME_MAX 32
|
|
||||||
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
|
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
|
||||||
|
|
||||||
#ifndef SAVE_DOLPHIN_GAME_CODE
|
#ifndef SAVE_DOLPHIN_GAME_CODE
|
||||||
@@ -21,6 +21,17 @@
|
|||||||
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
|
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
|
||||||
#endif
|
#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 {
|
typedef struct {
|
||||||
card_file cardFile;
|
card_file cardFile;
|
||||||
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
|
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
|
||||||
@@ -42,42 +53,60 @@ errorret_t saveInitDolphin(void);
|
|||||||
errorret_t saveDisposeDolphin(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.
|
* @return An error code if the card is mounted but the read/parse fails.
|
||||||
* @param file Output save file data.
|
|
||||||
* @return An error code if the load 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.
|
* @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.
|
* @param slot The save slot index.
|
||||||
* @return An error code if the delete fails.
|
* @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
|
* Meta platform entry point - always (re)reads the whole consolidated
|
||||||
* SAVE_DOLPHIN_GAME_CODE and the slot index.
|
* file (see saveCombinedLoadDolphin()); out is unused since meta is
|
||||||
*
|
* populated in the same pass.
|
||||||
* @param slot The save slot index.
|
|
||||||
* @param out Destination buffer for the file name.
|
|
||||||
* @param max Size of out, in bytes.
|
|
||||||
*/
|
*/
|
||||||
void saveGetFileNameDolphin(
|
errorret_t saveMetaLoadDolphin(savemeta_t *out);
|
||||||
const uint8_t slot, char_t *out, const size_t max
|
|
||||||
);
|
/**
|
||||||
|
* 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
|
* Describes a libogc CARD_ERROR_* result code (see
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ typedef savestreamdolphin_t saveplatformstream_t;
|
|||||||
|
|
||||||
#define saveInitPlatform saveInitDolphin
|
#define saveInitPlatform saveInitDolphin
|
||||||
#define saveDisposePlatform saveDisposeDolphin
|
#define saveDisposePlatform saveDisposeDolphin
|
||||||
#define saveDeletePlatform saveDeleteDolphin
|
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
#define saveSlotDeletePlatform saveSlotDeleteDolphin
|
||||||
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found, slot)
|
#define saveSlotLoadPlatform saveSlotLoadDolphin
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
#define saveSlotWritePlatform saveSlotWriteDolphin
|
||||||
saveStreamOpenWriteDolphin(&(stream)->platform, slot)
|
#define saveMetaLoadPlatform saveMetaLoadDolphin
|
||||||
|
#define saveMetaWritePlatform saveMetaWriteDolphin
|
||||||
|
|
||||||
|
#define saveStreamOpenReadPlatform(stream) \
|
||||||
|
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found)
|
||||||
|
#define saveStreamOpenWritePlatform(stream) \
|
||||||
|
saveStreamOpenWriteDolphin(&(stream)->platform)
|
||||||
#define saveStreamClosePlatform(stream) \
|
#define saveStreamClosePlatform(stream) \
|
||||||
saveStreamCloseDolphin(&(stream)->platform)
|
saveStreamCloseDolphin(&(stream)->platform)
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||||
@@ -28,3 +33,5 @@ typedef savestreamdolphin_t saveplatformstream_t;
|
|||||||
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
|
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
#define saveStreamSeekPlatform(stream, pos) \
|
||||||
saveStreamSeekDolphin(&(stream)->platform, pos)
|
saveStreamSeekDolphin(&(stream)->platform, pos)
|
||||||
|
#define saveStreamTellPlatform(stream, out) \
|
||||||
|
saveStreamTellDolphin(&(stream)->platform, out)
|
||||||
|
|||||||
@@ -8,29 +8,17 @@
|
|||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/savestreamdolphin.h"
|
#include "save/savestreamdolphin.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
|
|
||||||
static void _saveStreamGetFileName(
|
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found) {
|
||||||
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
|
|
||||||
) {
|
|
||||||
if(!SAVE.platform.mounted) {
|
if(!SAVE.platform.mounted) {
|
||||||
*found = false;
|
*found = false;
|
||||||
errorThrow("No memory card mounted");
|
errorThrow("No memory card mounted");
|
||||||
}
|
}
|
||||||
|
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
|
||||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot);
|
|
||||||
|
|
||||||
int32_t result;
|
int32_t result;
|
||||||
do {
|
do {
|
||||||
result = CARD_Open(
|
result = CARD_Open(
|
||||||
SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile
|
SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile
|
||||||
);
|
);
|
||||||
} while(result == CARD_ERROR_BUSY);
|
} while(result == CARD_ERROR_BUSY);
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
if(result == CARD_ERROR_NOFILE) {
|
||||||
@@ -41,8 +29,8 @@ errorret_t saveStreamOpenReadDolphin(
|
|||||||
}
|
}
|
||||||
if(result < 0) {
|
if(result < 0) {
|
||||||
*found = false;
|
*found = false;
|
||||||
errorThrow("Failed to open memory card file for slot %u: %s (%d)",
|
errorThrow("Failed to open memory card file: %s (%d)",
|
||||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
saveCardErrorStringDolphin(result), result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,44 +40,40 @@ errorret_t saveStreamOpenReadDolphin(
|
|||||||
CARD_Close(&p->cardFile);
|
CARD_Close(&p->cardFile);
|
||||||
if(result < 0) {
|
if(result < 0) {
|
||||||
*found = false;
|
*found = false;
|
||||||
errorThrow("Failed to read memory card data for slot %u: %s (%d)",
|
errorThrow("Failed to read memory card data: %s (%d)",
|
||||||
(uint32_t)slot, saveCardErrorStringDolphin(result), result
|
saveCardErrorStringDolphin(result), result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
*found = true;
|
*found = true;
|
||||||
p->position = 0;
|
p->position = 0;
|
||||||
p->writing = false;
|
p->writing = false;
|
||||||
p->slot = slot;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamOpenWriteDolphin(
|
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p) {
|
||||||
savestreamdolphin_t *p, const uint8_t slot
|
|
||||||
) {
|
|
||||||
if(!SAVE.platform.mounted) errorThrow("No memory card mounted");
|
if(!SAVE.platform.mounted) errorThrow("No memory card mounted");
|
||||||
|
|
||||||
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
|
||||||
p->position = 0;
|
p->position = 0;
|
||||||
p->writing = true;
|
p->writing = true;
|
||||||
p->slot = slot;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
|
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
|
||||||
if(!p->writing) return;
|
if(!p->writing) return;
|
||||||
|
|
||||||
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
|
|
||||||
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot);
|
|
||||||
|
|
||||||
int32_t result;
|
int32_t result;
|
||||||
do {
|
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);
|
} while(result == CARD_ERROR_BUSY);
|
||||||
if(result == CARD_ERROR_NOFILE) {
|
if(result == CARD_ERROR_NOFILE) {
|
||||||
do {
|
do {
|
||||||
result = CARD_Create(
|
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);
|
} while(result == CARD_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
@@ -129,3 +113,8 @@ errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos) {
|
|||||||
p->position = pos;
|
p->position = pos;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out) {
|
||||||
|
*out = p->position;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,34 +20,28 @@ typedef struct {
|
|||||||
size_t position;
|
size_t position;
|
||||||
/** True when opened for writing; flushes buffer to card on close. */
|
/** True when opened for writing; flushes buffer to card on close. */
|
||||||
bool_t writing;
|
bool_t writing;
|
||||||
/** Slot index stored at open time so Close can derive the filename. */
|
|
||||||
uint8_t slot;
|
|
||||||
} savestreamdolphin_t;
|
} 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 p Stream to initialize.
|
||||||
* @param found Set to true if the file exists, false if it does not.
|
* @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
|
* @return An error if reading the card fails for a reason other than
|
||||||
* missing file.
|
* missing file.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamOpenReadDolphin(
|
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found);
|
||||||
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a memory card slot for writing by zeroing the sector buffer.
|
* Opens the consolidated memory card file for writing by zeroing the
|
||||||
* The buffer is flushed to the card when savestreamCloseDolphin is called.
|
* sector buffer. The buffer is flushed to the card when
|
||||||
|
* saveStreamCloseDolphin is called.
|
||||||
*
|
*
|
||||||
* @param p Stream to initialize.
|
* @param p Stream to initialize.
|
||||||
* @param slot Save slot index.
|
|
||||||
* @return An error if initialization fails.
|
* @return An error if initialization fails.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamOpenWriteDolphin(
|
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p);
|
||||||
savestreamdolphin_t *p, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flushes the sector buffer to the memory card (write mode only) and
|
* 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.
|
* @return An error if pos is out of range.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos);
|
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 "input/input.h"
|
||||||
#include "save/settings.h"
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -548,5 +548,5 @@ errorret_t inputInitLinux(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return settingsGet()->deadzone;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
savelinux.c
|
savelinux.c
|
||||||
savestreamlinux.c
|
savejsonlinux.c
|
||||||
settingslinux.c
|
|
||||||
settingsstreamlinux.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
|
||||||
|
);
|
||||||
+103
-41
@@ -6,8 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savejsonlinux.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <stdio.h>
|
#include "util/memory.h"
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
@@ -25,60 +26,121 @@ errorret_t saveDisposeLinux(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file) {
|
static void _saveSlotPathLinux(
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
char_t *out, const size_t max, const uint8_t slot
|
||||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
) {
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
snprintf(
|
||||||
|
out, max, SAVE_LINUX_SLOT_FILE_FORMAT, SAVE.platform.savePath,
|
||||||
|
(uint32_t)slot
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
FILE *f = fopen(path, "rb");
|
static void _saveMetaPathLinux(char_t *out, const size_t max) {
|
||||||
if(!f) {
|
snprintf(out, max, SAVE_LINUX_META_FILE_FORMAT, SAVE.platform.savePath);
|
||||||
file->exists = false;
|
}
|
||||||
|
|
||||||
|
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out) {
|
||||||
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, 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();
|
||||||
|
|
||||||
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t read = fread(file, sizeof(savefile_t), 1, f);
|
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData) {
|
||||||
fclose(f);
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||||
|
|
||||||
if(read != 1) {
|
slotData->version = SAVE_SLOT_VERSION;
|
||||||
file->exists = false;
|
|
||||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
file->exists = true;
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
|
||||||
|
saveJsonWriterDisposeLinux(&writer);
|
||||||
|
errorChain(ret);
|
||||||
|
|
||||||
|
slotData->exists = true;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file) {
|
errorret_t saveDeleteSlotLinux(const uint8_t slot) {
|
||||||
char_t path[SAVE_LINUX_PATH_MAX];
|
char_t path[SAVE_LINUX_PATH_MAX];
|
||||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||||
SAVE.platform.savePath, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveDeleteLinux(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
|
|
||||||
);
|
|
||||||
|
|
||||||
if(remove(path) != 0 && errno != ENOENT) {
|
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();
|
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
|
#pragma once
|
||||||
#include "error/error.h"
|
#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_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
|
#ifndef SAVE_LINUX_PATH
|
||||||
#define SAVE_LINUX_PATH "./saves"
|
#define SAVE_LINUX_PATH "./saves"
|
||||||
@@ -21,7 +23,8 @@ typedef struct {
|
|||||||
} savelinux_t;
|
} 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.
|
* @return An error code if initialization fails.
|
||||||
*/
|
*/
|
||||||
@@ -35,27 +38,43 @@ errorret_t saveInitLinux(void);
|
|||||||
errorret_t saveDisposeLinux(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 slot The save slot index.
|
||||||
* @param file Output save file data.
|
* @param out Output slot data.
|
||||||
* @return An error code if the load fails.
|
* @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 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.
|
* @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.
|
* @param slot The save slot index.
|
||||||
* @return An error code if the delete fails.
|
* @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
|
#pragma once
|
||||||
#include "save/savelinux.h"
|
#include "save/savelinux.h"
|
||||||
#include "save/savestreamlinux.h"
|
|
||||||
|
|
||||||
typedef savelinux_t saveplatform_t;
|
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 saveInitPlatform saveInitLinux
|
||||||
#define saveDisposePlatform saveDisposeLinux
|
#define saveDisposePlatform saveDisposeLinux
|
||||||
#define saveDeletePlatform saveDeleteLinux
|
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
#define saveSlotDeletePlatform saveDeleteSlotLinux
|
||||||
saveStreamOpenReadLinux(&(stream)->platform, &(stream)->found, slot)
|
#define saveSlotLoadPlatform saveSlotLoadLinux
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
#define saveSlotWritePlatform saveSlotWriteLinux
|
||||||
saveStreamOpenWriteLinux(&(stream)->platform, slot)
|
#define saveMetaLoadPlatform saveMetaLoadLinux
|
||||||
#define saveStreamClosePlatform(stream) \
|
#define saveMetaWritePlatform saveMetaWriteLinux
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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 "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_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||||
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL 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) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return settingsGet()->deadzone;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,12 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
savepsp.c
|
savepsp.c
|
||||||
savestreampsp.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 saveInitPlatform saveInitPSP
|
||||||
#define saveDisposePlatform saveDisposePSP
|
#define saveDisposePlatform saveDisposePSP
|
||||||
#define saveDeletePlatform saveDeletePSP
|
#define saveSlotDeletePlatform saveDeleteSlotPSP
|
||||||
|
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||||
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
||||||
@@ -22,16 +22,32 @@ typedef savestreampsp_t saveplatformstream_t;
|
|||||||
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
#define saveStreamSeekPlatform(stream, pos) \
|
||||||
saveStreamSeekPSP(&(stream)->platform, pos)
|
saveStreamSeekPSP(&(stream)->platform, pos)
|
||||||
|
#define saveStreamTellPlatform(stream, out) \
|
||||||
|
saveStreamTellPSP(&(stream)->platform, out)
|
||||||
|
|
||||||
// Save/load go entirely through the native sceUtilitySavedata dialog
|
// Save/load go entirely through the native sceUtilitySavedata dialog
|
||||||
// (savePSPBeginSave/Load), which spans multiple frames - these bypass
|
// (savePSPBeginSave/Load), which spans multiple frames - these bypass
|
||||||
// save.c's normal synchronous open/write-fields/close flow above (that's
|
// save.c's normal synchronous open/write-fields/close flow above (that's
|
||||||
// still used internally, just against an in-memory buffer, from within
|
// still used internally, just against an in-memory buffer, from within
|
||||||
// savePSPBeginSave/Load themselves) and are what save.c's saveWrite()/
|
// savePSPBeginSave/Load themselves) and are what save.c's saveWriteSlot()/
|
||||||
// saveLoad() actually call on this platform.
|
// saveLoadSlot()/saveWriteMeta()/saveLoadMeta() actually call on this
|
||||||
#define saveAsyncWritePlatform(slot, onComplete, user) \
|
// 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)
|
savePSPBeginSave(slot, onComplete, user)
|
||||||
#define saveAsyncLoadPlatform(slot, onComplete, user) \
|
#define saveSlotAsyncLoadPlatform(slot, onComplete, user) \
|
||||||
savePSPBeginLoad(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 saveIsBusyPlatform() savePSPIsBusy()
|
||||||
#define savePlatformUpdate() savePSPUpdate()
|
#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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDeletePSP(const uint8_t slot) {
|
errorret_t saveDeleteSlotPSP(const uint8_t slot) {
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
char_t path[SAVE_PSP_PATH_MAX];
|
||||||
stringFormat(
|
stringFormat(
|
||||||
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
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);
|
int32_t result = sceIoRemove(path);
|
||||||
if(result < 0 && result != (int32_t)0x80010002) {
|
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];
|
char_t dir[SAVE_PSP_PATH_MAX];
|
||||||
@@ -79,19 +79,19 @@ void savePSPBeginSave(
|
|||||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
|
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
|
// Serialize meta then the slot into the buffer synchronously (plain
|
||||||
// header/version/CRC framing as every other platform) before the dialog
|
// memory writes, same header/version/CRC framing as every other
|
||||||
// ever starts - only the actual commit-to-storage step needs to wait on
|
// platform) before the dialog ever starts - only the actual commit-to-
|
||||||
// the dialog.
|
// storage step needs to wait on the dialog.
|
||||||
savestream_t stream;
|
savestream_t stream;
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
stream.platform.buffer = SAVE.platform.dataBuffer;
|
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||||
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
|
|
||||||
errorret_t ret = saveFileWrite(&stream, file);
|
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
|
||||||
if(errorIsOk(ret)) ret = saveStreamFinalizeWriteImpl(&stream);
|
if(errorIsOk(ret)) ret = saveSlotSerializeWrite(&stream, slotData);
|
||||||
if(errorIsNotOk(ret)) {
|
if(errorIsNotOk(ret)) {
|
||||||
onComplete(ret, user);
|
onComplete(ret, user);
|
||||||
return;
|
return;
|
||||||
@@ -121,7 +121,7 @@ void savePSPBeginSave(
|
|||||||
// save browser entry.
|
// save browser entry.
|
||||||
stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title));
|
stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title));
|
||||||
stringCopy(
|
stringCopy(
|
||||||
param->sfoParam.savedataTitle, file->playerName,
|
param->sfoParam.savedataTitle, slotData->playerName,
|
||||||
sizeof(param->sfoParam.savedataTitle)
|
sizeof(param->sfoParam.savedataTitle)
|
||||||
);
|
);
|
||||||
stringCopy(
|
stringCopy(
|
||||||
@@ -157,10 +157,10 @@ void savePSPBeginLoad(
|
|||||||
|
|
||||||
SceIoStat stat;
|
SceIoStat stat;
|
||||||
if(sceIoGetstat(path, &stat) < 0) {
|
if(sceIoGetstat(path, &stat) < 0) {
|
||||||
// No save data for this slot yet - not an error (matches every other
|
// No save data yet - not an error (matches every other platform's
|
||||||
// platform's "nothing to load yet" behavior), and deliberately skips
|
// "nothing to load yet" behavior), and deliberately skips showing the
|
||||||
// showing the dialog at all rather than surfacing an empty "no data"
|
// dialog at all rather than surfacing an empty "no data" native
|
||||||
// native screen for a slot the player has never saved to.
|
// screen for data the player has never saved.
|
||||||
onComplete(errorOkImpl(), user);
|
onComplete(errorOkImpl(), user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -264,19 +264,20 @@ errorret_t savePSPUpdate(void) {
|
|||||||
SAVE.available = true;
|
SAVE.available = true;
|
||||||
|
|
||||||
if(op == SAVE_PSP_OP_LOAD) {
|
if(op == SAVE_PSP_OP_LOAD) {
|
||||||
savefile_t *file = &SAVE.files[slot];
|
|
||||||
savestream_t stream;
|
savestream_t stream;
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
stream.platform.buffer = SAVE.platform.dataBuffer;
|
stream.platform.buffer = SAVE.platform.dataBuffer;
|
||||||
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
|
||||||
stream.platform.length = SAVE.platform.param.dataSize;
|
stream.platform.length = SAVE.platform.param.dataSize;
|
||||||
|
|
||||||
errorret_t ret = saveFileLoad(&stream, file);
|
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
|
||||||
if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
|
if(errorIsOk(ret)) {
|
||||||
file->exists = errorIsOk(ret);
|
ret = saveSlotSerializeRead(&stream, &SAVE.slots[slot]);
|
||||||
|
}
|
||||||
cb(ret, user);
|
cb(ret, user);
|
||||||
} else {
|
} else {
|
||||||
SAVE.files[slot].exists = true;
|
SAVE.meta.exists = true;
|
||||||
|
SAVE.slots[slot].exists = true;
|
||||||
cb(errorOkImpl(), user);
|
cb(errorOkImpl(), user);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
+21
-15
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savemeta.h"
|
||||||
#include <pspiofilemgr.h>
|
#include <pspiofilemgr.h>
|
||||||
#include <psputility.h>
|
#include <psputility.h>
|
||||||
|
|
||||||
@@ -30,8 +31,13 @@ typedef enum {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
SceUtilitySavedataParam param;
|
SceUtilitySavedataParam param;
|
||||||
// Raw buffer sceUtilitySavedata reads/writes the whole save into/from -
|
// 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)
|
// populated by our own savestream_t serialization (see savestreampsp.h)
|
||||||
// before a save starts, and deserialized from after a load finishes.
|
// 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)));
|
uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64)));
|
||||||
size_t dataLength;
|
size_t dataLength;
|
||||||
|
|
||||||
@@ -66,24 +72,24 @@ errorret_t saveInitPSP(void);
|
|||||||
errorret_t saveDisposePSP(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.
|
* @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 -
|
* Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE -
|
||||||
* writes silently with just a brief icon flash, no confirm screen, since
|
* 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
|
* SAVE mode shows one even for a slot with no existing data - but
|
||||||
* PARAM.SFO/title/description are generated identically regardless of
|
* PARAM.SFO/title/description are generated identically regardless of
|
||||||
* mode, and the OS handles the save browser entry either way) for the
|
* mode, and the OS handles the save browser entry either way). Serializes
|
||||||
* given slot. Serializes SAVE.files[slot] into SAVE.platform.dataBuffer
|
* SAVE.meta then SAVE.slots[slot] into SAVE.platform.dataBuffer first,
|
||||||
* first, synchronously, then kicks off the dialog and returns - completion
|
* synchronously, then kicks off the dialog and returns - completion is
|
||||||
* is reported later via onComplete, driven by savePSPUpdate() each frame.
|
* reported later via onComplete, driven by savePSPUpdate() each frame.
|
||||||
* If no save data exists yet for this slot, sceUtilitySavedataInitStart()
|
* If no save data exists yet, sceUtilitySavedataInitStart() creates it.
|
||||||
* creates it.
|
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @param onComplete Callback invoked once the dialog finishes.
|
* @param onComplete Callback invoked once the dialog finishes.
|
||||||
@@ -95,11 +101,11 @@ void savePSPBeginSave(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD -
|
* Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD -
|
||||||
* see savePSPBeginSave() for why not the plain LOAD mode) for the given
|
* see savePSPBeginSave() for why not the plain LOAD mode) unless a quick
|
||||||
* slot, unless a quick sceIoGetstat check finds no save data for this slot
|
* sceIoGetstat check finds no save data yet - in which case onComplete is
|
||||||
* yet - in which case onComplete is invoked immediately with
|
* invoked immediately with SAVE.meta/SAVE.slots[slot].exists left false,
|
||||||
* SAVE.files[slot].exists left false, matching the other platforms'
|
* matching the other platforms' "no file yet" semantics, and no dialog is
|
||||||
* "no file yet" semantics, and no dialog is shown at all.
|
* shown at all.
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @param onComplete Callback invoked once the dialog (or immediate
|
* @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;
|
p->position = pos;
|
||||||
errorOk();
|
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.
|
* @return An error if pos is out of range.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
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 "input/input.h"
|
||||||
#include "save/settings.h"
|
#include "save/save.h"
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
{ .name = "triangle", {
|
{ .name = "triangle", {
|
||||||
@@ -84,5 +84,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return settingsGet()->deadzone;
|
return saveGetMeta()->deadzone;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user