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:
@@ -21,8 +21,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/savemanager.h"\
|
||||||
#include "save/autosave.h"
|
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
errorChain(saveInit());
|
errorChain(saveManagerInit());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
@@ -68,8 +67,7 @@ errorret_t engineUpdate(void) {
|
|||||||
#ifdef DUSK_NETWORK
|
#ifdef DUSK_NETWORK
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
#endif
|
#endif
|
||||||
errorChain(saveUpdate());
|
// errorChain(saveManagerUpdate());
|
||||||
autoSaveUpdate();
|
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
@@ -98,7 +96,7 @@ errorret_t engineDispose(void) {
|
|||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
errorChain(saveDispose());
|
errorChain(saveManagerDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -13,17 +13,31 @@ typedef struct {
|
|||||||
const char_t *file;
|
const char_t *file;
|
||||||
} localeinfo_t;
|
} localeinfo_t;
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_EN_US = {
|
static const localeinfo_t LOCALE_INFO_EN_US = {
|
||||||
.name = "en-US",
|
.name = "en-US",
|
||||||
.file = "locale/en_US.po",
|
.file = "locale/en_US.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_JP_JP = {
|
static const localeinfo_t LOCALE_INFO_JP_JP = {
|
||||||
.name = "ja-JP",
|
.name = "ja-JP",
|
||||||
.file = "locale/jp_JP.po",
|
.file = "locale/jp_JP.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_ES_MX = {
|
static const localeinfo_t LOCALE_INFO_ES_MX = {
|
||||||
.name = "es-MX",
|
.name = "es-MX",
|
||||||
.file = "locale/es_MX.po",
|
.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
|
||||||
|
|||||||
@@ -9,40 +9,23 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "save/save.h"
|
#include "ui/ui.h"
|
||||||
|
#include "system/system.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
localemanager_t LOCALE;
|
localemanager_t LOCALE;
|
||||||
|
|
||||||
const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT] = {
|
|
||||||
&LOCALE_EN_US,
|
|
||||||
&LOCALE_JP_JP,
|
|
||||||
&LOCALE_ES_MX
|
|
||||||
};
|
|
||||||
|
|
||||||
errorret_t localeManagerInit() {
|
errorret_t localeManagerInit() {
|
||||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
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
|
// TODO: Set locale based on system locale.
|
||||||
// this point - see localeManagerGetByIndex().
|
const localeinfo_t *locale = systemGetLocale();
|
||||||
errorChain(localeManagerSetLocale(
|
errorChain(localeManagerSetLocale(locale));
|
||||||
localeManagerGetByIndex(saveGetMeta()->language)
|
consolePrint("Locale set to: %s", locale->name);
|
||||||
));
|
|
||||||
errorOk();
|
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) {
|
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||||
assertNotNull(locale, "Locale cannot be NULL");
|
assertNotNull(locale, "Locale cannot be NULL");
|
||||||
|
|
||||||
@@ -56,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
|||||||
assetEntryLock(LOCALE.entry);
|
assetEntryLock(LOCALE.entry);
|
||||||
errorChain(assetRequireLoaded(LOCALE.entry));
|
errorChain(assetRequireLoaded(LOCALE.entry));
|
||||||
|
|
||||||
|
// TODO : Trigger UI update.
|
||||||
|
errorChain(uiUpdateTranslations());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "locale/localeinfo.h"
|
#include "locale/localeinfo.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
@@ -18,15 +18,6 @@ typedef struct {
|
|||||||
|
|
||||||
extern localemanager_t LOCALE;
|
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.
|
* Initialize the locale system.
|
||||||
*
|
*
|
||||||
@@ -42,27 +33,6 @@ errorret_t localeManagerInit();
|
|||||||
*/
|
*/
|
||||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
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.
|
* Get a localized string for the given message ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,5 +6,4 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
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
@@ -17,11 +17,8 @@
|
|||||||
#include "rpgcamera.h"
|
#include "rpgcamera.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "save/autosave.h"
|
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
|
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
#include "rpg/story/storyflag.h"
|
#include "rpg/story/storyflag.h"
|
||||||
|
|
||||||
@@ -29,27 +26,14 @@ errorret_t rpgInit(void) {
|
|||||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||||
|
|
||||||
saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT);
|
|
||||||
// saveInit() eagerly loads every slot from disk, but there's no
|
|
||||||
// continue-game flow yet - every boot is a fresh game regardless of
|
|
||||||
// what was found on disk, so force this false rather than let a stale
|
|
||||||
// "exists" from a real save skip storyFlagInitDefaults() below while
|
|
||||||
// everything else here still hardcodes new-game state.
|
|
||||||
saveSlot->exists = false;
|
|
||||||
|
|
||||||
// Must run before any code reads a story flag - stamps CSV-defined
|
|
||||||
// defaults onto the active save slot since it's now forced to look
|
|
||||||
// unloaded.
|
|
||||||
storyFlagInitDefaults(saveSlot);
|
|
||||||
|
|
||||||
backpackInit();
|
backpackInit();
|
||||||
partyInit();
|
partyInit();
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
|
|
||||||
errorChain(mapInit());
|
errorChain(mapInit());
|
||||||
|
|
||||||
rpgCameraInit();
|
rpgCameraInit();
|
||||||
// Init world
|
|
||||||
|
|
||||||
|
// Init test world
|
||||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||||
|
|
||||||
// The player is the one entity that isn't sourced from map/chunk data -
|
// 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_POTATO, 3);
|
||||||
backpackAdd(ITEM_ID_APPLE, 8);
|
backpackAdd(ITEM_ID_APPLE, 8);
|
||||||
|
|
||||||
|
|
||||||
// All Good!
|
// All Good!
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -79,12 +64,6 @@ errorret_t rpgUpdate(void) {
|
|||||||
}
|
}
|
||||||
#endif
|
#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?
|
// TODO: Do not update if the scene is not the map scene?
|
||||||
errorChain(mapUpdate());
|
errorChain(mapUpdate());
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,17 @@
|
|||||||
|
|
||||||
#include "storyflag.h"
|
#include "storyflag.h"
|
||||||
#include "assert/assert.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) {
|
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||||
saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void storyFlagInitDefaults(saveslot_t *file) {
|
// TODO: Dirty savefile
|
||||||
assertNotNull(file, "Save slot cannot be NULL");
|
consolePrint("Story flag set without save implementation");
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "rpg/story/storyflagvalue.h"
|
#include "rpg/story/storyflagvalue.h"
|
||||||
#include "save/save.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of a story flag. Reads directly from the active save
|
* Gets the value of a story flag. Reads directly from the active save
|
||||||
@@ -16,25 +15,12 @@
|
|||||||
* @param flag The story flag to get.
|
* @param flag The story flag to get.
|
||||||
* @return The value of the story flag.
|
* @return The value of the story flag.
|
||||||
*/
|
*/
|
||||||
#define storyFlagGet(flag) (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
|
* Sets the value of a story flag. Will dirty the savefile.
|
||||||
* SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call
|
|
||||||
* saveWriteSlot() separately once ready to persist it.
|
|
||||||
*
|
*
|
||||||
* @param flag The story flag to set.
|
* @param flag The story flag to set.
|
||||||
* @param value The value to set the story flag to.
|
* @param value The value to set the story flag to.
|
||||||
*/
|
*/
|
||||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
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);
|
|
||||||
|
|||||||
@@ -6,7 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
save.c
|
savemanager.c
|
||||||
savestream.c
|
|
||||||
autosave.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
@@ -9,71 +9,13 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "ui/frame/initial/uiinitialnocard.h"
|
#include "ui/frame/initial/uiinitialnocard.h"
|
||||||
#include "ui/frame/initial/uiinitialcreatesave.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) {
|
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||||
assertNotNull(sceneData, "Scene data cannot be null");
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
||||||
|
|
||||||
sceneInitialCheckSave();
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
#error "systemInitPlatform is not defined"
|
#error "systemInitPlatform is not defined"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef systemGetLocalePlatform
|
||||||
|
#error "systemGetLocalePlatform is not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
errorret_t systemInit() {
|
errorret_t systemInit() {
|
||||||
return systemInitPlatform();
|
return systemInitPlatform();
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,10 @@ systemdialogtype_t systemGetActiveDialogType() {
|
|||||||
return systemGetActiveDialogTypePlatform();
|
return systemGetActiveDialogTypePlatform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localeinfo_t * systemGetLocale(void) {
|
||||||
|
return systemGetLocalePlatform();
|
||||||
|
}
|
||||||
|
|
||||||
systemplatform_t systemGetPlatform(void) {
|
systemplatform_t systemGetPlatform(void) {
|
||||||
#if defined(DUSK_KNULLI)
|
#if defined(DUSK_KNULLI)
|
||||||
return SYSTEM_PLATFORM_KNULLI;
|
return SYSTEM_PLATFORM_KNULLI;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "system/systemplatformlist.h"
|
#include "system/systemplatformlist.h"
|
||||||
|
#include "locale/localeinfo.h"
|
||||||
|
|
||||||
#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value,
|
#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value,
|
||||||
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
|
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
|
||||||
@@ -48,3 +49,10 @@ systemdialogtype_t systemGetActiveDialogType();
|
|||||||
* @return The current platform.
|
* @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);
|
||||||
@@ -16,5 +16,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
ui.c
|
ui.c
|
||||||
uielement.c
|
uielement.c
|
||||||
|
uielementlist.c
|
||||||
# uitextbox.c
|
# uitextbox.c
|
||||||
)
|
)
|
||||||
@@ -11,7 +11,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
|
|
||||||
add_subdirectory(game)
|
add_subdirectory(game)
|
||||||
add_subdirectory(mainmenu)
|
add_subdirectory(mainmenu)
|
||||||
add_subdirectory(settings)
|
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
add_subdirectory(backpack)
|
add_subdirectory(backpack)
|
||||||
add_subdirectory(initial)
|
add_subdirectory(initial)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#include "uigamemenu.h"
|
#include "uigamemenu.h"
|
||||||
#include "ui/frame/uiframe.h"
|
#include "ui/frame/uiframe.h"
|
||||||
#include "ui/frame/uiconfirm.h"
|
#include "ui/frame/uiconfirm.h"
|
||||||
#include "ui/frame/settings/uisettings.h"
|
|
||||||
#include "ui/frame/backpack/uibackpack.h"
|
#include "ui/frame/backpack/uibackpack.h"
|
||||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
@@ -17,85 +16,11 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
||||||
#define UI_GAME_MENU_INDEX_ITEMS 1
|
#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;
|
uigamemenu_t UI_GAME_MENU;
|
||||||
|
|
||||||
@@ -104,9 +29,13 @@ void uiGameMenuSelected(
|
|||||||
const uint8_t index,
|
const uint8_t index,
|
||||||
const uimenuitem_t *item
|
const uimenuitem_t *item
|
||||||
) {
|
) {
|
||||||
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
switch(index) {
|
||||||
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
case UI_GAME_MENU_INDEX_ITEMS:
|
||||||
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
|
uiBackpackOpen();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable("Unhandled game menu index");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiGameMenuInit(void) {
|
errorret_t uiGameMenuInit(void) {
|
||||||
@@ -133,13 +62,6 @@ errorret_t uiGameMenuInit(void) {
|
|||||||
UI_GAME_MENU.settingsLabel,
|
UI_GAME_MENU.settingsLabel,
|
||||||
UI_GAME_MENU_LABEL_MAX
|
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(
|
errorChain(assetLocaleGetString(
|
||||||
&LOCALE.entry->data.locale,
|
&LOCALE.entry->data.locale,
|
||||||
"ui.game_menu.save_success",
|
"ui.game_menu.save_success",
|
||||||
@@ -195,7 +117,6 @@ errorret_t uiGameMenuInit(void) {
|
|||||||
);
|
);
|
||||||
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
MENU_BUTTON(UI_GAME_MENU.charactersLabel);
|
||||||
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
MENU_BUTTON(UI_GAME_MENU.itemsLabel);
|
||||||
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
|
|
||||||
MENU_BUTTON(UI_GAME_MENU.saveLabel);
|
MENU_BUTTON(UI_GAME_MENU.saveLabel);
|
||||||
|
|
||||||
MENU_END(UI_GAME_MENU.items, 1);
|
MENU_END(UI_GAME_MENU.items, 1);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "engine/engine.h"
|
#include "engine/engine.h"
|
||||||
#include "rpg/battle/testbattle/testbattle.h"
|
#include "rpg/battle/testbattle/testbattle.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
@@ -34,9 +33,6 @@ void uiMainMenuSelected(
|
|||||||
|
|
||||||
switch(index) {
|
switch(index) {
|
||||||
case UI_MAIN_MENU_INDEX_NEW_GAME:
|
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();
|
testBattleStart();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -8,5 +8,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
uicrop.c
|
uicrop.c
|
||||||
uifullbox.c
|
uifullbox.c
|
||||||
uiloading.c
|
uiloading.c
|
||||||
uiautosave.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -27,7 +27,7 @@ void uiTextboxMiniInit(uitextboxmini_t *mini) {
|
|||||||
void uiTextboxMiniShow(
|
void uiTextboxMiniShow(
|
||||||
uitextboxmini_t *mini,
|
uitextboxmini_t *mini,
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
vec3 position,
|
const vec3 position,
|
||||||
const float_t duration,
|
const float_t duration,
|
||||||
uitextboxminiclosedcallback_t closed,
|
uitextboxminiclosedcallback_t closed,
|
||||||
void *user
|
void *user
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ void uiTextboxMiniInit(uitextboxmini_t *mini);
|
|||||||
void uiTextboxMiniShow(
|
void uiTextboxMiniShow(
|
||||||
uitextboxmini_t *mini,
|
uitextboxmini_t *mini,
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
vec3 position,
|
const vec3 position,
|
||||||
const float_t duration,
|
const float_t duration,
|
||||||
uitextboxminiclosedcallback_t closed,
|
uitextboxminiclosedcallback_t closed,
|
||||||
void *user
|
void *user
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "ui/uielement.h"
|
#include "ui/uielement.h"
|
||||||
|
#include "ui/uielementlist.h"
|
||||||
#include "ui/focus/uifocus.h"
|
#include "ui/focus/uifocus.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
|
|
||||||
@@ -64,3 +65,12 @@ errorret_t uiDispose(void) {
|
|||||||
}
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t uiUpdateTranslations(void) {
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementUpdateTranslations(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -39,3 +39,11 @@ errorret_t uiRender(void);
|
|||||||
* @return Any error that occurs.
|
* @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
@@ -8,154 +8,14 @@
|
|||||||
#include "uielement.h"
|
#include "uielement.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "ui/frame/uiframe.h"
|
#include "ui/frame/uiframe.h"
|
||||||
#include "ui/debug/uifps.h"
|
|
||||||
#include "engine/engine.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) {
|
bool_t uiElementIsNull(const uielement_t *element) {
|
||||||
return element->init == NULL &&
|
return element->init == NULL &&
|
||||||
element->update == NULL &&
|
element->update == NULL &&
|
||||||
element->draw == NULL &&
|
element->draw == NULL &&
|
||||||
element->dispose == NULL;
|
element->dispose == NULL &&
|
||||||
|
element->updateTranslations == NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiElementInit(uielement_t *element) {
|
errorret_t uiElementInit(uielement_t *element) {
|
||||||
@@ -183,3 +43,11 @@ errorret_t uiElementDispose(uielement_t *element) {
|
|||||||
errorChain(uiFrameDispose());
|
errorChain(uiFrameDispose());
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementUpdateTranslations(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->updateTranslations != NULL) {
|
||||||
|
errorChain(element->updateTranslations());
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
+13
-4
@@ -13,13 +13,12 @@ typedef struct {
|
|||||||
errorret_t (*update)();
|
errorret_t (*update)();
|
||||||
errorret_t (*draw)();
|
errorret_t (*draw)();
|
||||||
errorret_t (*dispose)();
|
errorret_t (*dispose)();
|
||||||
|
errorret_t (*updateTranslations)();
|
||||||
} uielement_t;
|
} uielement_t;
|
||||||
|
|
||||||
extern uielement_t UI_ELEMENTS[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when all four callbacks on the element are NULL,
|
* Returns true when all callbacks on the element are NULL, which marks
|
||||||
* which marks the end of the UI_ELEMENTS array.
|
* the end of the UI_ELEMENTS array.
|
||||||
*
|
*
|
||||||
* @param element The element to test.
|
* @param element The element to test.
|
||||||
* @returns True if the element is the null terminator.
|
* @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.
|
* @return Any error that occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t uiElementDispose(uielement_t *element);
|
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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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[];
|
||||||
@@ -22,6 +22,5 @@ add_subdirectory(input)
|
|||||||
if(DUSK_NETWORK)
|
if(DUSK_NETWORK)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(save)
|
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "save/save.h"
|
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -188,5 +187,5 @@ float_t inputButtonGetValueDolphin(const inputbutton_t button) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
|
float_t inputGetDeadzoneDolphin(const inputbutton_t button) {
|
||||||
return saveGetMeta()->deadzone;
|
return 0.2f;
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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)
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -40,3 +40,11 @@ int32_t systemGetAspectRatioDolphin(void) {
|
|||||||
int32_t systemGetLanguageDolphin(void) {
|
int32_t systemGetLanguageDolphin(void) {
|
||||||
return CONF_GetLanguage();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,5 +47,13 @@ int32_t systemGetAspectRatioDolphin(void);
|
|||||||
*/
|
*/
|
||||||
int32_t systemGetLanguageDolphin(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
|
// There's actually a tonne more things Wii can return, this is it for now
|
||||||
// though.
|
// though.
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
#define systemInitPlatform systemInitDolphin
|
#define systemInitPlatform systemInitDolphin
|
||||||
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeDolphin
|
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeDolphin
|
||||||
|
#define systemGetLocalePlatform systemGetLocaleDolphin
|
||||||
@@ -16,6 +16,5 @@ add_subdirectory(input)
|
|||||||
if(DUSK_NETWORK)
|
if(DUSK_NETWORK)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(save)
|
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "save/save.h"
|
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
#ifdef DUSK_INPUT_GAMEPAD
|
#ifdef DUSK_INPUT_GAMEPAD
|
||||||
@@ -548,5 +547,5 @@ errorret_t inputInitLinux(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return saveGetMeta()->deadzone;
|
return 0.2f;
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
|
|
||||||
@@ -14,3 +14,7 @@ errorret_t systemInitLinux() {
|
|||||||
systemdialogtype_t systemGetActiveDialogTypeLinux() {
|
systemdialogtype_t systemGetActiveDialogTypeLinux() {
|
||||||
return SYSTEM_DIALOG_TYPE_NONE;
|
return SYSTEM_DIALOG_TYPE_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localeinfo_t * systemGetLocaleLinux() {
|
||||||
|
return &LOCALE_DEFAULT;
|
||||||
|
}
|
||||||
@@ -19,3 +19,11 @@ errorret_t systemInitLinux(void);
|
|||||||
* @return Currently open system dialog type.
|
* @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();
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
#define systemInitPlatform systemInitLinux
|
#define systemInitPlatform systemInitLinux
|
||||||
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeLinux
|
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeLinux
|
||||||
|
#define systemGetLocalePlatform systemGetLocaleLinux
|
||||||
@@ -21,6 +21,5 @@ add_subdirectory(log)
|
|||||||
if(DUSK_NETWORK)
|
if(DUSK_NETWORK)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(save)
|
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "save/save.h"
|
|
||||||
|
|
||||||
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||||
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM
|
||||||
@@ -95,5 +94,5 @@ errorret_t inputInitPSP(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return saveGetMeta()->deadzone;
|
return 0.25f;
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
#define systemInitPlatform systemInitPSP
|
#define systemInitPlatform systemInitPSP
|
||||||
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypePSP
|
#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypePSP
|
||||||
|
#define systemGetLocalePlatform systemGetLocalePSP
|
||||||
@@ -62,3 +62,11 @@ int_t systemPSPGetCrossButtonSetting() {
|
|||||||
ret == 1 ? PSP_UTILITY_ACCEPT_CROSS : PSP_UTILITY_ACCEPT_CIRCLE
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,3 +38,11 @@ int_t systemPSPGetLanguage();
|
|||||||
* @return PSP_UTILITY_ACCEPT_CROSS or PSP_UTILITY_ACCEPT_CIRCLE.
|
* @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);
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "save/save.h"
|
|
||||||
|
|
||||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||||
{ .name = "triangle", {
|
{ .name = "triangle", {
|
||||||
@@ -84,5 +83,5 @@ inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||||
return saveGetMeta()->deadzone;
|
return 0.2f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
Reference in New Issue
Block a user