Strip legacy save/settings systems for a unified savemanager rewrite; split uielement list out of uielement.c

Removes the per-platform save/savestream/autosave implementations, the settings UI frames, and the autosave overlay, replacing them with a savemanager.c/h + savefile.h/savedevice.h scaffold to build the new save system on top of. Also separates the concrete UI_ELEMENTS registration into uielementlist.h/.c so uielement.c only holds the generic per-element lifecycle logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:56:02 -05:00
parent 6c8e4d5cbd
commit 33f50a2c69
91 changed files with 402 additions and 5074 deletions
+4 -6
View File
@@ -21,8 +21,7 @@
#endif
#include "system/system.h"
#include "console/console.h"
#include "save/save.h"
#include "save/autosave.h"
#include "save/savemanager.h"\
engine_t ENGINE;
@@ -40,7 +39,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit());
errorChain(inputInit());
errorChain(assetInit());
errorChain(saveInit());
errorChain(saveManagerInit());
errorChain(localeManagerInit());
errorChain(displayInit());
errorChain(uiInit());
@@ -68,8 +67,7 @@ errorret_t engineUpdate(void) {
#ifdef DUSK_NETWORK
errorChain(networkUpdate());
#endif
errorChain(saveUpdate());
autoSaveUpdate();
// errorChain(saveManagerUpdate());
timeUpdate();
inputUpdate();
consoleUpdate();
@@ -98,7 +96,7 @@ errorret_t engineDispose(void) {
errorChain(uiDispose());
consoleDispose();
errorChain(displayDispose());
errorChain(saveDispose());
errorChain(saveManagerDispose());
errorChain(assetDispose());
errorOk();
+18 -4
View File
@@ -13,17 +13,31 @@ typedef struct {
const char_t *file;
} localeinfo_t;
static const localeinfo_t LOCALE_EN_US = {
static const localeinfo_t LOCALE_INFO_EN_US = {
.name = "en-US",
.file = "locale/en_US.po",
};
static const localeinfo_t LOCALE_JP_JP = {
static const localeinfo_t LOCALE_INFO_JP_JP = {
.name = "ja-JP",
.file = "locale/jp_JP.po",
};
static const localeinfo_t LOCALE_ES_MX = {
static const localeinfo_t LOCALE_INFO_ES_MX = {
.name = "es-MX",
.file = "locale/es_MX.po",
};
};
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
&LOCALE_INFO_EN_US,
&LOCALE_INFO_JP_JP,
&LOCALE_INFO_ES_MX
};
#define LOCALE_INFO_LIST_COUNT ( \
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
)
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
// EOF
+12 -26
View File
@@ -9,40 +9,23 @@
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "save/save.h"
#include "ui/ui.h"
#include "system/system.h"
#include "console/console.h"
localemanager_t LOCALE;
const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT] = {
&LOCALE_EN_US,
&LOCALE_JP_JP,
&LOCALE_ES_MX
};
errorret_t localeManagerInit() {
memoryZero(&LOCALE, sizeof(localemanager_t));
// saveInit() runs before this (see engine.c) and, on most platforms,
// has already loaded a persisted language choice into savemeta_t by
// this point - see localeManagerGetByIndex().
errorChain(localeManagerSetLocale(
localeManagerGetByIndex(saveGetMeta()->language)
));
// TODO: Set locale based on system locale.
const localeinfo_t *locale = systemGetLocale();
errorChain(localeManagerSetLocale(locale));
consolePrint("Locale set to: %s", locale->name);
errorOk();
}
uint8_t localeManagerGetIndex(const localeinfo_t *locale) {
assertNotNull(locale, "Locale cannot be NULL");
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
if(stringCompare(LOCALE_LIST[i]->file, locale->file) == 0) return i;
}
return 0;
}
const localeinfo_t * localeManagerGetByIndex(const uint8_t index) {
if(index >= LOCALE_LIST_COUNT) return LOCALE_LIST[0];
return LOCALE_LIST[index];
}
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assertNotNull(locale, "Locale cannot be NULL");
@@ -56,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
// TODO : Trigger UI update.
errorChain(uiUpdateTranslations());
errorOk();
}
+1 -31
View File
@@ -7,7 +7,7 @@
#pragma once
#include "error/error.h"
#include "localemanager.h"
#include "locale/localemanager.h"
#include "locale/localeinfo.h"
#include "asset/asset.h"
@@ -18,15 +18,6 @@ typedef struct {
extern localemanager_t LOCALE;
/**
* Every locale the game supports, in a fixed, stable order - index into
* this array is what gets persisted as savemeta_t.language (see
* save/savemeta.h), so the order here must never change once shipped
* (only append new locales at the end).
*/
#define LOCALE_LIST_COUNT 3
extern const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT];
/**
* Initialize the locale system.
*
@@ -42,27 +33,6 @@ errorret_t localeManagerInit();
*/
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
/**
* Finds the LOCALE_LIST index matching the given locale's file, by
* content rather than pointer identity (localeinfo_t instances are
* declared `static const` in a header, so the same locale gets a
* distinct pointer in every translation unit that references it).
*
* @param locale The locale to find.
* @return The matching index in LOCALE_LIST, or 0 if not found.
*/
uint8_t localeManagerGetIndex(const localeinfo_t *locale);
/**
* Gets the locale at the given LOCALE_LIST index, clamping to index 0
* (LOCALE_EN_US) if out of range - e.g. a save file's persisted language
* index from a future build with more locales than this one supports.
*
* @param index The index to look up.
* @return The locale at that index, or LOCALE_LIST[0] if out of range.
*/
const localeinfo_t * localeManagerGetByIndex(const uint8_t index);
/**
* Get a localized string for the given message ID.
*
@@ -6,5 +6,4 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
globalitemstore.c
)
@@ -1,25 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "globalitemstore.h"
#include "assert/assert.h"
bool_t globalItemStoreIsCollected(
const saveslot_t *file, const entityglobalid_t id
) {
assertNotNull(file, "Save slot cannot be NULL");
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
return file->globalItemCollected[id];
}
void globalItemStoreSetCollected(
saveslot_t *file, const entityglobalid_t id, const bool_t collected
) {
assertNotNull(file, "Save slot cannot be NULL");
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
file->globalItemCollected[id] = collected;
}
@@ -1,40 +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/saveslot.h"
#include "rpg/entity/entity.h"
/**
* Checks whether the global entity with the given ID has already been
* marked collected in the given save slot's data - e.g. so a global item
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
* skip spawning itself if the player already picked it up in a prior
* session, without needing to keep the entity itself alive to remember
* that (which would need render/collision special-casing - this doesn't).
*
* @param file The save slot to check.
* @param id The global entity ID to check.
* @return True if already marked collected.
*/
bool_t globalItemStoreIsCollected(
const saveslot_t *file, const entityglobalid_t id
);
/**
* Marks the global entity with the given ID as collected (or not) in the
* given save slot's data. Does not itself write the save to disk - call
* saveWriteSlot() separately once ready to persist it.
*
* @param file The save slot to write into.
* @param id The global entity ID to mark.
* @param collected The new collected state.
*/
void globalItemStoreSetCollected(
saveslot_t *file, const entityglobalid_t id, const bool_t collected
);
+4 -25
View File
@@ -17,11 +17,8 @@
#include "rpgcamera.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "save/save.h"
#include "save/autosave.h"
#include "error/error.h"
#include "scene/scene.h"
#include "ui/rpg/uiemoji.h"
#include "rpg/story/storyflag.h"
@@ -29,27 +26,14 @@ errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT);
// saveInit() eagerly loads every slot from disk, but there's no
// continue-game flow yet - every boot is a fresh game regardless of
// what was found on disk, so force this false rather than let a stale
// "exists" from a real save skip storyFlagInitDefaults() below while
// everything else here still hardcodes new-game state.
saveSlot->exists = false;
// Must run before any code reads a story flag - stamps CSV-defined
// defaults onto the active save slot since it's now forced to look
// unloaded.
storyFlagInitDefaults(saveSlot);
backpackInit();
partyInit();
cutsceneSystemInit();
errorChain(mapInit());
rpgCameraInit();
// Init world
// Init test world
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
// The player is the one entity that isn't sourced from map/chunk data -
@@ -68,6 +52,7 @@ errorret_t rpgInit(void) {
backpackAdd(ITEM_ID_POTATO, 3);
backpackAdd(ITEM_ID_APPLE, 8);
// All Good!
errorOk();
}
@@ -79,12 +64,6 @@ errorret_t rpgUpdate(void) {
}
#endif
// A failed autosave forces a no-save-device prompt (see autosave.h) -
// freeze the world entirely until the player resolves it, rather than
// letting NPCs/cutscenes/camera keep running behind a modal that's
// supposed to be blocking.
if(autoSaveIsBlocking()) errorOk();
// TODO: Do not update if the scene is not the map scene?
errorChain(mapUpdate());
+11 -14
View File
@@ -7,20 +7,17 @@
#include "storyflag.h"
#include "assert/assert.h"
#include "console/console.h"
storyflagvalue_t storyFlagGet(const storyflag_t flag) {
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
consolePrint("Story flag get without save implementation");
return 0;
}
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
}
void storyFlagInitDefaults(saveslot_t *file) {
assertNotNull(file, "Save slot cannot be NULL");
if(file->exists) return;
assertTrue(
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
"Too many story flags for the save format - bump SAVE_STORY_FLAG_COUNT_MAX"
);
for(storyflag_t i = 0; i < STORY_FLAG_COUNT; i++) {
file->storyFlags[i] = STORY_FLAG_DEFAULTS[i];
}
}
// TODO: Dirty savefile
consolePrint("Story flag set without save implementation");
}
+2 -16
View File
@@ -7,7 +7,6 @@
#pragma once
#include "rpg/story/storyflagvalue.h"
#include "save/save.h"
/**
* Gets the value of a story flag. Reads directly from the active save
@@ -16,25 +15,12 @@
* @param flag The story flag to get.
* @return The value of the story flag.
*/
#define storyFlagGet(flag) (saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[(flag)])
storyflagvalue_t storyFlagGet(const storyflag_t flag);
/**
* 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
* saveWriteSlot() separately once ready to persist it.
* Sets the value of a story flag. Will dirty the savefile.
*
* @param flag The story flag to set.
* @param value The value to set the story flag to.
*/
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
/**
* Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto
* the given save slot, but only if it hasn't actually been loaded from
* disk yet (file->exists is false) - otherwise leaves already-played
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
* code reads a story flag.
*
* @param file The save slot to stamp defaults onto.
*/
void storyFlagInitDefaults(saveslot_t *file);
+1 -3
View File
@@ -6,7 +6,5 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
save.c
savestream.c
autosave.c
savemanager.c
)
-72
View File
@@ -1,72 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "autosave.h"
#include "save.h"
#include "error/error.h"
#include "ui/frame/initial/uiinitialnocard.h"
autosave_t AUTO_SAVE;
static void autoSaveNoCardResult(const bool_t retry, void *user) {
AUTO_SAVE.blocking = false;
// "Retry" just re-queues the write for the next tick - there's no way
// to force a re-probe of the hardware mid-session (see saveInit()'s doc
// comment), so this only actually helps for a transient failure.
if(retry) {
AUTO_SAVE.pending = true;
return;
}
// The player explicitly acknowledged there's no save device and chose
// to proceed anyway - sticks for the rest of the session, so future
// autoSaveUpdate() calls silently skip instead of prompting again.
saveMarkTemporary();
}
static void autoSaveWriteComplete(errorret_t result, void *user) {
AUTO_SAVE.saving = false;
if(errorIsOk(result)) return;
errorCatch(errorPrint(result));
// The write failed - most likely no save medium is present right now
// (a GameCube with no memory card inserted, for example). Pause world
// simulation and force the same prompt the initial boot scene uses so
// the player can insert a card and retry, or explicitly accept a
// temporary, unsaved session.
AUTO_SAVE.blocking = true;
uiInitialNoCardOpen(autoSaveNoCardResult, NULL);
}
void autoSaveQueue(void) {
AUTO_SAVE.pending = true;
}
void autoSaveUpdate(void) {
if(AUTO_SAVE.blocking) return;
if(!AUTO_SAVE.pending) return;
if(saveIsBusy()) return;
AUTO_SAVE.pending = false;
// Already opted out of saving for this session - nothing to do, and
// definitely don't reprompt every time an autosave is queued.
if(saveIsTemporary()) return;
AUTO_SAVE.saving = true;
saveWriteSlot(SAVE_ACTIVE_SLOT, autoSaveWriteComplete, NULL);
}
bool_t autoSaveIsSaving(void) {
return AUTO_SAVE.saving;
}
bool_t autoSaveIsBlocking(void) {
return AUTO_SAVE.blocking;
}
-57
View File
@@ -1,57 +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"
typedef struct {
/** True from autoSaveQueue() until the queued write starts. */
bool_t pending;
/** True while the queued write is actually in flight. */
bool_t saving;
/**
* True while the forced no-save-device prompt is up after a queued
* write failed - see autoSaveUpdate(). World simulation must pause
* while this is true (see rpgUpdate()).
*/
bool_t blocking;
} autosave_t;
extern autosave_t AUTO_SAVE;
/**
* Queues an autosave to run on a future engine tick. Safe to call as
* often as needed (e.g. after every map transition or story event) -
* repeated calls before the queued save starts just collapse into one.
*/
void autoSaveQueue(void);
/**
* Pumps the queued autosave: starts the write once nothing else is
* using the save system, and if that write fails (e.g. a GameCube with
* no memory card inserted), forces the same no-save-device prompt the
* initial boot scene uses and pauses world simulation until the player
* resolves it. Must be called every engine frame, after saveUpdate().
*/
void autoSaveUpdate(void);
/**
* True while a queued autosave's write is actually in progress. Intended
* for UI to show a "Saving" indicator.
*
* @return true if an autosave write is currently in flight.
*/
bool_t autoSaveIsSaving(void);
/**
* True while a failed autosave is forcing the no-save-device prompt and
* waiting on the player's decision. Intended for the world simulation to
* pause while this is true.
*
* @return true if an autosave is currently blocking on player input.
*/
bool_t autoSaveIsBlocking(void);
-188
View File
@@ -1,188 +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/savestream.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "error/error.h"
save_t SAVE;
const saveslot_t SAVE_DEFAULT = {
.header = {
SAVE_SLOT_HEADER[0], SAVE_SLOT_HEADER[1], SAVE_SLOT_HEADER[2]
},
.version = SAVE_SLOT_VERSION,
.exists = true,
.playerName = "Player"
// globalItemCollected/storyFlags are left at their zero default.
};
static void _saveEagerLoadComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
}
errorret_t saveInit(void) {
memoryZero(&SAVE, sizeof(save_t));
SAVE.meta.deadzone = SAVE_META_DEADZONE_DEFAULT;
SAVE.meta.language = SAVE_META_LANGUAGE_DEFAULT;
#ifdef saveInitPlatform
// A missing/unreachable save medium is expected, recoverable state,
// not a reason to fail booting the whole game - log it and carry on
// with SAVE.available false instead of chaining the error upward.
errorret_t result = saveInitPlatform();
SAVE.available = errorIsOk(result);
if(!SAVE.available) errorCatch(errorPrint(result));
#else
SAVE.available = false;
#endif
// Eagerly pull meta + every slot into memory up front - cheap and
// harmless (nothing consumes loaded slot data automatically; there's no
// continue-game flow yet). PSP opts out entirely (see
// saveSkipEagerLoadPlatform) since its only read path is now the native
// savedata dialog, and running that on every boot would defeat the whole
// point of folding meta into it instead of a separate instant-write file.
#ifndef saveSkipEagerLoadPlatform
if(SAVE.available) {
saveLoadMeta(_saveEagerLoadComplete, NULL);
for(uint8_t i = 0; i < SAVE_SLOT_COUNT_MAX; i++) {
saveLoadSlot(i, _saveEagerLoadComplete, NULL);
}
}
#endif
errorOk();
}
bool_t saveIsAvailable(void) {
return SAVE.available && !SAVE.temporary;
}
void saveMarkTemporary(void) {
SAVE.temporary = true;
}
bool_t saveIsTemporary(void) {
return SAVE.temporary;
}
errorret_t saveDispose(void) {
#ifdef saveDisposePlatform
errorChain(saveDisposePlatform());
#endif
errorOk();
}
errorret_t saveUpdate(void) {
#ifdef savePlatformUpdate
errorChain(savePlatformUpdate());
#endif
errorOk();
}
bool_t saveIsBusy(void) {
#ifdef saveIsBusyPlatform
return saveIsBusyPlatform();
#else
return false;
#endif
}
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
assertNotNull(onComplete, "onComplete cannot be NULL");
SAVE.slots[slot].exists = false;
// Some platforms (PSP's native save dialog) can't complete within this
// call - they take over entirely and invoke onComplete later, from
// saveUpdate(), once their own multi-frame flow finishes. Those
// platforms never define the sync saveSlotLoadPlatform() at all, so the
// fallback below must live in the #else, not just after an early return.
#ifdef saveSlotAsyncLoadPlatform
saveSlotAsyncLoadPlatform(slot, onComplete, user);
#else
errorret_t ret = saveSlotLoadPlatform(slot, &SAVE.slots[slot]);
SAVE.available = errorIsOk(ret);
onComplete(ret, user);
#endif
}
void saveLoadDefault(const uint8_t slot) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
SAVE.slots[slot] = SAVE_DEFAULT;
}
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
assertNotNull(onComplete, "onComplete cannot be NULL");
// See saveLoadSlot() - some platforms take over and complete later.
#ifdef saveSlotAsyncWritePlatform
saveSlotAsyncWritePlatform(slot, onComplete, user);
#else
errorret_t ret = saveSlotWritePlatform(slot, &SAVE.slots[slot]);
SAVE.available = errorIsOk(ret);
onComplete(ret, user);
#endif
}
errorret_t saveDeleteSlot(const uint8_t slot) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
#ifdef saveSlotDeletePlatform
errorret_t deleteRet = saveSlotDeletePlatform(slot);
SAVE.available = errorIsOk(deleteRet);
errorChain(deleteRet);
#endif
SAVE.slots[slot].exists = false;
errorOk();
}
bool_t saveSlotExists(const uint8_t slot) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
return SAVE.slots[slot].exists;
}
saveslot_t * saveGetSlot(const uint8_t slot) {
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
return &SAVE.slots[slot];
}
void saveLoadMeta(savecallback_t onComplete, void *user) {
assertNotNull(onComplete, "onComplete cannot be NULL");
SAVE.meta.exists = false;
#ifdef saveMetaAsyncLoadPlatform
saveMetaAsyncLoadPlatform(onComplete, user);
#else
errorret_t ret = saveMetaLoadPlatform(&SAVE.meta);
SAVE.available = errorIsOk(ret);
onComplete(ret, user);
#endif
}
void saveWriteMeta(savecallback_t onComplete, void *user) {
assertNotNull(onComplete, "onComplete cannot be NULL");
#ifdef saveMetaAsyncWritePlatform
saveMetaAsyncWritePlatform(onComplete, user);
#else
errorret_t ret = saveMetaWritePlatform(&SAVE.meta);
SAVE.available = errorIsOk(ret);
onComplete(ret, user);
#endif
}
savemeta_t * saveGetMeta(void) {
return &SAVE.meta;
}
-210
View File
@@ -1,210 +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 "saveslot.h"
#include "savemeta.h"
#include "save/saveplatform.h"
typedef struct {
/** Per-slot save data; indexed 0 to SAVE_SLOT_COUNT_MAX - 1. */
saveslot_t slots[SAVE_SLOT_COUNT_MAX];
/** Device-wide preferences - see savemeta.h. */
savemeta_t meta;
/** Platform-specific save system state (paths, card handles, etc.). */
saveplatform_t platform;
/**
* True if the save medium (memory card/stick/disk) was reachable the
* last time it was checked - at saveInit(), and refreshed by every
* subsequent load/write attempt. Starting the game with no card/stick
* inserted, or one being removed mid-session, are both expected
* conditions here, not fatal errors - see saveIsAvailable().
*/
bool_t available;
/**
* True once the player has explicitly acknowledged there's no save
* device and chosen to continue anyway (see the initial scene's "no
* card" prompt) - sticky for the rest of the session, folded into
* saveIsAvailable() so every existing/future call site that already
* gates on that automatically refuses to save/load from here on. See
* saveMarkTemporary()/saveIsTemporary().
*/
bool_t temporary;
/**
* Scratch error state used by platforms whose save/load completes
* asynchronously (see saveIsBusy()) to construct a result to hand to a
* savecallback_t from inside saveUpdate(), rather than from a direct
* errorThrow() return - mirrors network_t.errorState for the same reason.
*/
errorstate_t errorState;
} save_t;
extern save_t SAVE;
/**
* Initializes the save system. Never fails the way saveWriteSlot()/
* saveWriteMeta() can - if the platform's save medium isn't reachable
* (e.g. no memory card/stick inserted), that's logged and reflected in
* saveIsAvailable() rather than treated as fatal, since the game should
* still be playable without save support.
*
* On most platforms, this also eagerly loads meta and every slot from
* disk immediately - cheap and harmless, since nothing consumes the
* loaded slot data automatically (there's no "continue game" flow yet;
* rpgInit() always hardcodes a fresh game regardless of what's loaded).
* PSP skips this (see saveSkipEagerLoadPlatform) - its only read path is
* the native sceUtilitySavedata dialog now that meta lives inside the
* same payload as the save slot, and running that multi-frame dialog on
* every single boot would reintroduce the exact UX problem a lightweight
* settings-only file used to avoid.
*
* @return An error code only for unexpected platform failures.
*/
errorret_t saveInit(void);
/**
* Checks whether the save medium was reachable as of the last load/write
* attempt (or saveInit(), if none has been attempted yet), AND the
* session hasn't been marked temporary (see saveMarkTemporary()).
* Intended for UI to decide whether to offer saving/loading at all, or to
* explain why it isn't available right now - e.g. "No memory card
* inserted".
*
* @return true if saving/loading can be attempted right now.
*/
bool_t saveIsAvailable(void);
/**
* Marks this session as temporary - the player explicitly acknowledged
* there's no save device and chose to continue anyway. One-way: nothing
* currently re-probes the save medium mid-session (see saveInit()'s doc
* comment), so there's no legitimate way to un-stick this once set short
* of restarting the game.
*/
void saveMarkTemporary(void);
/**
* True once saveMarkTemporary() has been called this session. Distinct
* from saveIsAvailable() so UI can give a more specific message (e.g.
* "this session is temporary" rather than the generic "no save device
* found") once the player has already made that choice.
*
* @return true if this session has been marked temporary.
*/
bool_t saveIsTemporary(void);
/**
* Disposes of the save system.
*
* @return An error code if disposal fails.
*/
errorret_t saveDispose(void);
/**
* Updates the save manager, pumping any in-progress async save/load and
* dispatching its callback once complete. No-op on platforms where every
* operation always completes synchronously (see saveIsBusy()). Must be
* called every engine frame for platforms that need it (PSP's native save
* dialog spans multiple frames).
*
* @return An error code indicating success or failure.
*/
errorret_t saveUpdate(void);
/**
* True while an async save/load is in progress (e.g. PSP's native save
* dialog is open). Calling any save/load function again while this is true
* is undefined behavior - wait for the previous call's callback first.
*
* @return True if a save/load request is currently in progress.
*/
bool_t saveIsBusy(void);
/**
* Loads the save slot for a given index from persistent storage. Slow/
* async on some platforms (PSP's native save dialog spans multiple
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
* call returns. See saveIsBusy().
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
* @param onComplete Callback invoked with the result once loading finishes.
* @param user User data passed through to onComplete.
*/
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user);
/**
* Resets a save slot in memory to SAVE_DEFAULT, for starting a new game
* without reading anything from persistent storage. Synchronous - no
* disk I/O is involved, so there's no callback to wait on.
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1) to reset.
*/
void saveLoadDefault(const uint8_t slot);
/**
* Writes the save slot for a given index to persistent storage. Slow/
* async on some platforms (PSP's native save dialog spans multiple
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
* call returns. See saveIsBusy().
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
* @param onComplete Callback invoked with the result once writing finishes.
* @param user User data passed through to onComplete.
*/
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user);
/**
* Deletes the save slot for a given index from persistent storage.
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
* @return An error code if the delete fails.
*/
errorret_t saveDeleteSlot(const uint8_t slot);
/**
* Checks whether a save slot has data for a given index.
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
* @return true if the slot has data, false otherwise.
*/
bool_t saveSlotExists(const uint8_t slot);
/**
* Gets a pointer to the save slot data for a given index.
*
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
* @return A pointer to the saveslot_t for the given 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);
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef enum {
SAVE_DEVICE_STATE_UNKNOWN,
SAVE_DEVICE_STATE_UNAVAILABLE,
SAVE_DEVICE_STATE_AVAILABLE,
SAVE_DEVICE_STATE_MOUNTING,
SAVE_DEVICE_STATE_MOUNTED,
SAVE_DEVICE_STATE_ERRORED
} savedevicestate_t;
typedef struct {
savedevicestate_t state;
} savedevice_t;
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "savemanager.h"
errorret_t saveManagerInit() {
errorOk();
}
errorret_t saveManagerDispose() {
errorOk();
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
void *nothing;
} savemanager_t;
extern savemanager_t SAVE_MANAGER;
/**
* Initializes the save manager, this does not do anything related to mounting
* files, memory cards, etc, this is entirely prepping for that capability.
*/
errorret_t saveManagerInit();
/**
* Disposes of the save manager.
*/
errorret_t saveManagerDispose();
-63
View File
@@ -1,63 +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"
/** 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
/**
* Default language index for meta that's never actually been loaded from
* disk yet - index 0 into LOCALE_LIST (see locale/localemanager.h), i.e.
* LOCALE_EN_US.
*/
#define SAVE_META_LANGUAGE_DEFAULT 0
/**
* 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;
/**
* Index into LOCALE_LIST (see locale/localemanager.h) for the player's
* preferred UI language - the save meta is the only place this lives;
* read it directly via saveGetMeta()->language rather than caching it
* anywhere else. Never reorder/remove entries from LOCALE_LIST, only
* append, since this index must keep meaning the same locale across
* versions.
*/
uint8_t language;
} savemeta_t;
-100
View File
@@ -1,100 +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"
/** Save slot format version. Increment on breaking change. */
#define SAVE_SLOT_VERSION 1
/** Magic bytes that identify a Dusk save slot. */
#define SAVE_SLOT_HEADER "DSK"
/** Byte length of the magic header (excludes the null terminator). */
#define SAVE_SLOT_HEADER_SIZE (sizeof(SAVE_SLOT_HEADER) - 1)
/**
* Maximum number of independent save slots supported. Platform-overridable
* via a compiler define (not a header #ifndef alone, since this header is
* included before any platform header gets a chance to react) - see PSP's
* CMakeLists.txt, which overrides this to 1.
*/
#ifndef SAVE_SLOT_COUNT_MAX
#define SAVE_SLOT_COUNT_MAX 3
#endif
/**
* The save slot actually used for gameplay right now - there's no slot
* select/multi-save UX yet (SAVE_SLOT_COUNT_MAX > 1 exists for later), so
* every part of the game that needs "the" save slot (the game menu's Save
* button, etc.) reads/writes this one slot.
*/
#define SAVE_ACTIVE_SLOT 0
/** Maximum length of a saved player name, including the null terminator. */
#define SAVE_PLAYER_NAME_MAX 32
/**
* Maximum number of global entities whose "collected" state can be
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
* rather than tied to ENTITY_GLOBAL_LIST_COUNT, since saveslot.h is a
* leaf header with no dependency on the entity system (and no reason to
* take one just for a size constant).
*/
#define SAVE_GLOBAL_ITEM_COUNT_MAX 64
/**
* Maximum number of story flags the save format can hold - see
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
* current flag count) rather than tied to STORY_FLAG_COUNT, since
* saveslot.h is a leaf header with no dependency on generated story
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
*/
#define SAVE_STORY_FLAG_COUNT_MAX 128
/** Per-slot game progress - the state a "save file" traditionally means. */
typedef struct {
/** Magic header bytes read from the slot; must equal SAVE_SLOT_HEADER. */
char_t header[SAVE_SLOT_HEADER_SIZE];
/** Format version read from the slot; used to branch on older layouts. */
uint32_t version;
/** Runtime flag - true if this slot was successfully loaded or written. */
bool_t exists;
/** The player's saved name. */
char_t playerName[SAVE_PLAYER_NAME_MAX];
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
/**
* Story flag values, indexed by storyflag_t - the save slot is the only
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
* rpg/story/storyflag.h), not directly.
*/
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
} saveslot_t;
/**
* Callback invoked when an async saveWriteSlot()/saveLoadSlot()/
* saveWriteMeta()/saveLoadMeta() request completes. Declared here (rather
* than save.h) so platform save headers - which save.h's platform
* indirection pulls in before save.h finishes defining anything else - can
* reference it without a circular include.
*
* @param result Whether the request succeeded.
* @param user User data passed through from the original call.
*/
typedef void (*savecallback_t)(errorret_t result, void *user);
/**
* The blank template used to (re)initialize a save slot for a brand new
* game via saveLoadDefault(), rather than reading one from persistent
* storage. Deliberately generic at this layer (empty collected-item
* flags, no story flags set) - anything that needs CSV-defined story flag
* defaults layers that on top afterward (see storyFlagInitDefaults()),
* since saveslot.h stays a leaf header with no dependency on generated
* story content (see SAVE_STORY_FLAG_COUNT_MAX's doc comment above).
*/
extern const saveslot_t SAVE_DEFAULT;
-403
View File
@@ -1,403 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/savestream.h"
#include "util/crypt.h"
#include "util/endian.h"
#include "util/string.h"
#include "util/memory.h"
errorret_t saveStreamReadBytesRawImpl(
savestream_t *stream, void *buf, const size_t len
) {
#ifdef saveStreamReadBytesPlatform
errorChain(saveStreamReadBytesPlatform(stream, buf, len));
#endif
errorOk();
}
errorret_t saveStreamWriteBytesRawImpl(
savestream_t *stream, const void *buf, const size_t len
) {
#ifdef saveStreamWriteBytesPlatform
errorChain(saveStreamWriteBytesPlatform(stream, buf, len));
#endif
errorOk();
}
errorret_t saveStreamReadBytesImpl(
savestream_t *stream, void *buf, const size_t len
) {
errorChain(saveStreamReadBytesRawImpl(stream, buf, len));
cryptCRC32Update(&stream->checksum, buf, len);
errorOk();
}
errorret_t saveStreamWriteBytesImpl(
savestream_t *stream, const void *buf, const size_t len
) {
cryptCRC32Update(&stream->checksum, buf, len);
errorChain(saveStreamWriteBytesRawImpl(stream, buf, len));
errorOk();
}
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out) {
#ifdef saveStreamTellPlatform
errorChain(saveStreamTellPlatform(stream, out));
#else
*out = 0;
#endif
errorOk();
}
errorret_t saveStreamFinalizeWriteImpl(
savestream_t *stream, const size_t headerPosition, const size_t headerSize
) {
uint32_t finalCRC = cryptCRC32End(stream->checksum);
uint32_t leChecksum = endianLittleToHost32(finalCRC);
#ifdef saveStreamSeekPlatform
errorChain(saveStreamSeekPlatform(stream, headerPosition + headerSize));
#endif
errorChain(saveStreamWriteBytesRawImpl(
stream, &leChecksum, sizeof(uint32_t)
));
errorOk();
}
errorret_t saveStreamVerifyChecksumImpl(
savestream_t *stream, const char_t *sectionLabel
) {
uint32_t computed = cryptCRC32End(stream->checksum);
if(computed != stream->expectedChecksum) {
errorThrow("%s has invalid checksum", sectionLabel);
}
errorOk();
}
errorret_t saveStreamReadHeaderImpl(
savestream_t *stream, char_t *header, const char_t *expectedHeader,
const size_t headerSize
) {
errorChain(saveStreamReadBytesRawImpl(stream, header, headerSize));
for(size_t i = 0; i < headerSize; i++) {
if(header[i] != expectedHeader[i]) {
errorThrow("Save data has invalid header");
}
}
uint32_t leChecksum;
errorChain(saveStreamReadBytesRawImpl(stream, &leChecksum, sizeof(uint32_t)));
stream->expectedChecksum = endianLittleToHost32(leChecksum);
stream->checksum = cryptCRC32Begin();
errorOk();
}
errorret_t saveStreamWriteHeaderImpl(
savestream_t *stream, const char_t *header, const size_t headerSize
) {
errorChain(saveStreamWriteBytesRawImpl(stream, header, headerSize));
uint32_t placeholder = 0;
errorChain(saveStreamWriteBytesRawImpl(
stream, &placeholder, sizeof(uint32_t)
));
stream->checksum = cryptCRC32Begin();
errorOk();
}
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
*out = endianLittleToHost32(raw);
errorOk();
}
errorret_t saveStreamWriteVersionImpl(
savestream_t *stream, const uint32_t *input
) {
uint32_t raw = endianLittleToHost32(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out) {
uint8_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint8_t)));
*out = (bool_t)(raw != 0);
errorOk();
}
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input) {
uint8_t raw = *input ? 1 : 0;
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t)));
errorOk();
}
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out) {
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(int8_t)));
errorOk();
}
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input) {
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(int8_t)));
errorOk();
}
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out) {
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(uint8_t)));
errorOk();
}
errorret_t saveStreamWriteUInt8Impl(
savestream_t *stream, const uint8_t *input
) {
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(uint8_t)));
errorOk();
}
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out) {
uint16_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
uint16_t host = endianLittleToHost16(raw);
memoryCopy(out, &host, sizeof(int16_t));
errorOk();
}
errorret_t saveStreamWriteInt16Impl(
savestream_t *stream, const int16_t *input
) {
uint16_t raw;
memoryCopy(&raw, input, sizeof(int16_t));
raw = endianLittleToHost16(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
errorOk();
}
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out) {
uint16_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
*out = endianLittleToHost16(raw);
errorOk();
}
errorret_t saveStreamWriteUInt16Impl(
savestream_t *stream, const uint16_t *input
) {
uint16_t raw = endianLittleToHost16(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
errorOk();
}
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
uint32_t host = endianLittleToHost32(raw);
memoryCopy(out, &host, sizeof(int32_t));
errorOk();
}
errorret_t saveStreamWriteInt32Impl(
savestream_t *stream, const int32_t *input
) {
uint32_t raw;
memoryCopy(&raw, input, sizeof(int32_t));
raw = endianLittleToHost32(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
*out = endianLittleToHost32(raw);
errorOk();
}
errorret_t saveStreamWriteUInt32Impl(
savestream_t *stream, const uint32_t *input
) {
uint32_t raw = endianLittleToHost32(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
uint64_t host = endianLittleToHost64(raw);
memoryCopy(out, &host, sizeof(int64_t));
errorOk();
}
errorret_t saveStreamWriteInt64Impl(
savestream_t *stream, const int64_t *input
) {
uint64_t raw;
memoryCopy(&raw, input, sizeof(int64_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
*out = endianLittleToHost64(raw);
errorOk();
}
errorret_t saveStreamWriteUInt64Impl(
savestream_t *stream, const uint64_t *input
) {
uint64_t raw = endianLittleToHost64(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out) {
float_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(float_t)));
*out = endianLittleToHostFloat(raw);
errorOk();
}
errorret_t saveStreamWriteFloatImpl(
savestream_t *stream, const float_t *input
) {
float_t raw = endianLittleToHostFloat(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(float_t)));
errorOk();
}
errorret_t saveStreamReadStringImpl(
savestream_t *stream, char_t *out, const size_t maxLen
) {
for(size_t i = 0; i < maxLen; i++) {
errorChain(saveStreamReadBytesImpl(stream, &out[i], sizeof(char_t)));
if(out[i] == '\0') errorOk();
}
out[maxLen - 1] = '\0';
errorOk();
}
errorret_t saveStreamWriteStringImpl(
savestream_t *stream, const char_t *input, const size_t maxLen
) {
size_t len = strlen(input);
if(len >= maxLen) len = maxLen - 1;
errorChain(saveStreamWriteBytesImpl(stream, input, len + 1));
errorOk();
}
errorret_t saveStreamReadDateImpl(savestream_t *stream, dusktimeepoch_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->time, &raw, sizeof(double_t));
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->timeZone, &raw, sizeof(double_t));
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->offsetTime, &raw, sizeof(double_t));
errorOk();
}
errorret_t saveStreamWriteDateImpl(
savestream_t *stream, const dusktimeepoch_t *input
) {
uint64_t raw;
memoryCopy(&raw, &input->time, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
memoryCopy(&raw, &input->timeZone, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
memoryCopy(&raw, &input->offsetTime, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta) {
saveFileReadHeader(
stream, meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE
);
saveFileReadVersion(stream, &meta->version);
saveFileReadFloat(stream, &meta->deadzone);
saveFileReadUInt8(stream, &meta->language);
errorChain(saveStreamVerifyChecksumImpl(stream, "Save meta"));
meta->exists = true;
errorOk();
}
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta) {
memoryCopy(meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE);
meta->version = SAVE_META_VERSION;
size_t headerPosition;
errorChain(saveStreamTellImpl(stream, &headerPosition));
saveFileWriteHeader(stream, meta->header, SAVE_META_HEADER_SIZE);
saveFileWriteVersion(stream, &meta->version);
saveFileWriteFloat(stream, &meta->deadzone);
saveFileWriteUInt8(stream, &meta->language);
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();
}
-511
View File
@@ -1,511 +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 "saveslot.h"
#include "savemeta.h"
#include "save/saveplatform.h"
#include "time/timeepoch.h"
typedef struct {
bool_t found;
uint32_t checksum;
uint32_t expectedChecksum;
saveplatformstream_t platform;
} savestream_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 saveStreamReadBytesRawImpl(
savestream_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 saveStreamWriteBytesRawImpl(
savestream_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 saveStreamReadBytesImpl(
savestream_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 saveStreamWriteBytesImpl(
savestream_t *stream, const void *buf, const size_t len
);
/**
* Gets the current read/write position within the stream. Used to capture
* a section's start position before writing its header, so its checksum
* can be backfilled at the right offset once the section's body is known -
* required now that a single stream can hold multiple self-contained
* sections back-to-back (meta + N save slots), not just one.
*
* @param stream Active stream.
* @param out Receives the current position.
* @return An error if the platform can't report a position.
*/
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out);
/**
* Finalizes a write stream: computes the final CRC32, seeks back to the
* checksum field just after this section's header, and writes it in
* little-endian order.
*
* @param stream Active write stream.
* @param headerPosition Byte offset where this section's header started
* (see saveStreamTellImpl), captured before writing the header.
* @param headerSize Byte length of this section's magic header.
* @return An error if the seek or write fails.
*/
errorret_t saveStreamFinalizeWriteImpl(
savestream_t *stream, const size_t headerPosition, const size_t headerSize
);
/**
* Verifies that the CRC32 accumulated during loading matches the value
* stored in this section's header.
*
* @param stream Active read stream (loading must be complete).
* @param sectionLabel Human-readable label used in the error message on
* mismatch (e.g. "save meta", "save slot").
* @return An error if the checksum does not match.
*/
errorret_t saveStreamVerifyChecksumImpl(
savestream_t *stream, const char_t *sectionLabel
);
/**
* Reads and validates a section's magic header, then reads its stored
* CRC32 and resets the running accumulator.
*
* @param stream Active read stream.
* @param header Buffer of headerSize bytes to receive the header.
* @param expectedHeader The magic bytes this section must match.
* @param headerSize Byte length of the magic header.
* @return An error if the header is missing or invalid.
*/
errorret_t saveStreamReadHeaderImpl(
savestream_t *stream, char_t *header, const char_t *expectedHeader,
const size_t headerSize
);
/**
* Writes a section's magic header and a zero CRC32 placeholder, then
* resets the running accumulator.
*
* @param stream Active write stream.
* @param header Buffer of headerSize bytes to write.
* @param headerSize Byte length of the magic header.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteHeaderImpl(
savestream_t *stream, const char_t *header, const size_t headerSize
);
/**
* 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 saveStreamReadVersionImpl(savestream_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 saveStreamWriteVersionImpl(
savestream_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 saveStreamReadBoolImpl(savestream_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 saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input);
/**
* Reads a signed 8-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out);
/**
* Writes a signed 8-bit integer to the stream.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input);
/**
* Reads an unsigned 8-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out);
/**
* Writes an unsigned 8-bit integer to the stream.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input);
/**
* Reads a little-endian signed 16-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out);
/**
* Writes a signed 16-bit integer 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 saveStreamWriteInt16Impl(
savestream_t *stream, const int16_t *input
);
/**
* Reads a little-endian unsigned 16-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out);
/**
* Writes an unsigned 16-bit integer 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 saveStreamWriteUInt16Impl(
savestream_t *stream, const uint16_t *input
);
/**
* Reads a little-endian signed 32-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out);
/**
* Writes a signed 32-bit integer 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 saveStreamWriteInt32Impl(
savestream_t *stream, const int32_t *input
);
/**
* Reads a little-endian unsigned 32-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out);
/**
* Writes an unsigned 32-bit integer 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 saveStreamWriteUInt32Impl(
savestream_t *stream, const uint32_t *input
);
/**
* Reads a little-endian signed 64-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out);
/**
* Writes a signed 64-bit integer 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 saveStreamWriteInt64Impl(
savestream_t *stream, const int64_t *input
);
/**
* Reads a little-endian unsigned 64-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out);
/**
* Writes an unsigned 64-bit integer 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 saveStreamWriteUInt64Impl(
savestream_t *stream, const uint64_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 saveStreamReadFloatImpl(savestream_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 saveStreamWriteFloatImpl(
savestream_t *stream, const float_t *input
);
/**
* Reads a null-terminated string from the stream up to maxLen bytes
* (including the terminator). Always null-terminates the output buffer.
*
* @param stream Active read stream.
* @param out Destination buffer of at least maxLen bytes.
* @param maxLen Maximum bytes to read, including the null terminator.
* @return An error if the read fails.
*/
errorret_t saveStreamReadStringImpl(
savestream_t *stream, char_t *out, const size_t maxLen
);
/**
* Writes a null-terminated string to the stream, truncating to maxLen-1
* characters and always appending a null terminator.
*
* @param stream Active write stream.
* @param input Source string.
* @param maxLen Maximum bytes to write, including the null terminator.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteStringImpl(
savestream_t *stream, const char_t *input, const size_t maxLen
);
/**
* Reads a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
* (time, timeZone, offsetTime).
*
* @param stream Active read stream.
* @param out Receives the epoch value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadDateImpl(
savestream_t *stream, dusktimeepoch_t *out
);
/**
* Writes a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
* (time, timeZone, offsetTime).
*
* @param stream Active write stream.
* @param input Epoch value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteDateImpl(
savestream_t *stream, const dusktimeepoch_t *input
);
/**
* Reads a self-contained save meta section (header, version, fields,
* checksum verification) from the stream.
*
* @param stream Active read stream, positioned at the section's start.
* @param meta Meta struct to populate.
* @return An error code if loading fails.
*/
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta);
/**
* Writes a self-contained save meta section (header, version, fields,
* checksum) to the stream.
*
* @param stream Active write stream, positioned at the section's start.
* @param meta Meta struct to serialize.
* @return An error code if writing fails.
*/
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta);
/**
* Reads a self-contained save slot section (header, version, fields,
* checksum verification) from the stream.
*
* @param stream Active read stream, positioned at the section's start.
* @param slot Slot struct to populate.
* @return An error code if loading fails.
*/
errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot);
/**
* Writes a self-contained save slot section (header, version, fields,
* checksum) to the stream.
*
* @param stream Active write stream, positioned at the section's start.
* @param slot Slot struct to serialize.
* @return An error code if writing fails.
*/
errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot);
#define saveFileReadHeader(stream, header, expected, size) \
errorChain(saveStreamReadHeaderImpl(stream, header, expected, size))
#define saveFileWriteHeader(stream, header, size) \
errorChain(saveStreamWriteHeaderImpl(stream, header, size))
#define saveFileReadVersion(stream, out) \
errorChain(saveStreamReadVersionImpl(stream, out))
#define saveFileWriteVersion(stream, input) \
errorChain(saveStreamWriteVersionImpl(stream, input))
#define saveFileReadBool(stream, out) \
errorChain(saveStreamReadBoolImpl(stream, out))
#define saveFileWriteBool(stream, input) \
errorChain(saveStreamWriteBoolImpl(stream, input))
#define saveFileReadInt8(stream, out) \
errorChain(saveStreamReadInt8Impl(stream, out))
#define saveFileWriteInt8(stream, input) \
errorChain(saveStreamWriteInt8Impl(stream, input))
#define saveFileReadUInt8(stream, out) \
errorChain(saveStreamReadUInt8Impl(stream, out))
#define saveFileWriteUInt8(stream, input) \
errorChain(saveStreamWriteUInt8Impl(stream, input))
#define saveFileReadInt16(stream, out) \
errorChain(saveStreamReadInt16Impl(stream, out))
#define saveFileWriteInt16(stream, input) \
errorChain(saveStreamWriteInt16Impl(stream, input))
#define saveFileReadUInt16(stream, out) \
errorChain(saveStreamReadUInt16Impl(stream, out))
#define saveFileWriteUInt16(stream, input) \
errorChain(saveStreamWriteUInt16Impl(stream, input))
#define saveFileReadInt32(stream, out) \
errorChain(saveStreamReadInt32Impl(stream, out))
#define saveFileWriteInt32(stream, input) \
errorChain(saveStreamWriteInt32Impl(stream, input))
#define saveFileReadUInt32(stream, out) \
errorChain(saveStreamReadUInt32Impl(stream, out))
#define saveFileWriteUInt32(stream, input) \
errorChain(saveStreamWriteUInt32Impl(stream, input))
#define saveFileReadInt64(stream, out) \
errorChain(saveStreamReadInt64Impl(stream, out))
#define saveFileWriteInt64(stream, input) \
errorChain(saveStreamWriteInt64Impl(stream, input))
#define saveFileReadUInt64(stream, out) \
errorChain(saveStreamReadUInt64Impl(stream, out))
#define saveFileWriteUInt64(stream, input) \
errorChain(saveStreamWriteUInt64Impl(stream, input))
#define saveFileReadFloat(stream, out) \
errorChain(saveStreamReadFloatImpl(stream, out))
#define saveFileWriteFloat(stream, input) \
errorChain(saveStreamWriteFloatImpl(stream, input))
#define saveFileReadString(stream, out, maxLen) \
errorChain(saveStreamReadStringImpl(stream, out, maxLen))
#define saveFileWriteString(stream, input, maxLen) \
errorChain(saveStreamWriteStringImpl(stream, input, maxLen))
#define saveFileReadDate(stream, out) \
errorChain(saveStreamReadDateImpl(stream, out))
#define saveFileWriteDate(stream, input) \
errorChain(saveStreamWriteDateImpl(stream, input))
-58
View File
@@ -9,71 +9,13 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "error/error.h"
#include "save/save.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
static void sceneInitialCheckSave(void);
static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
sceneSet(SCENE_TYPE_MAIN_MENU);
}
static void sceneInitialCreateSaveResult(const bool_t create, void *user) {
if(!create) {
sceneSet(SCENE_TYPE_MAIN_MENU);
return;
}
saveWriteSlot(SAVE_ACTIVE_SLOT, sceneInitialCreateSaveWriteComplete, NULL);
}
static void sceneInitialNoCardResult(const bool_t retry, void *user) {
if(retry) {
sceneInitialCheckSave();
return;
}
// The player explicitly acknowledged there's no save device and chose
// to proceed anyway - stick with that for the rest of the session (see
// saveMarkTemporary()'s doc comment for why this can't be un-set later).
saveMarkTemporary();
sceneSet(SCENE_TYPE_MAIN_MENU);
}
static void sceneInitialLoadComplete(errorret_t result, void *user) {
if(errorIsNotOk(result) || !saveIsAvailable()) {
errorCatch(result);
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
return;
}
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
sceneSet(SCENE_TYPE_MAIN_MENU);
return;
}
uiInitialCreateSaveOpen(sceneInitialCreateSaveResult, NULL);
}
static void sceneInitialCheckSave(void) {
if(!saveIsAvailable()) {
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
return;
}
if(saveIsBusy()) return;
saveLoadSlot(SAVE_ACTIVE_SLOT, sceneInitialLoadComplete, NULL);
}
errorret_t sceneInitialInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
sceneInitialCheckSave();
errorOk();
}
+8
View File
@@ -16,6 +16,10 @@
#error "systemInitPlatform is not defined"
#endif
#ifndef systemGetLocalePlatform
#error "systemGetLocalePlatform is not defined"
#endif
errorret_t systemInit() {
return systemInitPlatform();
}
@@ -24,6 +28,10 @@ systemdialogtype_t systemGetActiveDialogType() {
return systemGetActiveDialogTypePlatform();
}
const localeinfo_t * systemGetLocale(void) {
return systemGetLocalePlatform();
}
systemplatform_t systemGetPlatform(void) {
#if defined(DUSK_KNULLI)
return SYSTEM_PLATFORM_KNULLI;
+9 -1
View File
@@ -8,6 +8,7 @@
#pragma once
#include "error/error.h"
#include "system/systemplatformlist.h"
#include "locale/localeinfo.h"
#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value,
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
@@ -47,4 +48,11 @@ systemdialogtype_t systemGetActiveDialogType();
*
* @return The current platform.
*/
systemplatform_t systemGetPlatform(void);
systemplatform_t systemGetPlatform(void);
/**
* Returns the current locale of the system.
*
* @return The current locale.
*/
const localeinfo_t * systemGetLocale(void);
+1
View File
@@ -16,5 +16,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
ui.c
uielement.c
uielementlist.c
# uitextbox.c
)
-1
View File
@@ -11,7 +11,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(game)
add_subdirectory(mainmenu)
add_subdirectory(settings)
add_subdirectory(battle)
add_subdirectory(backpack)
add_subdirectory(initial)
+7 -86
View File
@@ -8,7 +8,6 @@
#include "uigamemenu.h"
#include "ui/frame/uiframe.h"
#include "ui/frame/uiconfirm.h"
#include "ui/frame/settings/uisettings.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/memory.h"
@@ -17,85 +16,11 @@
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "save/save.h"
#include "error/error.h"
#include "util/string.h"
#define UI_GAME_MENU_INDEX_CHARACTERS 0
#define UI_GAME_MENU_INDEX_ITEMS 1
#define UI_GAME_MENU_INDEX_SETTINGS 2
#define UI_GAME_MENU_INDEX_SAVE 3
static void uiGameMenuSaveWriteComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) {
// Generously sized - stringFormat asserts (crashes) rather than
// truncating if the message doesn't fit, so this must comfortably fit
// the longest platform save-error message plus this prefix.
char_t msg[256];
stringFormat(
msg, sizeof(msg), UI_GAME_MENU.saveFailedFormat, result.state->message
);
errorCatch(result);
uiTextboxMainSetText(msg);
return;
}
uiTextboxMainSetText(UI_GAME_MENU.saveSuccessText);
}
static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) {
if(!confirmed) {
uiTextboxMainSetText(UI_GAME_MENU.saveCancelledText);
return;
}
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
}
// Determines whether there's actually save data to overwrite (not just
// whether the medium is present) by attempting a real load first - this is
// what lets a fresh memory card/stick, with no prior save on it yet, be
// told apart from one that already has our data on it. Cheap either way
// (a single sector/file read), and correct on every platform without any
// platform-specific UI code - saveSlotExists() already reflects each
// platform's own notion of "found something."
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) {
char_t msg[256];
stringFormat(
msg, sizeof(msg), UI_GAME_MENU.saveCheckFailedFormat,
result.state->message
);
errorCatch(result);
uiTextboxMainSetText(msg);
return;
}
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
saveWriteSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
return;
}
uiConfirmOpen(
UI_GAME_MENU.saveCreateConfirmText,
uiGameMenuSaveCreateConfirmed,
NULL
);
}
static void uiGameMenuSave(void) {
if(saveIsTemporary()) {
uiTextboxMainSetText(UI_GAME_MENU.saveTemporaryText);
return;
}
if(!saveIsAvailable()) {
uiTextboxMainSetText(UI_GAME_MENU.saveUnavailableText);
return;
}
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
saveLoadSlot(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL);
}
uigamemenu_t UI_GAME_MENU;
@@ -104,9 +29,13 @@ void uiGameMenuSelected(
const uint8_t index,
const uimenuitem_t *item
) {
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
switch(index) {
case UI_GAME_MENU_INDEX_ITEMS:
uiBackpackOpen();
break;
default:
assertUnreachable("Unhandled game menu index");
}
}
errorret_t uiGameMenuInit(void) {
@@ -133,13 +62,6 @@ errorret_t uiGameMenuInit(void) {
UI_GAME_MENU.settingsLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.save",
0,
UI_GAME_MENU.saveLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.save_success",
@@ -195,7 +117,6 @@ errorret_t uiGameMenuInit(void) {
);
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
MENU_BUTTON(UI_GAME_MENU.saveLabel);
MENU_END(UI_GAME_MENU.items, 1);
-4
View File
@@ -10,7 +10,6 @@
#include "scene/scene.h"
#include "engine/engine.h"
#include "rpg/battle/testbattle/testbattle.h"
#include "save/save.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
@@ -34,9 +33,6 @@ void uiMainMenuSelected(
switch(index) {
case UI_MAIN_MENU_INDEX_NEW_GAME:
// Resets the active slot to SAVE_DEFAULT in memory -- no disk I/O,
// unlike Load Game (which will read a real slot via saveLoadSlot()).
saveLoadDefault(SAVE_ACTIVE_SLOT);
testBattleStart();
break;
-13
View File
@@ -1,13 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uisettings.c
uisettingsgeneral.c
uisettingsinput.c
uisettingsdisplay.c
uisettingsaudio.c
)
-250
View File
@@ -1,250 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettings.h"
#include "uisettingsgeneral.h"
#include "uisettingsinput.h"
#include "uisettingsdisplay.h"
#include "uisettingsaudio.h"
#include "ui/frame/uiframe.h"
#include "ui/frame/uiconfirm.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
uisettings_t UI_SETTINGS;
uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = {
{
.localeId = "ui.settings.tabs.general",
.init = uiSettingsGeneralInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsGeneralLoad,
.apply = uiSettingsGeneralApply,
.hasChanges = uiSettingsGeneralHasChanges
},
{
.localeId = "ui.settings.tabs.input",
.init = uiSettingsInputInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsInputLoad,
.apply = uiSettingsInputApply,
.hasChanges = uiSettingsInputHasChanges
},
{
.localeId = "ui.settings.tabs.display",
.init = uiSettingsDisplayInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsDisplayLoad,
.apply = uiSettingsDisplayApply,
.hasChanges = uiSettingsDisplayHasChanges
},
{
.localeId = "ui.settings.tabs.audio",
.init = uiSettingsAudioInit,
.menu = (uimenu_t *)&UI_SETTINGS.data,
.load = uiSettingsAudioLoad,
.apply = uiSettingsAudioApply,
.hasChanges = uiSettingsAudioHasChanges
}
};
static uisettingstabdef_t UI_SETTINGS_TOP_DEF = {
.menu = &UI_SETTINGS.tabsMenu,
.load = uiSettingsLoad,
.hasChanges = uiSettingsHasChanges
};
static uisettingstabdef_t *UI_SETTINGS_PENDING_DISCARD = NULL;
static uisettingstabdef_t *UI_SETTINGS_ACTIVE_TAB = NULL;
errorret_t uiSettingsActivateTab(uisettingstabdef_t *def) {
if(def == UI_SETTINGS_ACTIVE_TAB) errorOk();
errorChain(def->init(&UI_SETTINGS.data));
def->menu->user = def;
def->load();
UI_SETTINGS_ACTIVE_TAB = def;
errorOk();
}
void uiSettingsTabChanged(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index >= UI_SETTINGS_TAB_COUNT) return;
errorCatch(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[index]));
}
void uiSettingsTabSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index >= UI_SETTINGS_TAB_COUNT) return;
uisettingstabdef_t *def = &UI_SETTINGS_TAB_DEFS[index];
errorCatch(uiSettingsActivateTab(def));
uiMenuOpen(def->menu);
}
errorret_t uiSettingsInit(void) {
memoryZero(&UI_SETTINGS, sizeof(uisettings_t));
UI_SETTINGS_ACTIVE_TAB = NULL;
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
UI_SETTINGS_TAB_DEFS[i].localeId,
0,
UI_SETTINGS.tabLabels[i],
UI_SETTINGS_TAB_LABEL_MAX
));
}
MENU_BEGIN(
&UI_SETTINGS.tabsMenu, UI_SETTINGS.tabs, uiSettingsTabSelected,
uiSettingsDiscardMenuClosed, uiSettingsTabChanged
);
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
MENU_TAB(UI_SETTINGS.tabLabels[i]);
}
MENU_END(UI_SETTINGS.tabs, menuIndex);
UI_SETTINGS.tabsMenu.user = &UI_SETTINGS_TOP_DEF;
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.apply",
0,
UI_SETTINGS.applyLabel,
UI_SETTINGS_APPLY_LABEL_MAX
));
errorChain(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[0]));
errorOk();
}
errorret_t uiSettingsDraw(void) {
if(!uiMenuIsActive(&UI_SETTINGS.tabsMenu)) errorOk();
const float_t width = SCREEN.width;
const float_t height = SCREEN.height;
const float_t x = (float_t)SCREEN.scanX +
((float_t)SCREEN.scanWidth - width) * 0.5f;
const float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
errorChain(uiMenuDraw(
&UI_SETTINGS.tabsMenu,
contentX,
contentY,
contentWidth,
contentHeight
));
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t pageY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
const float_t pageHeight = contentHeight - tabsRowHeight - UI_FRAME_PADDING_Y;
if(UI_SETTINGS_ACTIVE_TAB != NULL) {
errorChain(uiMenuDraw(
UI_SETTINGS_ACTIVE_TAB->menu, contentX, pageY, contentWidth, pageHeight
));
}
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiSettingsUpdate(void) {
if(!UI_SETTINGS_PENDING_DISCARD) errorOk();
uisettingstabdef_t *def = UI_SETTINGS_PENDING_DISCARD;
UI_SETTINGS_PENDING_DISCARD = NULL;
return uiSettingsDiscardUpdate(def);
}
bool_t uiSettingsIsOpen(void) {
return uiMenuIsActive(&UI_SETTINGS.tabsMenu);
}
void uiSettingsOpen() {
uiSettingsLoad();
uiMenuOpen(&UI_SETTINGS.tabsMenu);
}
void uiSettingsClose() {
uiMenuClose(&UI_SETTINGS.tabsMenu);
}
errorret_t uiSettingsDispose(void) {
errorOk();
}
void uiSettingsLoad(void) {
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->load();
}
void uiSettingsApply(void) {
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->apply();
}
bool_t uiSettingsHasChanges(void) {
return UI_SETTINGS_ACTIVE_TAB != NULL && UI_SETTINGS_ACTIVE_TAB->hasChanges();
}
void uiSettingsDiscardMenuClosed(const uimenu_t *menu) {
uisettingstabdef_t *def = (uisettingstabdef_t *)menu->user;
if(def->hasChanges()) UI_SETTINGS_PENDING_DISCARD = def;
}
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def) {
#define UI_SETTINGS_DISCARD_TEXT_MAX 128
char_t text[UI_SETTINGS_DISCARD_TEXT_MAX];
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.confirm.discard_changes",
0,
text,
UI_SETTINGS_DISCARD_TEXT_MAX
));
#undef UI_SETTINGS_DISCARD_TEXT_MAX
uiConfirmOpen(text, uiSettingsDiscardConfirmResult, def);
errorOk();
}
void uiSettingsDiscardConfirmResult(const bool_t result, void *user) {
uisettingstabdef_t *def = (uisettingstabdef_t *)user;
if(!result) {
uiMenuOpen(def->menu);
return;
}
def->load();
}
-133
View File
@@ -1,133 +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 "ui/widget/uimenu.h"
#include "uisettingsdata.h"
#define UI_SETTINGS_TAB_COUNT 4
#define UI_SETTINGS_TAB_LABEL_MAX 32
#define UI_SETTINGS_APPLY_LABEL_MAX 32
typedef struct {
const char_t *localeId;
errorret_t (*init)(uisettingsdata_t *data);
uimenu_t *menu;
void (*load)(void);
void (*apply)(void);
bool_t (*hasChanges)(void);
} uisettingstabdef_t;
typedef struct {
uimenu_t tabsMenu;
uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT];
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
uisettingsdata_t data;
} uisettings_t;
extern uisettings_t UI_SETTINGS;
extern uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT];
/**
* Menu closed callback that flags a pending discard-changes
* confirmation if the menu's settings page/panel reports unsaved
* changes. Pass this directly as a settings page menu's closed
* callback. The pending def is picked up by uiSettingsUpdate on the
* next tick.
*
* @param menu The menu that was just closed.
*/
void uiSettingsDiscardMenuClosed(const uimenu_t *menu);
/**
* Shows the discard-changes confirmation dialog for def. Called once
* by uiSettingsUpdate when uiSettingsDiscardMenuClosed has flagged a
* pending def.
*
* @param def The tab/panel def to show the confirmation for.
* @return Any error that occurs.
*/
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def);
/**
* Callback passed to uiConfirmOpen by uiSettingsDiscardUpdate. Reopens
* the menu if the user chose to keep editing, otherwise reloads the
* widgets back to the actual engine values.
*
* @param result True if the user confirmed discarding changes, false
* to keep editing.
* @param user The uisettingstabdef_t* this confirmation belongs to.
*/
void uiSettingsDiscardConfirmResult(const bool_t result, void *user);
/**
* Initializes the settings panel and all of its category pages.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsInit(void);
/**
* Draws the settings panel. No-op when not visible.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsDraw(void);
/**
* Updates the settings panel. Shows a discard-changes confirmation if
* the panel was just closed with unsaved changes.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsUpdate(void);
/**
* Returns true when the settings panel is currently open.
*
* @returns True if open.
*/
bool_t uiSettingsIsOpen(void);
/**
* Opens the settings panel. No-op when already open.
*/
void uiSettingsOpen();
/**
* Closes the settings panel. No-op when already closed.
*/
void uiSettingsClose();
/**
* Disposes of the settings panel.
*
* @return Any error that occurs.
*/
errorret_t uiSettingsDispose(void);
/**
* Loads every category page's widgets from the current engine settings.
* Called automatically by uiSettingsOpen.
*/
void uiSettingsLoad(void);
/**
* Applies every category page's widget values to the engine settings.
* Called automatically when a page's own Apply button is pressed.
*/
void uiSettingsApply(void);
/**
* Returns whether any category page has a widget value that differs
* from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsHasChanges(void);
@@ -1,57 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsaudio.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
void uiSettingsAudioSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index != UI_SETTINGS_AUDIO_INDEX_APPLY) return;
uiSettingsAudioApply();
}
errorret_t uiSettingsAudioInit(uisettingsdata_t *data) {
uisettingsaudio_t *audio = &data->audio;
memoryZero(audio, sizeof(uisettingsaudio_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.audio.placeholder",
0,
audio->placeholderLabel,
UI_SETTINGS_AUDIO_PLACEHOLDER_MAX
));
MENU_BEGIN(
&audio->menu, audio->items,
uiSettingsAudioSelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_LABEL(audio->placeholderLabel);
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(audio->items, 1);
errorOk();
}
void uiSettingsAudioLoad(void) {
}
void uiSettingsAudioApply(void) {
uiMenuClose(&UI_SETTINGS.data.audio.menu);
}
bool_t uiSettingsAudioHasChanges(void) {
return false;
}
@@ -1,36 +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 "uisettingsdata.h"
/**
* Initializes the audio settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsAudioInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine audio settings.
*/
void uiSettingsAudioLoad(void);
/**
* Applies the page's widget values to the engine audio settings.
*/
void uiSettingsAudioApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsAudioHasChanges(void);
@@ -1,73 +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 "ui/widget/uimenu.h"
#include "locale/localemanager.h"
#define UI_SETTINGS_GENERAL_ITEM_COUNT 3
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1
#define UI_SETTINGS_GENERAL_INDEX_APPLY 2
#define UI_SETTINGS_GENERAL_LABEL_MAX 32
#define UI_SETTINGS_GENERAL_INFO_MAX 96
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT];
const char_t *localeNames[LOCALE_LIST_COUNT];
char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX];
char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX];
} uisettingsgeneral_t;
#define UI_SETTINGS_INPUT_ITEM_COUNT 2
#define UI_SETTINGS_INPUT_LABEL_MAX 32
#define UI_SETTINGS_INPUT_PLACEHOLDER_MAX 64
#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0
#define UI_SETTINGS_INPUT_INDEX_APPLY 1
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT];
char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX];
char_t placeholderLabel[UI_SETTINGS_INPUT_PLACEHOLDER_MAX];
} uisettingsinput_t;
#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2
#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1
#define UI_SETTINGS_DISPLAY_PLACEHOLDER_MAX 64
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT];
char_t placeholderLabel[UI_SETTINGS_DISPLAY_PLACEHOLDER_MAX];
} uisettingsdisplay_t;
#define UI_SETTINGS_AUDIO_ITEM_COUNT 2
#define UI_SETTINGS_AUDIO_INDEX_APPLY 1
#define UI_SETTINGS_AUDIO_PLACEHOLDER_MAX 64
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT];
char_t placeholderLabel[UI_SETTINGS_AUDIO_PLACEHOLDER_MAX];
} uisettingsaudio_t;
/**
* Shared, overlapping storage for every settings tab's page data. Only
* one member is ever live at a time -- the active tab's Init writes
* into it, overwriting whatever the previously active tab left there.
* This trades simultaneous access to every tab for a memory footprint
* of only the single largest page.
*/
typedef union {
uisettingsgeneral_t general;
uisettingsinput_t input;
uisettingsdisplay_t display;
uisettingsaudio_t audio;
} uisettingsdata_t;
@@ -1,57 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsdisplay.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
void uiSettingsDisplaySelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index != UI_SETTINGS_DISPLAY_INDEX_APPLY) return;
uiSettingsDisplayApply();
}
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data) {
uisettingsdisplay_t *display = &data->display;
memoryZero(display, sizeof(uisettingsdisplay_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.display.placeholder",
0,
display->placeholderLabel,
UI_SETTINGS_DISPLAY_PLACEHOLDER_MAX
));
MENU_BEGIN(
&display->menu, display->items,
uiSettingsDisplaySelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_LABEL(display->placeholderLabel);
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(display->items, 1);
errorOk();
}
void uiSettingsDisplayLoad(void) {
}
void uiSettingsDisplayApply(void) {
uiMenuClose(&UI_SETTINGS.data.display.menu);
}
bool_t uiSettingsDisplayHasChanges(void) {
return false;
}
@@ -1,36 +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 "uisettingsdata.h"
/**
* Initializes the display settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine display settings.
*/
void uiSettingsDisplayLoad(void);
/**
* Applies the page's widget values to the engine display settings.
*/
void uiSettingsDisplayApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsDisplayHasChanges(void);
@@ -1,108 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsgeneral.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "locale/localemanager.h"
#include "locale/localeinfo.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "save/save.h"
#include "error/error.h"
static void uiSettingsGeneralSaveComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
}
void uiSettingsGeneralSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
switch(index) {
case UI_SETTINGS_GENERAL_INDEX_APPLY:
uiSettingsGeneralApply();
break;
default:
break;
}
}
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
uisettingsgeneral_t *general = &data->general;
memoryZero(general, sizeof(uisettingsgeneral_t));
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
general->localeNames[i] = LOCALE_LIST[i]->name;
}
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.general.language",
0,
general->languageLabel,
UI_SETTINGS_GENERAL_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.general.language_detail",
0,
general->labelInfo,
UI_SETTINGS_GENERAL_INFO_MAX
));
MENU_BEGIN(
&general->menu, general->items,
uiSettingsGeneralSelected, uiSettingsDiscardMenuClosed, NULL
);
MENU_DROPDOWN(
general->languageLabel, general->localeNames,
LOCALE_LIST_COUNT, 0
);
MENU_LABEL(general->labelInfo);
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(general->items, 1);
errorOk();
}
void uiSettingsGeneralLoad(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = localeManagerGetIndex(LOCALE.locale);
uiDropdownSetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index
);
}
void uiSettingsGeneralApply(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = uiDropdownGetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
);
errorCatch(localeManagerSetLocale(LOCALE_LIST[index]));
saveGetMeta()->language = index;
saveWriteMeta(uiSettingsGeneralSaveComplete, NULL);
uiMenuClose(&general->menu);
}
bool_t uiSettingsGeneralHasChanges(void) {
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
uint8_t index = uiDropdownGetSelectedIndex(
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
);
return localeManagerGetIndex(LOCALE.locale) != index;
}
@@ -1,36 +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 "uisettingsdata.h"
/**
* Initializes the general settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine general settings.
*/
void uiSettingsGeneralLoad(void);
/**
* Applies the page's widget values to the engine general settings.
*/
void uiSettingsGeneralApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsGeneralHasChanges(void);
@@ -1,92 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisettingsinput.h"
#include "uisettings.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "save/save.h"
static void uiSettingsInputSaveComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
}
void uiSettingsInputSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index != UI_SETTINGS_INPUT_INDEX_APPLY) return;
uiSettingsInputApply();
}
errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
uisettingsinput_t *input = &data->input;
memoryZero(input, sizeof(uisettingsinput_t));
MENU_BEGIN(
&input->menu, input->items,
uiSettingsInputSelected, uiSettingsDiscardMenuClosed, NULL
);
#ifdef DUSK_INPUT_GAMEPAD
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.input.deadzone",
0,
input->deadzoneLabel,
UI_SETTINGS_INPUT_LABEL_MAX
));
MENU_SLIDER_FLOAT(
input->deadzoneLabel, SAVE_META_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
);
#else
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.settings.input.placeholder",
0,
input->placeholderLabel,
UI_SETTINGS_INPUT_PLACEHOLDER_MAX
));
MENU_LABEL(input->placeholderLabel);
#endif
MENU_BUTTON(UI_SETTINGS.applyLabel);
MENU_END(input->items, 1);
errorOk();
}
void uiSettingsInputLoad(void) {
#ifdef DUSK_INPUT_GAMEPAD
uiSliderSetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
saveGetMeta()->deadzone
);
#endif
}
void uiSettingsInputApply(void) {
#ifdef DUSK_INPUT_GAMEPAD
saveGetMeta()->deadzone = uiSliderGetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
);
saveWriteMeta(uiSettingsInputSaveComplete, NULL);
#endif
uiMenuClose(&UI_SETTINGS.data.input.menu);
}
bool_t uiSettingsInputHasChanges(void) {
#ifdef DUSK_INPUT_GAMEPAD
if(uiSliderGetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
) != saveGetMeta()->deadzone) return true;
#endif
return false;
}
@@ -1,36 +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 "uisettingsdata.h"
/**
* Initializes the input settings page into the shared settings data
* union.
*
* @param data The shared settings data union to initialize into.
* @return Any error that occurs.
*/
errorret_t uiSettingsInputInit(uisettingsdata_t *data);
/**
* Loads the page's widgets from the current engine input settings.
*/
void uiSettingsInputLoad(void);
/**
* Applies the page's widget values to the engine input settings.
*/
void uiSettingsInputApply(void);
/**
* Returns whether any widget's value differs from what was loaded.
*
* @returns True if there are unsaved changes.
*/
bool_t uiSettingsInputHasChanges(void);
-1
View File
@@ -8,5 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
uicrop.c
uifullbox.c
uiloading.c
uiautosave.c
)
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiautosave.h"
#include "save/autosave.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "display/spritebatch/spritebatch.h"
#include "util/memory.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
uiautosave_t UI_AUTO_SAVE;
errorret_t uiAutoSaveInit(void) {
memoryZero(&UI_AUTO_SAVE, sizeof(uiautosave_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.autosave.saving",
0,
UI_AUTO_SAVE.text,
UI_AUTOSAVE_TEXT_MAX
));
errorOk();
}
errorret_t uiAutoSaveDraw(void) {
if(!autoSaveIsSaving()) errorOk();
int32_t textW, textH;
textMeasure(UI_AUTO_SAVE.text, &FONT_DEFAULT, &textW, &textH);
float_t x = (float_t)SCREEN.scanX + UI_AUTOSAVE_MARGIN;
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) -
(float_t)textH - UI_AUTOSAVE_MARGIN;
errorChain(textDraw(x, y, UI_AUTO_SAVE.text, COLOR_WHITE, &FONT_DEFAULT));
return spriteBatchFlush();
}
-34
View File
@@ -1,34 +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"
#define UI_AUTOSAVE_MARGIN 8.0f
#define UI_AUTOSAVE_TEXT_MAX 32
typedef struct {
char_t text[UI_AUTOSAVE_TEXT_MAX];
} uiautosave_t;
extern uiautosave_t UI_AUTO_SAVE;
/**
* Loads the "Saving" indicator text from the active locale.
*
* @return Any error that occurs.
*/
errorret_t uiAutoSaveInit(void);
/**
* Draws a "Saving" indicator in the bottom-left corner of the screen
* while an autosave write is in flight (see save/autosave.h). No-op
* otherwise.
*
* @return Any error that occurs.
*/
errorret_t uiAutoSaveDraw(void);
+1 -1
View File
@@ -27,7 +27,7 @@ void uiTextboxMiniInit(uitextboxmini_t *mini) {
void uiTextboxMiniShow(
uitextboxmini_t *mini,
const char_t *text,
vec3 position,
const vec3 position,
const float_t duration,
uitextboxminiclosedcallback_t closed,
void *user
+1 -1
View File
@@ -72,7 +72,7 @@ void uiTextboxMiniInit(uitextboxmini_t *mini);
void uiTextboxMiniShow(
uitextboxmini_t *mini,
const char_t *text,
vec3 position,
const vec3 position,
const float_t duration,
uitextboxminiclosedcallback_t closed,
void *user
+10
View File
@@ -11,6 +11,7 @@
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "ui/uielement.h"
#include "ui/uielementlist.h"
#include "ui/focus/uifocus.h"
#include "log/log.h"
@@ -63,4 +64,13 @@ errorret_t uiDispose(void) {
element++;
}
errorOk();
}
errorret_t uiUpdateTranslations(void) {
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementUpdateTranslations(element));
element++;
}
errorOk();
}
+9 -1
View File
@@ -38,4 +38,12 @@ errorret_t uiRender(void);
*
* @return Any error that occurs.
*/
errorret_t uiDispose(void);
errorret_t uiDispose(void);
/**
* Notifies the UI system that the active locale has changed, so every
* registered element can re-fetch any localized strings it caches.
*
* @return Any error that occurs.
*/
errorret_t uiUpdateTranslations(void);
+10 -142
View File
@@ -8,154 +8,14 @@
#include "uielement.h"
#include "assert/assert.h"
#include "ui/frame/uiframe.h"
#include "ui/debug/uifps.h"
#include "engine/engine.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/overlay/uiautosave.h"
#include "ui/debug/uiplayerpos.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
#include "ui/debug/uiconsole.h"
#include "ui/frame/settings/uisettings.h"
#include "ui/frame/game/uigamemenu.h"
#include "ui/frame/mainmenu/uimainmenu.h"
#include "ui/frame/battle/uibattlemenu.h"
#include "ui/frame/battle/uibattlehud.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
#include "ui/rpg/uiemoji.h"
uielement_t UI_ELEMENTS[] = {
{
.init = uiFrameInit,
.dispose = uiFrameDispose
},
// Fullbox under: above scene, below system UI.
{
.init = uiFullboxUnderInit,
.update = uiFullboxUnderUpdate,
.draw = uiFullboxUnderDraw
},
// in world stuff
{
.init = uiEmojiInit,
.update = uiEmojiUpdate,
.draw = uiEmojiDraw,
.dispose = uiEmojiDispose
},
// Ingame menus
{
.init = uiGameMenuInit,
.draw = uiGameMenuDraw,
.dispose = uiGameMenuDispose
},
{
.init = uiMainMenuInit,
.update = uiMainMenuUpdate,
.draw = uiMainMenuDraw,
.dispose = uiMainMenuDispose
},
{ .init = uiBattleHudInit, .draw = uiBattleHudDraw },
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,
.draw = uiBattleMenuDraw
},
{
.init = uiBackpackInit,
.draw = uiBackpackDraw,
.dispose = uiBackpackDispose
},
{
.init = uiSettingsInit,
.update = uiSettingsUpdate,
.draw = uiSettingsDraw,
.dispose = uiSettingsDispose
},
// Text stuffs
{
.init = uiConfirmInit,
.draw = uiConfirmDraw,
.dispose = uiConfirmDispose
},
{
.init = uiInitialNoCardInit,
.draw = uiInitialNoCardDraw,
.dispose = uiInitialNoCardDispose
},
{
.init = uiInitialCreateSaveInit,
.draw = uiInitialCreateSaveDraw,
.dispose = uiInitialCreateSaveDispose
},
{
.init = uiTextboxMainInit,
.update = uiTextboxMainUpdate,
.draw = uiTextboxMainDraw
},
{
.init = uiTextboxMiniListInit,
.update = uiTextboxMiniListUpdate,
.draw = uiTextboxMiniListDraw
},
{
.init = uiTransitionInit,
.update = uiTransitionUpdate,
.draw = uiTransitionDraw
},
// Fullbox over: above absolutely everything.
{
.init = uiFullboxOverInit,
.update = uiFullboxOverUpdate,
.draw = uiFullboxOverDraw
},
{
.init = uiLoadingInit,
.update = uiLoadingUpdate,
.draw = uiLoadingDraw
},
{
.init = uiCropInit,
.draw = uiCropDraw
},
{ .init = uiAutoSaveInit, .draw = uiAutoSaveDraw },
// Debug items
{ .draw = uiConsoleDraw },
{ .draw = uiFPSDraw },
{ .draw = uiPlayerPosDraw },
{ 0 } // Null terminator
};
bool_t uiElementIsNull(const uielement_t *element) {
return element->init == NULL &&
element->update == NULL &&
element->draw == NULL &&
element->dispose == NULL;
element->dispose == NULL &&
element->updateTranslations == NULL;
}
errorret_t uiElementInit(uielement_t *element) {
@@ -182,4 +42,12 @@ errorret_t uiElementDispose(uielement_t *element) {
if(element->dispose != NULL) errorChain(element->dispose());
errorChain(uiFrameDispose());
errorOk();
}
errorret_t uiElementUpdateTranslations(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->updateTranslations != NULL) {
errorChain(element->updateTranslations());
}
errorOk();
}
+13 -4
View File
@@ -13,13 +13,12 @@ typedef struct {
errorret_t (*update)();
errorret_t (*draw)();
errorret_t (*dispose)();
errorret_t (*updateTranslations)();
} uielement_t;
extern uielement_t UI_ELEMENTS[];
/**
* Returns true when all four callbacks on the element are NULL,
* which marks the end of the UI_ELEMENTS array.
* Returns true when all callbacks on the element are NULL, which marks
* the end of the UI_ELEMENTS array.
*
* @param element The element to test.
* @returns True if the element is the null terminator.
@@ -57,3 +56,13 @@ errorret_t uiElementDraw(const uielement_t *element);
* @return Any error that occurs.
*/
errorret_t uiElementDispose(uielement_t *element);
/**
* Notifies a UI element that the active locale has changed, calling its
* updateTranslations callback if set, so it can re-fetch any localized
* strings it caches.
*
* @param element The element to notify.
* @return Any error that occurs.
*/
errorret_t uiElementUpdateTranslations(uielement_t *element);
+139
View File
@@ -0,0 +1,139 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uielementlist.h"
#include "ui/frame/uiframe.h"
#include "ui/debug/uifps.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/debug/uiplayerpos.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
#include "ui/debug/uiconsole.h"
#include "ui/frame/game/uigamemenu.h"
#include "ui/frame/mainmenu/uimainmenu.h"
#include "ui/frame/battle/uibattlemenu.h"
#include "ui/frame/battle/uibattlehud.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
#include "ui/rpg/uiemoji.h"
uielement_t UI_ELEMENTS[] = {
{
.init = uiFrameInit,
.dispose = uiFrameDispose
},
// Fullbox under: above scene, below system UI.
{
.init = uiFullboxUnderInit,
.update = uiFullboxUnderUpdate,
.draw = uiFullboxUnderDraw
},
// in world stuff
{
.init = uiEmojiInit,
.update = uiEmojiUpdate,
.draw = uiEmojiDraw,
.dispose = uiEmojiDispose
},
// Ingame menus
{
.init = uiGameMenuInit,
.draw = uiGameMenuDraw,
.dispose = uiGameMenuDispose
},
{
.init = uiMainMenuInit,
.update = uiMainMenuUpdate,
.draw = uiMainMenuDraw,
.dispose = uiMainMenuDispose
},
{ .init = uiBattleHudInit, .draw = uiBattleHudDraw },
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,
.draw = uiBattleMenuDraw
},
{
.init = uiBackpackInit,
.draw = uiBackpackDraw,
.dispose = uiBackpackDispose
},
// Text stuffs
{
.init = uiConfirmInit,
.draw = uiConfirmDraw,
.dispose = uiConfirmDispose
},
{
.init = uiInitialNoCardInit,
.draw = uiInitialNoCardDraw,
.dispose = uiInitialNoCardDispose
},
{
.init = uiInitialCreateSaveInit,
.draw = uiInitialCreateSaveDraw,
.dispose = uiInitialCreateSaveDispose
},
{
.init = uiTextboxMainInit,
.update = uiTextboxMainUpdate,
.draw = uiTextboxMainDraw
},
{
.init = uiTextboxMiniListInit,
.update = uiTextboxMiniListUpdate,
.draw = uiTextboxMiniListDraw
},
{
.init = uiTransitionInit,
.update = uiTransitionUpdate,
.draw = uiTransitionDraw
},
// Fullbox over: above absolutely everything.
{
.init = uiFullboxOverInit,
.update = uiFullboxOverUpdate,
.draw = uiFullboxOverDraw
},
{
.init = uiLoadingInit,
.update = uiLoadingUpdate,
.draw = uiLoadingDraw
},
{
.init = uiCropInit,
.draw = uiCropDraw
},
// Debug items
{ .draw = uiConsoleDraw },
{ .draw = uiFPSDraw },
{ .draw = uiPlayerPosDraw },
{ 0 } // Null terminator
};
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "ui/uielement.h"
extern uielement_t UI_ELEMENTS[];
-1
View File
@@ -22,6 +22,5 @@ add_subdirectory(input)
if(DUSK_NETWORK)
add_subdirectory(network)
endif()
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+1 -2
View File
@@ -9,7 +9,6 @@
#include "assert/assert.h"
#include "log/log.h"
#include "util/string.h"
#include "save/save.h"
inputbuttondata_t INPUT_BUTTON_DATA[] = {
#ifdef DUSK_INPUT_GAMEPAD
@@ -188,5 +187,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) {
}
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
return saveGetMeta()->deadzone;
return 0.2f;
}
-11
View File
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savedolphin.c
savestreamdolphin.c
)
-168
View File
@@ -1,168 +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/savestream.h"
#include "util/memory.h"
#include "util/string.h"
errorret_t saveInitDolphin(void) {
SAVE.platform.mounted = false;
// Must run once before any other CARD_* call: sets up card_inited,
// the per-channel control blocks (wait queues, alarms) CARD_Mount reads,
// and initializes the DSP (needed for the card unlock sequence).
// Skipping this leaves those structures unset, so CARD_Mount ends up
// touching hardware state that was never brought up -- e.g. Dolphin's
// "Trying to read 32 bits from an invalid MMIO" error -- rather than
// failing cleanly with a CARD_ERROR_* code.
int32_t result = CARD_Init(SAVE_DOLPHIN_GAME_CODE, NULL);
if(result < 0) {
errorThrow("Failed to initialize memory card subsystem: %s (%d)",
saveCardErrorStringDolphin(result), result
);
}
do {
result = CARD_Mount(
SAVE_DOLPHIN_CHANNEL,
SAVE.platform.cardBuffer,
NULL
);
} while(result == CARD_ERROR_BUSY);
// Special-case the failures a player can actually act on; everything
// else falls through to the generic, fully-enumerated message below.
switch(result) {
case CARD_ERROR_NOCARD:
errorThrow("No memory card inserted in the slot");
case CARD_ERROR_WRONGDEVICE:
errorThrow("Unsupported device inserted in the memory card slot");
case CARD_ERROR_BROKEN:
errorThrow("Memory card is damaged or unformatted");
default:
break;
}
if(result < 0) {
errorThrow("Failed to mount memory card: %s (%d)",
saveCardErrorStringDolphin(result), result
);
}
SAVE.platform.mounted = true;
errorOk();
}
errorret_t saveDisposeDolphin(void) {
if(SAVE.platform.mounted) {
CARD_Unmount(SAVE_DOLPHIN_CHANNEL);
SAVE.platform.mounted = false;
}
errorOk();
}
errorret_t saveCombinedLoadDolphin(void) {
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
errorret_t openRet = saveStreamOpenReadPlatform(&stream);
SAVE.available = errorIsOk(openRet);
errorChain(openRet);
if(!stream.found) errorOk();
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]);
}
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
errorChain(ret);
errorOk();
}
errorret_t saveCombinedWriteDolphin(void) {
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
errorret_t openRet = saveStreamOpenWritePlatform(&stream);
SAVE.available = errorIsOk(openRet);
errorChain(openRet);
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]);
}
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
errorChain(ret);
errorOk();
}
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) {
(void)slot;
(void)out;
return saveCombinedLoadDolphin();
}
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) {
(void)slot;
(void)slotData;
return saveCombinedWriteDolphin();
}
errorret_t saveSlotDeleteDolphin(const uint8_t slot) {
memoryZero(&SAVE.slots[slot], sizeof(saveslot_t));
SAVE.slots[slot].exists = false;
return saveCombinedWriteDolphin();
}
errorret_t saveMetaLoadDolphin(savemeta_t *out) {
(void)out;
return saveCombinedLoadDolphin();
}
errorret_t saveMetaWriteDolphin(savemeta_t *meta) {
(void)meta;
return saveCombinedWriteDolphin();
}
const char_t *saveCardErrorStringDolphin(const int32_t result) {
switch(result) {
case CARD_ERROR_READY: return "card is ready";
case CARD_ERROR_UNLOCKED:
return "card is being unlocked or already unlocked";
case CARD_ERROR_BUSY: return "card is busy";
case CARD_ERROR_WRONGDEVICE: return "wrong device connected in slot";
case CARD_ERROR_NOCARD: return "no memory card in slot";
case CARD_ERROR_NOFILE: return "specified file not found";
case CARD_ERROR_IOERROR: return "internal EXI I/O error";
case CARD_ERROR_BROKEN:
return "directory structure or file entry broken";
case CARD_ERROR_EXIST:
return "file already exists with the specified parameters";
case CARD_ERROR_NOENT:
return "no empty block available to create the file";
case CARD_ERROR_INSSPACE:
return "not enough space to write file to memory card";
case CARD_ERROR_NOPERM:
return "not enough permissions to operate on the file";
case CARD_ERROR_LIMIT: return "card size limit reached";
case CARD_ERROR_NAMETOOLONG: return "filename too long";
case CARD_ERROR_ENCODING: return "font encoding PAL/SJIS mismatch";
case CARD_ERROR_CANCELED: return "card operation canceled";
case CARD_ERROR_FATAL_ERROR: return "fatal error, non-recoverable";
default: return "unknown card error";
}
}
-120
View File
@@ -1,120 +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/saveslot.h"
#include "save/savemeta.h"
#include <gccore.h>
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
#ifndef SAVE_DOLPHIN_GAME_CODE
#define SAVE_DOLPHIN_GAME_CODE "DUSK"
#endif
#ifndef SAVE_DOLPHIN_CHANNEL
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
#endif
/**
* Fixed memory card file name holding meta + every save slot, back to
* back, in one file - GameCube memory cards are small enough that one
* consolidated file (rather than one per slot, plus a separate one for
* meta) meaningfully saves card space, and nothing needs true random
* access into just one section (see saveCombinedLoadDolphin()).
*/
#ifndef SAVE_DOLPHIN_FILE_NAME
#define SAVE_DOLPHIN_FILE_NAME "DUSK_SAVE"
#endif
typedef struct {
card_file cardFile;
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
bool_t mounted;
} savedolphin_t;
/**
* Initializes the save system on GameCube (memory card slot A by default).
*
* @return An error code if initialization fails.
*/
errorret_t saveInitDolphin(void);
/**
* Disposes of the save system on GameCube.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposeDolphin(void);
/**
* 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.
*
* @return An error code if the card is mounted but the read/parse fails.
*/
errorret_t saveCombinedLoadDolphin(void);
/**
* Writes SAVE.meta and every SAVE.slots[i], in order, into the one
* consolidated card file (SAVE_DOLPHIN_FILE_NAME), creating it if needed.
*
* @return An error code if the write fails.
*/
errorret_t saveCombinedWriteDolphin(void);
/**
* Save-slot platform entry point - always (re)reads the whole
* consolidated file (see saveCombinedLoadDolphin()); slot/out are unused
* since every slot is populated in the same pass.
*/
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out);
/**
* Save-slot platform entry point - always (re)writes the whole
* consolidated file (see saveCombinedWriteDolphin()); slot/slotData are
* unused since every slot is written in the same pass.
*/
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData);
/**
* Deletes a single save slot's data (zeroes it in memory) and re-writes
* the consolidated file - the file itself always exists as long as any
* slot or meta does, so "delete" can't remove the file wholesale.
*
* @param slot The save slot index.
* @return An error code if the delete fails.
*/
errorret_t saveSlotDeleteDolphin(const uint8_t slot);
/**
* Meta platform entry point - always (re)reads the whole consolidated
* file (see saveCombinedLoadDolphin()); out is unused since meta is
* populated in the same pass.
*/
errorret_t saveMetaLoadDolphin(savemeta_t *out);
/**
* Meta platform entry point - always (re)writes the whole consolidated
* file (see saveCombinedWriteDolphin()); meta is unused since it's
* written in the same pass.
*/
errorret_t saveMetaWriteDolphin(savemeta_t *meta);
/**
* Describes a libogc CARD_ERROR_* result code (see
* https://libogc.devkitpro.org/group__card__errors.html), for logging
* alongside the raw numeric code.
*
* @param result The result code returned by a CARD_* libogc call.
* @return A human-readable description of the result code, or
* "unknown card error" if result doesn't match a known CARD_ERROR_* code.
*/
const char_t *saveCardErrorStringDolphin(const int32_t result);
-37
View File
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savedolphin.h"
#include "save/savestreamdolphin.h"
typedef savedolphin_t saveplatform_t;
typedef savestreamdolphin_t saveplatformstream_t;
#define saveInitPlatform saveInitDolphin
#define saveDisposePlatform saveDisposeDolphin
#define saveSlotDeletePlatform saveSlotDeleteDolphin
#define saveSlotLoadPlatform saveSlotLoadDolphin
#define saveSlotWritePlatform saveSlotWriteDolphin
#define saveMetaLoadPlatform saveMetaLoadDolphin
#define saveMetaWritePlatform saveMetaWriteDolphin
#define saveStreamOpenReadPlatform(stream) \
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found)
#define saveStreamOpenWritePlatform(stream) \
saveStreamOpenWriteDolphin(&(stream)->platform)
#define saveStreamClosePlatform(stream) \
saveStreamCloseDolphin(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesDolphin(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekDolphin(&(stream)->platform, pos)
#define saveStreamTellPlatform(stream, out) \
saveStreamTellDolphin(&(stream)->platform, out)
-120
View File
@@ -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/savestreamdolphin.h"
#include "util/memory.h"
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_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, SAVE_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 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 data: %s (%d)",
saveCardErrorStringDolphin(result), result
);
}
*found = true;
p->position = 0;
p->writing = false;
errorOk();
}
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_t *p) {
if(!SAVE.platform.mounted) errorThrow("No memory card mounted");
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
p->position = 0;
p->writing = true;
errorOk();
}
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
if(!p->writing) return;
int32_t result;
do {
result = CARD_Open(
SAVE_DOLPHIN_CHANNEL, SAVE_DOLPHIN_FILE_NAME, &p->cardFile
);
} while(result == CARD_ERROR_BUSY);
if(result == CARD_ERROR_NOFILE) {
do {
result = CARD_Create(
SAVE_DOLPHIN_CHANNEL, SAVE_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 saveStreamReadBytesDolphin(
savestreamdolphin_t *p, void *buf, const size_t len
) {
if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream read exceeds sector size");
}
memoryCopy(buf, p->buffer + p->position, len);
p->position += len;
errorOk();
}
errorret_t saveStreamWriteBytesDolphin(
savestreamdolphin_t *p, const void *buf, const size_t len
) {
if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream write exceeds sector size");
}
memoryCopy(p->buffer + p->position, buf, len);
p->position += len;
errorOk();
}
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos) {
if(pos >= SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream seek out of range");
}
p->position = pos;
errorOk();
}
errorret_t saveStreamTellDolphin(savestreamdolphin_t *p, size_t *out) {
*out = p->position;
errorOk();
}
-94
View File
@@ -1,94 +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;
} savestreamdolphin_t;
/**
* Opens the consolidated memory card file for reading by loading its
* sector into buffer.
*
* @param p Stream to initialize.
* @param found Set to true if the file exists, false if it does not.
* @return An error if reading the card fails for a reason other than
* missing file.
*/
errorret_t saveStreamOpenReadDolphin(savestreamdolphin_t *p, bool_t *found);
/**
* Opens the consolidated memory card file for writing by zeroing the
* sector buffer. The buffer is flushed to the card when
* saveStreamCloseDolphin is called.
*
* @param p Stream to initialize.
* @return An error if initialization fails.
*/
errorret_t saveStreamOpenWriteDolphin(savestreamdolphin_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 saveStreamCloseDolphin(savestreamdolphin_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 saveStreamReadBytesDolphin(
savestreamdolphin_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 saveStreamWriteBytesDolphin(
savestreamdolphin_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 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);
+8
View File
@@ -39,4 +39,12 @@ int32_t systemGetAspectRatioDolphin(void) {
int32_t systemGetLanguageDolphin(void) {
return CONF_GetLanguage();
}
const localeinfo_t * systemGetLocaleDolphin(void) {
switch(systemGetLanguageDolphin()) {
case CONF_LANG_JAPANESE: return &LOCALE_INFO_JP_JP;
case CONF_LANG_SPANISH: return &LOCALE_INFO_ES_MX;
default: return &LOCALE_DEFAULT;
}
}
+8
View File
@@ -47,5 +47,13 @@ int32_t systemGetAspectRatioDolphin(void);
*/
int32_t systemGetLanguageDolphin(void);
/**
* Returns the locale matching the system's configured language, falling
* back to LOCALE_DEFAULT if the system language has no matching locale.
*
* @return The current locale.
*/
const localeinfo_t * systemGetLocaleDolphin(void);
// There's actually a tonne more things Wii can return, this is it for now
// though.
+2 -1
View File
@@ -9,4 +9,5 @@
#include "system/systemdolphin.h"
#define systemInitPlatform systemInitDolphin
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeDolphin
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeDolphin
#define systemGetLocalePlatform systemGetLocaleDolphin
-1
View File
@@ -16,6 +16,5 @@ add_subdirectory(input)
if(DUSK_NETWORK)
add_subdirectory(network)
endif()
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+1 -2
View File
@@ -6,7 +6,6 @@
*/
#include "input/input.h"
#include "save/save.h"
inputbuttondata_t INPUT_BUTTON_DATA[] = {
#ifdef DUSK_INPUT_GAMEPAD
@@ -548,5 +547,5 @@ errorret_t inputInitLinux(void) {
}
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
return saveGetMeta()->deadzone;
return 0.2f;
}
-11
View File
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savelinux.c
savejsonlinux.c
)
-175
View File
@@ -1,175 +0,0 @@
/**
* 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 saveJsonWriterAddUInt8Linux(
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t value
) {
yyjson_mut_obj_add_uint(writer->doc, writer->root, key, (uint64_t)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);
}
uint8_t saveJsonReadUInt8Linux(
yyjson_val *root, const char_t *key, const uint8_t defaultValue
) {
yyjson_val *val = yyjson_obj_get(root, key);
if(!val || !yyjson_is_num(val)) return defaultValue;
return (uint8_t)yyjson_get_uint(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);
}
}
-162
View File
@@ -1,162 +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 "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 "key": <uint> field to the root object.
*/
void saveJsonWriterAddUInt8Linux(
savejsonwriterlinux_t *writer, const char_t *key, const uint8_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 uint8 field, falling back to defaultValue if the key is
* missing or not a number.
*/
uint8_t saveJsonReadUInt8Linux(
yyjson_val *root, const char_t *key, const uint8_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
);
-150
View File
@@ -1,150 +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/savejsonlinux.h"
#include "util/string.h"
#include "util/memory.h"
#include <sys/stat.h>
#include <errno.h>
errorret_t saveInitLinux(void) {
stringCopy(SAVE.platform.savePath, SAVE_LINUX_PATH, SAVE_LINUX_PATH_MAX);
if(mkdir(SAVE.platform.savePath, 0755) != 0 && errno != EEXIST) {
errorThrow("Failed to create save directory: %s", SAVE.platform.savePath);
}
errorOk();
}
errorret_t saveDisposeLinux(void) {
errorOk();
}
static void _saveSlotPathLinux(
char_t *out, const size_t max, const uint8_t slot
) {
snprintf(
out, max, SAVE_LINUX_SLOT_FILE_FORMAT, SAVE.platform.savePath,
(uint32_t)slot
);
}
static void _saveMetaPathLinux(char_t *out, const size_t max) {
snprintf(out, max, SAVE_LINUX_META_FILE_FORMAT, SAVE.platform.savePath);
}
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();
}
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData) {
char_t path[SAVE_LINUX_PATH_MAX];
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
slotData->version = SAVE_SLOT_VERSION;
savejsonwriterlinux_t writer;
errorChain(saveJsonWriterInitLinux(&writer));
saveJsonWriterAddUInt32Linux(&writer, "version", slotData->version);
saveJsonWriterAddStringLinux(&writer, "playerName", slotData->playerName);
saveJsonWriterAddBoolArrayLinux(
&writer, "globalItemCollected", slotData->globalItemCollected,
SAVE_GLOBAL_ITEM_COUNT_MAX
);
saveJsonWriterAddUInt8ArrayLinux(
&writer, "storyFlags", slotData->storyFlags, SAVE_STORY_FLAG_COUNT_MAX
);
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
saveJsonWriterDisposeLinux(&writer);
errorChain(ret);
slotData->exists = true;
errorOk();
}
errorret_t saveDeleteSlotLinux(const uint8_t slot) {
char_t path[SAVE_LINUX_PATH_MAX];
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
if(remove(path) != 0 && errno != ENOENT) {
errorThrow("Failed to delete save slot %u: %s", (uint32_t)slot, path);
}
errorOk();
}
errorret_t saveMetaLoadLinux(savemeta_t *out) {
char_t path[SAVE_LINUX_PATH_MAX];
_saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX);
yyjson_doc *doc;
yyjson_val *root;
bool_t found;
errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found);
if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); }
if(!found) errorOk();
out->version = saveJsonReadUInt32Linux(root, "version", SAVE_META_VERSION);
out->deadzone = saveJsonReadFloatLinux(
root, "deadzone", SAVE_META_DEADZONE_DEFAULT
);
out->language = saveJsonReadUInt8Linux(
root, "language", SAVE_META_LANGUAGE_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);
saveJsonWriterAddUInt8Linux(&writer, "language", meta->language);
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
saveJsonWriterDisposeLinux(&writer);
errorChain(ret);
meta->exists = true;
errorOk();
}
-80
View File
@@ -1,80 +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/saveslot.h"
#include "save/savemeta.h"
#define SAVE_LINUX_PATH_MAX FILENAME_MAX
#define SAVE_LINUX_SLOT_FILE_FORMAT "%s/slot%u.json"
#define SAVE_LINUX_META_FILE_FORMAT "%s/settings.json"
#ifndef SAVE_LINUX_PATH
#define SAVE_LINUX_PATH "./saves"
#endif
typedef struct {
char_t savePath[SAVE_LINUX_PATH_MAX];
} savelinux_t;
/**
* 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.
*/
errorret_t saveInitLinux(void);
/**
* Disposes of the save system on Linux.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposeLinux(void);
/**
* Loads a save slot as JSON (slotN.json) from disk, if it exists.
*
* @param slot The save slot index.
* @param out Output slot data.
* @return An error code if the slot exists but fails to parse.
*/
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out);
/**
* Writes a save slot as JSON (slotN.json) to disk.
*
* @param slot The save slot index.
* @param slotData Slot data to write.
* @return An error code if the write fails.
*/
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData);
/**
* Deletes the save slot JSON file for the given index.
*
* @param slot The save slot index.
* @return An error code if the delete fails.
*/
errorret_t 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);
-27
View File
@@ -1,27 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savelinux.h"
typedef savelinux_t saveplatform_t;
// Linux fully overrides every save operation with JSON I/O (see
// savelinux.c/savejsonlinux.c) - nothing generic ever opens a
// savestream_t here, so this only needs to exist for that shared type to
// compile.
typedef struct {
uint8_t reserved;
} saveplatformstream_t;
#define saveInitPlatform saveInitLinux
#define saveDisposePlatform saveDisposeLinux
#define saveSlotDeletePlatform saveDeleteSlotLinux
#define saveSlotLoadPlatform saveSlotLoadLinux
#define saveSlotWritePlatform saveSlotWriteLinux
#define saveMetaLoadPlatform saveMetaLoadLinux
#define saveMetaWritePlatform saveMetaWriteLinux
+4
View File
@@ -13,4 +13,8 @@ errorret_t systemInitLinux() {
systemdialogtype_t systemGetActiveDialogTypeLinux() {
return SYSTEM_DIALOG_TYPE_NONE;
}
const localeinfo_t * systemGetLocaleLinux() {
return &LOCALE_DEFAULT;
}
+9 -1
View File
@@ -18,4 +18,12 @@ errorret_t systemInitLinux(void);
*
* @return Currently open system dialog type.
*/
systemdialogtype_t systemGetActiveDialogTypeLinux();
systemdialogtype_t systemGetActiveDialogTypeLinux();
/**
* Returns the current locale of the system. Linux has no system language
* query wired up yet, so this always returns LOCALE_DEFAULT.
*
* @return The current locale.
*/
const localeinfo_t * systemGetLocaleLinux();
+2 -1
View File
@@ -9,4 +9,5 @@
#include "system/systemlinux.h"
#define systemInitPlatform systemInitLinux
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeLinux
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeLinux
#define systemGetLocalePlatform systemGetLocaleLinux
-1
View File
@@ -21,6 +21,5 @@ add_subdirectory(log)
if(DUSK_NETWORK)
add_subdirectory(network)
endif()
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+1 -2
View File
@@ -6,7 +6,6 @@
*/
#include "input/input.h"
#include "save/save.h"
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
@@ -95,5 +94,5 @@ errorret_t inputInitPSP(void) {
}
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
return saveGetMeta()->deadzone;
return 0.25f;
}
-19
View File
@@ -1,19 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savepsp.c
savestreampsp.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
)
-53
View File
@@ -1,53 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savepsp.h"
#include "save/savestreampsp.h"
typedef savepsp_t saveplatform_t;
typedef savestreampsp_t saveplatformstream_t;
#define saveInitPlatform saveInitPSP
#define saveDisposePlatform saveDisposePSP
#define saveSlotDeletePlatform saveDeleteSlotPSP
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekPSP(&(stream)->platform, pos)
#define saveStreamTellPlatform(stream, out) \
saveStreamTellPSP(&(stream)->platform, out)
// Save/load go entirely through the native sceUtilitySavedata dialog
// (savePSPBeginSave/Load), which spans multiple frames - these bypass
// save.c's normal synchronous open/write-fields/close flow above (that's
// still used internally, just against an in-memory buffer, from within
// savePSPBeginSave/Load themselves) and are what save.c's saveWriteSlot()/
// saveLoadSlot()/saveWriteMeta()/saveLoadMeta() actually call on this
// platform. Meta and the (one) slot are serialized together into the same
// buffer - there is no separate meta-only path on PSP - so the meta
// variants just drive the same dialog against SAVE_ACTIVE_SLOT.
#define saveSlotAsyncWritePlatform(slot, onComplete, user) \
savePSPBeginSave(slot, onComplete, user)
#define saveSlotAsyncLoadPlatform(slot, onComplete, user) \
savePSPBeginLoad(slot, onComplete, user)
#define saveMetaAsyncWritePlatform(onComplete, user) \
savePSPBeginSave(SAVE_ACTIVE_SLOT, onComplete, user)
#define saveMetaAsyncLoadPlatform(onComplete, user) \
savePSPBeginLoad(SAVE_ACTIVE_SLOT, onComplete, user)
#define saveIsBusyPlatform() savePSPIsBusy()
#define savePlatformUpdate() savePSPUpdate()
// Meta only reaches memory via the native dialog now (folded into the same
// payload as the save slot) - running that multi-frame dialog on every
// single boot just to eagerly populate SAVE.meta would reintroduce the
// exact UX problem a lightweight settings-only file used to avoid, so
// saveInit() skips eager loading entirely on this platform.
#define saveSkipEagerLoadPlatform
-291
View File
@@ -1,291 +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/savepsp.h"
#include "save/savestream.h"
#include "system/systempsp.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
static void savePSPParamCommonInit(SceUtilitySavedataParam *param) {
memoryZero(param, sizeof(SceUtilitySavedataParam));
param->base.size = sizeof(SceUtilitySavedataParam);
param->base.language = systemPSPGetLanguage();
param->base.buttonSwap = systemPSPGetCrossButtonSetting();
param->base.graphicsThread = 17;
param->base.accessThread = 19;
param->base.fontThread = 18;
param->base.soundThread = 16;
stringCopy(param->gameName, SAVE_PSP_GAME_NAME, sizeof(param->gameName));
stringCopy(param->fileName, SAVE_PSP_FILE_NAME, sizeof(param->fileName));
}
static void savePSPSaveNameForSlot(
char_t *out, const size_t max, const uint8_t slot
) {
stringFormat(out, max, "%02u", (uint32_t)slot);
}
errorret_t saveInitPSP(void) {
SceIoStat stat;
if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) {
errorThrow("No memory stick detected");
}
errorOk();
}
errorret_t saveDisposePSP(void) {
errorOk();
}
errorret_t saveDeleteSlotPSP(const uint8_t slot) {
char_t path[SAVE_PSP_PATH_MAX];
stringFormat(
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
(uint32_t)slot
);
int32_t result = sceIoRemove(path);
if(result < 0 && result != (int32_t)0x80010002) {
errorThrow("Failed to delete save data for slot %u", (uint32_t)slot);
}
char_t dir[SAVE_PSP_PATH_MAX];
stringFormat(
dir, sizeof(dir), "ms0:/PSP/SAVEDATA/%s%02u", SAVE_PSP_GAME_NAME,
(uint32_t)slot
);
char_t sfoPath[SAVE_PSP_PATH_MAX];
stringFormat(sfoPath, sizeof(sfoPath), "%s/PARAM.SFO", dir);
// Best-effort - PARAM.SFO/the directory itself may not exist (e.g. this
// slot was written by the old raw-file format, pre-dating this dialog-
// based rewrite) or the directory may still contain other entries.
sceIoRemove(sfoPath);
sceIoRmdir(dir);
errorOk();
}
void savePSPBeginSave(
const uint8_t slot, savecallback_t onComplete, void *user
) {
assertNotNull(onComplete, "onComplete cannot be NULL");
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
saveslot_t *slotData = &SAVE.slots[slot];
// Serialize meta then the slot into the buffer synchronously (plain
// memory writes, same header/version/CRC framing as every other
// platform) before the dialog ever starts - only the actual commit-to-
// storage step needs to wait on the dialog.
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
stream.platform.buffer = SAVE.platform.dataBuffer;
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
if(errorIsOk(ret)) ret = saveSlotSerializeWrite(&stream, slotData);
if(errorIsNotOk(ret)) {
onComplete(ret, user);
return;
}
SAVE.platform.dataLength = stream.platform.length;
SceUtilitySavedataParam *param = &SAVE.platform.param;
savePSPParamCommonInit(param);
// AUTOSAVE rather than SAVE - SAVE shows a "save to this data?" confirm
// screen even for a slot with no existing data, which isn't the UX we
// want for a menu-triggered "Save" action (that confirmation already
// happened when the player chose to save). AUTOSAVE writes silently
// (just a brief "saving" icon flash) while still generating the same
// PARAM.SFO/title/description as any other mode.
param->mode = PSP_UTILITY_SAVEDATA_AUTOSAVE;
param->overwrite = 1;
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
param->dataBuf = SAVE.platform.dataBuffer;
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
param->dataSize = SAVE.platform.dataLength;
// No ICON0/PIC1/SND0 art exists in this project yet, so these are left
// zeroed (bufSize 0) - the utility treats that as "no icon/background/
// sound" rather than an error. title/savedataTitle/detail are still
// fully functional and are what actually populates PARAM.SFO and the
// save browser entry.
stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title));
stringCopy(
param->sfoParam.savedataTitle, slotData->playerName,
sizeof(param->sfoParam.savedataTitle)
);
stringCopy(
param->sfoParam.detail, "Dusk save file.", sizeof(param->sfoParam.detail)
);
int32_t initRet = sceUtilitySavedataInitStart(param);
if(initRet < 0) {
onComplete(errorThrowImpl(
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
"Failed to start save dialog: 0x%08X", initRet
), user);
return;
}
SAVE.platform.op = SAVE_PSP_OP_SAVE;
SAVE.platform.slot = slot;
SAVE.platform.onComplete = onComplete;
SAVE.platform.onCompleteUser = user;
}
void savePSPBeginLoad(
const uint8_t slot, savecallback_t onComplete, void *user
) {
assertNotNull(onComplete, "onComplete cannot be NULL");
assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress");
char_t path[SAVE_PSP_PATH_MAX];
stringFormat(
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
(uint32_t)slot
);
SceIoStat stat;
if(sceIoGetstat(path, &stat) < 0) {
// No save data yet - not an error (matches every other platform's
// "nothing to load yet" behavior), and deliberately skips showing the
// dialog at all rather than surfacing an empty "no data" native
// screen for data the player has never saved.
onComplete(errorOkImpl(), user);
return;
}
SceUtilitySavedataParam *param = &SAVE.platform.param;
savePSPParamCommonInit(param);
param->mode = PSP_UTILITY_SAVEDATA_AUTOLOAD;// See savePSPBeginSave().
savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot);
param->dataBuf = SAVE.platform.dataBuffer;
param->dataBufSize = sizeof(SAVE.platform.dataBuffer);
int32_t initRet = sceUtilitySavedataInitStart(param);
if(initRet < 0) {
onComplete(errorThrowImpl(
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
"Failed to start load dialog: 0x%08X", initRet
), user);
return;
}
SAVE.platform.op = SAVE_PSP_OP_LOAD;
SAVE.platform.slot = slot;
SAVE.platform.onComplete = onComplete;
SAVE.platform.onCompleteUser = user;
}
bool_t savePSPIsBusy(void) {
return SAVE.platform.op != SAVE_PSP_OP_NONE;
}
errorret_t savePSPUpdate(void) {
if(SAVE.platform.op == SAVE_PSP_OP_NONE) errorOk();
int32_t status = sceUtilitySavedataGetStatus();
switch(status) {
case PSP_UTILITY_DIALOG_INIT:
break;
// NOTE: unlike the netconf dialog, this does not replicate Dusk's own
// GL state (blend/cull/depth + texture/color) before calling Update().
// A prior fix for exactly that class of bug was documented for the
// network dialog, but no longer exists in the current codebase to
// copy from - if the save dialog's own text/icons don't render
// correctly on real hardware (PPSSPP won't reproduce this - it doesn't
// model pspGL's deferred state application), that state-priming
// pattern is the fix to reach for. See the network dialog's git
// history / the project's PSP dialog memory notes for the exact
// technique (state flags + a forced flush via a degenerate triangle
// draw).
case PSP_UTILITY_DIALOG_VISIBLE:
// sceUtilitySavedataUpdate() is void, unlike sceUtilityNetconfUpdate()
// - nothing to check here, GetStatus() next frame reflects any
// resulting state change.
sceUtilitySavedataUpdate(1);
break;
case PSP_UTILITY_DIALOG_QUIT:
// The save/load operation itself has already finished (successfully
// or not) - this just starts tearing the dialog down. The actual
// result is read once that teardown settles, below - don't call
// ShutdownStart more than once while waiting for it to.
if(!SAVE.platform.shuttingDown) {
SAVE.platform.shuttingDown = true;
sceUtilitySavedataShutdownStart();
}
break;
// Confirmed under PPSSPP: status settles straight from QUIT to NONE,
// without FINISHED ever being separately observed in between - so
// both are treated identically here as "torn down, read the result",
// and it's shuttingDown (not which of these two codes we saw) that
// distinguishes that from a genuine disappearance.
case PSP_UTILITY_DIALOG_FINISHED:
case PSP_UTILITY_DIALOG_NONE: {
savepspop_t op = SAVE.platform.op;
uint8_t slot = SAVE.platform.slot;
savecallback_t cb = SAVE.platform.onComplete;
void *user = SAVE.platform.onCompleteUser;
bool_t reachedQuit = SAVE.platform.shuttingDown;
SAVE.platform.op = SAVE_PSP_OP_NONE;
SAVE.platform.shuttingDown = false;
if(!reachedQuit) {
cb(errorThrowImpl(
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
"Save dialog disappeared without a result"
), user);
break;
}
int32_t result = SAVE.platform.param.base.result;
if(result != 0) {
SAVE.available = false;
cb(errorThrowImpl(
&SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
"Save dialog failed: 0x%08X", result
), user);
break;
}
SAVE.available = true;
if(op == SAVE_PSP_OP_LOAD) {
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
stream.platform.buffer = SAVE.platform.dataBuffer;
stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer);
stream.platform.length = SAVE.platform.param.dataSize;
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
if(errorIsOk(ret)) {
ret = saveSlotSerializeRead(&stream, &SAVE.slots[slot]);
}
cb(ret, user);
} else {
SAVE.meta.exists = true;
SAVE.slots[slot].exists = true;
cb(errorOkImpl(), user);
}
break;
}
default:
errorThrow("Unknown savedata dialog status: %d", status);
}
errorOk();
}
-133
View File
@@ -1,133 +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/saveslot.h"
#include "save/savemeta.h"
#include <pspiofilemgr.h>
#include <psputility.h>
#define SAVE_PSP_PATH_MAX 256
#define SAVE_PSP_ROOT "ms0:/"
#define SAVE_PSP_FILE_NAME "save.bin"
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/" SAVE_PSP_FILE_NAME
#define SAVE_PSP_DATA_BUFFER_SIZE 4096
#ifndef SAVE_PSP_GAME_NAME
#define SAVE_PSP_GAME_NAME "DUSK00001"
#endif
typedef enum {
SAVE_PSP_OP_NONE,
SAVE_PSP_OP_SAVE,
SAVE_PSP_OP_LOAD
} savepspop_t;
typedef struct {
SceUtilitySavedataParam param;
// Raw buffer sceUtilitySavedata reads/writes the whole save into/from -
// holds BOTH save meta and the (single) save slot back-to-back,
// populated by our own savestream_t serialization (see savestreampsp.h)
// before a save starts, and deserialized from after a load finishes.
// Meta lives in here rather than its own lightweight file specifically
// because a device-wide preference change is meant to feel like a real
// save on this platform (a brief native icon flash), not need its own
// separate storage mechanism.
uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64)));
size_t dataLength;
savepspop_t op;
// True once sceUtilitySavedataShutdownStart() has been requested (dialog
// status PSP_UTILITY_DIALOG_QUIT seen) - distinguishes a normal "torn
// down after finishing" NONE/FINISHED from a genuinely unexpected one
// seen before ever reaching QUIT. Some implementations (confirmed on
// PPSSPP) settle straight to NONE after shutdown without a separately
// observable FINISHED step in between.
bool_t shuttingDown;
uint8_t slot;
savecallback_t onComplete;
void *onCompleteUser;
} savepsp_t;
/**
* Initializes the save system on PSP. Confirms the memory stick is
* actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming
* so, since the savedata dialog otherwise only reports failure once a
* save/load is actually attempted.
*
* @return An error code if no memory stick is reachable.
*/
errorret_t saveInitPSP(void);
/**
* Disposes of the save system on PSP.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposePSP(void);
/**
* Deletes the (one) save data folder from the memory stick.
*
* @param slot The save slot index (always 0 on PSP - see
* SAVE_SLOT_COUNT_MAX's override in this platform's CMakeLists.txt).
* @return An error code if the delete fails.
*/
errorret_t saveDeleteSlotPSP(const uint8_t slot);
/**
* Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE -
* writes silently with just a brief icon flash, no confirm screen, since
* SAVE mode shows one even for a slot with no existing data - but
* PARAM.SFO/title/description are generated identically regardless of
* mode, and the OS handles the save browser entry either way). Serializes
* SAVE.meta then SAVE.slots[slot] into SAVE.platform.dataBuffer first,
* synchronously, then kicks off the dialog and returns - completion is
* reported later via onComplete, driven by savePSPUpdate() each frame.
* If no save data exists yet, sceUtilitySavedataInitStart() creates it.
*
* @param slot The save slot index.
* @param onComplete Callback invoked once the dialog finishes.
* @param user User data passed through to onComplete.
*/
void savePSPBeginSave(
const uint8_t slot, savecallback_t onComplete, void *user
);
/**
* Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD -
* see savePSPBeginSave() for why not the plain LOAD mode) unless a quick
* sceIoGetstat check finds no save data yet - in which case onComplete is
* invoked immediately with SAVE.meta/SAVE.slots[slot].exists left false,
* matching the other platforms' "no file yet" semantics, and no dialog is
* shown at all.
*
* @param slot The save slot index.
* @param onComplete Callback invoked once the dialog (or immediate
* not-found short-circuit) finishes.
* @param user User data passed through to onComplete.
*/
void savePSPBeginLoad(
const uint8_t slot, savecallback_t onComplete, void *user
);
/**
* Pumps the in-progress save/load dialog one step, if any - must be called
* every engine frame (see saveUpdate()). No-op if no dialog is active.
*
* @return An error code indicating success or failure.
*/
errorret_t savePSPUpdate(void);
/**
* True while a save/load dialog is in progress (see savePSPBeginSave()/
* savePSPBeginLoad()).
*
* @return True if a save/load dialog is currently open.
*/
bool_t savePSPIsBusy(void);
-46
View File
@@ -1,46 +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/savestreampsp.h"
#include "util/memory.h"
errorret_t saveStreamReadBytesPSP(
savestreampsp_t *p, void *buf, const size_t len
) {
if(p->position + len > p->length) {
errorThrow("Save stream read exceeds buffer length");
}
memoryCopy(buf, p->buffer + p->position, len);
p->position += len;
errorOk();
}
errorret_t saveStreamWriteBytesPSP(
savestreampsp_t *p, const void *buf, const size_t len
) {
if(p->position + len > p->bufferSize) {
errorThrow("Save stream write exceeds buffer size");
}
memoryCopy(p->buffer + p->position, buf, len);
p->position += len;
if(p->position > p->length) p->length = p->position;
errorOk();
}
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
if(pos > p->bufferSize) {
errorThrow("Save stream seek out of range");
}
p->position = pos;
errorOk();
}
errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out) {
*out = p->position;
errorOk();
}
-64
View File
@@ -1,64 +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 <stddef.h>
// Backed by SAVE.platform.dataBuffer (see savepsp.h) rather than owning its
// own memory - the buffer has to outlive a single saveFileWrite()/Load()
// call, since the actual save/load dialog it's handed to only completes
// several frames later.
typedef struct {
uint8_t *buffer;
size_t bufferSize;
size_t position;
size_t length;
} savestreampsp_t;
/**
* Copies len bytes from the 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 populated data length.
*/
errorret_t saveStreamReadBytesPSP(
savestreampsp_t *p, void *buf, const size_t len
);
/**
* Copies len bytes from buf into the buffer at the current position,
* growing p->length if this write extends past it.
*
* @param p Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write would exceed bufferSize.
*/
errorret_t saveStreamWriteBytesPSP(
savestreampsp_t *p, const void *buf, const size_t len
);
/**
* Sets the current read/write position within the buffer.
*
* @param p Active stream.
* @param pos Target byte offset from the start of the buffer.
* @return An error if pos is out of range.
*/
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
/**
* Gets the current read/write position within the buffer.
*
* @param p Active stream.
* @param out Receives the current position.
* @return An error - always succeeds, matches saveStreamTellImpl's shape.
*/
errorret_t saveStreamTellPSP(savestreampsp_t *p, size_t *out);
+2 -1
View File
@@ -9,4 +9,5 @@
#include "system/systempsp.h"
#define systemInitPlatform systemInitPSP
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypePSP
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypePSP
#define systemGetLocalePlatform systemGetLocalePSP
+8
View File
@@ -61,4 +61,12 @@ int_t systemPSPGetCrossButtonSetting() {
return (
ret == 1 ? PSP_UTILITY_ACCEPT_CROSS : PSP_UTILITY_ACCEPT_CIRCLE
);
}
const localeinfo_t * systemGetLocalePSP(void) {
switch(systemPSPGetLanguage()) {
case PSP_SYSTEMPARAM_LANGUAGE_JAPANESE: return &LOCALE_INFO_JP_JP;
case PSP_SYSTEMPARAM_LANGUAGE_SPANISH: return &LOCALE_INFO_ES_MX;
default: return &LOCALE_DEFAULT;
}
}
+9 -1
View File
@@ -37,4 +37,12 @@ int_t systemPSPGetLanguage();
*
* @return PSP_UTILITY_ACCEPT_CROSS or PSP_UTILITY_ACCEPT_CIRCLE.
*/
int_t systemPSPGetCrossButtonSetting();
int_t systemPSPGetCrossButtonSetting();
/**
* Returns the locale matching the system's configured language, falling
* back to LOCALE_DEFAULT if the system language has no matching locale.
*
* @return The current locale.
*/
const localeinfo_t * systemGetLocalePSP(void);
+1 -2
View File
@@ -6,7 +6,6 @@
*/
#include "input/input.h"
#include "save/save.h"
inputbuttondata_t INPUT_BUTTON_DATA[] = {
{ .name = "triangle", {
@@ -84,5 +83,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = {
};
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
return saveGetMeta()->deadzone;
return 0.2f;
}
-11
View File
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savevita.c
savestreamvita.c
)
-30
View File
@@ -1,30 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savevita.h"
#include "save/savestreamvita.h"
typedef savevita_t saveplatform_t;
typedef savestreamvita_t saveplatformstream_t;
#define saveInitPlatform saveInitVita
#define saveDisposePlatform saveDisposeVita
#define saveDeletePlatform saveDeleteVita
#define saveStreamOpenReadPlatform(stream, slot) \
saveStreamOpenReadVita(&(stream)->platform, &(stream)->found, slot)
#define saveStreamOpenWritePlatform(stream, slot) \
saveStreamOpenWriteVita(&(stream)->platform, slot)
#define saveStreamClosePlatform(stream) \
saveStreamCloseVita(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesVita(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesVita(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekVita(&(stream)->platform, pos)
-77
View File
@@ -1,77 +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/savestreamvita.h"
errorret_t saveStreamOpenReadVita(
savestreamvita_t *p, bool_t *found, const uint8_t slot
) {
char_t path[SAVE_VITA_PATH_MAX];
snprintf(path, SAVE_VITA_PATH_MAX, SAVE_VITA_FILE_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, SCE_O_RDONLY, 0);
*found = (p->fd >= 0);
errorOk();
}
errorret_t saveStreamOpenWriteVita(savestreamvita_t *p, const uint8_t slot) {
char_t dir[SAVE_VITA_PATH_MAX];
snprintf(dir, SAVE_VITA_PATH_MAX, SAVE_VITA_DIR_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
sceIoMkdir(dir, 0777);
char_t path[SAVE_VITA_PATH_MAX];
snprintf(path, SAVE_VITA_PATH_MAX, SAVE_VITA_FILE_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777);
if(p->fd < 0) {
errorThrow(
"Failed to open Vita save file for writing: slot %u", (uint32_t)slot
);
}
errorOk();
}
void saveStreamCloseVita(savestreamvita_t *p) {
if(p->fd >= 0) {
sceIoClose(p->fd);
p->fd = -1;
}
}
errorret_t saveStreamReadBytesVita(
savestreamvita_t *p, void *buf, const size_t len
) {
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
if(read != (int32_t)len) {
errorThrow("Unexpected end of Vita save file");
}
errorOk();
}
errorret_t saveStreamWriteBytesVita(
savestreamvita_t *p, const void *buf, const size_t len
) {
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
if(written != (int32_t)len) {
errorThrow("Failed to write Vita save data");
}
errorOk();
}
errorret_t saveStreamSeekVita(savestreamvita_t *p, const size_t pos) {
if(sceIoLseek(p->fd, (SceOff)pos, SCE_SEEK_SET) < 0) {
errorThrow("Failed to seek in Vita save file");
}
errorOk();
}
-78
View File
@@ -1,78 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <psp2/io/fcntl.h>
#include <psp2/io/stat.h>
#include <stddef.h>
typedef struct {
SceUID fd;
} savestreamvita_t;
/**
* Opens a Vita save data 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 saveStreamOpenReadVita(
savestreamvita_t *p, bool_t *found, const uint8_t slot
);
/**
* Opens a Vita save data file for writing, creating or truncating it.
* Creates the save data directory if it does not already exist.
*
* @param p Stream to initialize.
* @param slot Save slot index.
* @return An error if the file cannot be opened for writing.
*/
errorret_t saveStreamOpenWriteVita(savestreamvita_t *p, const uint8_t slot);
/**
* Closes the file descriptor held by the stream.
*
* @param p Stream to close.
*/
void saveStreamCloseVita(savestreamvita_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 saveStreamReadBytesVita(
savestreamvita_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 saveStreamWriteBytesVita(
savestreamvita_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 saveStreamSeekVita(savestreamvita_t *p, const size_t pos);