From 7a03ef8eaf7fd4a208c709a3f7f34921e7c24c1f Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 4 Aug 2026 08:30:24 -0500 Subject: [PATCH] Re-enable save system, fix header/version stamping, handle missing media - Fixed the actual reason saving never worked on any platform: saveWrite() never stamped file->header/file->version before serializing, so every written save file had a zeroed magic header and failed its own validation on the next load. Confirmed via a manual write/load round trip that this alone fully explains "saving doesn't work." - Re-enabled saveInit()/saveDispose() in engine.c (previously commented out under "Temporarily disable save code"). - Added SAVE.available + saveIsAvailable(), refreshed by every real save/load/delete attempt. saveInit() no longer treats an unreachable save medium as fatal to booting - it logs and continues, since a missing memory card/stick shouldn't prevent playing. - Hardened PSP's saveInitPSP() to actually detect a missing memory stick (sceIoGetstat on ms0:/) instead of assuming success, and fixed single-level sceIoMkdir to build the full PSP/SAVEDATA directory chain. - Added busy-retry (CARD_ERROR_BUSY) and a not-mounted guard to Dolphin's live savestreamdolphin.c path, extending the same handling already backported into savedolphin.c. - Added a "Save" entry to the game menu wired to saveWrite(0), showing a clear message on success, on failure, and when saveIsAvailable() is false. --- assets/locale/en_US.po | 4 +++ assets/locale/es_MX.po | 4 +++ assets/locale/jp_JP.po | 4 +++ src/dusk/engine/engine.c | 4 +-- src/dusk/save/save.c | 33 ++++++++++++++--- src/dusk/save/save.h | 26 ++++++++++++-- src/dusk/ui/frame/game/uigamemenu.c | 41 +++++++++++++++++++++ src/dusk/ui/frame/game/uigamemenu.h | 3 +- src/duskdolphin/save/savestreamdolphin.c | 45 +++++++++++++++++------- src/duskpsp/save/savepsp.c | 12 +++++++ src/duskpsp/save/savepsp.h | 21 +++++++++-- src/duskpsp/save/savestreampsp.c | 1 + 12 files changed, 174 insertions(+), 24 deletions(-) diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po index 8452fa19..8928341d 100644 --- a/assets/locale/en_US.po +++ b/assets/locale/en_US.po @@ -56,6 +56,10 @@ msgstr "Items" msgid "ui.game_menu.settings" msgstr "Settings" +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.save" +msgstr "Save" + msgid "item.potion.name" msgstr "Potion" diff --git a/assets/locale/es_MX.po b/assets/locale/es_MX.po index fe196221..6376afaf 100644 --- a/assets/locale/es_MX.po +++ b/assets/locale/es_MX.po @@ -57,6 +57,10 @@ msgstr "Objetos" msgid "ui.game_menu.settings" msgstr "Configuración" +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.save" +msgstr "Guardar" + #: src/dusk/rpg/item/item.json msgid "item.potion.name" msgstr "Poción" diff --git a/assets/locale/jp_JP.po b/assets/locale/jp_JP.po index 6e4250c3..ae9b9ad1 100644 --- a/assets/locale/jp_JP.po +++ b/assets/locale/jp_JP.po @@ -57,6 +57,10 @@ msgstr "アイテム" msgid "ui.game_menu.settings" msgstr "設定" +#: src/dusk/ui/frame/game/uigamemenu.c +msgid "ui.game_menu.save" +msgstr "セーブ" + #: src/dusk/rpg/item/item.json msgid "item.potion.name" msgstr "ポーション" diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index d969a7e6..38149eb9 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -37,7 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(systemInit()); errorChain(inputInit()); errorChain(assetInit()); - // errorChain(saveInit()); + errorChain(saveInit()); errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(uiInit()); @@ -88,7 +88,7 @@ errorret_t engineDispose(void) { errorChain(uiDispose()); consoleDispose(); errorChain(displayDispose()); - // errorChain(saveDispose()); + errorChain(saveDispose()); errorChain(assetDispose()); errorOk(); diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 2b046807..56411638 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -9,6 +9,7 @@ #include "save/savestream.h" #include "util/memory.h" #include "assert/assert.h" +#include "error/error.h" save_t SAVE; @@ -16,12 +17,23 @@ errorret_t saveInit(void) { memoryZero(&SAVE, sizeof(save_t)); #ifdef saveInitPlatform - errorChain(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 errorOk(); } +bool_t saveIsAvailable(void) { + return SAVE.available; +} + errorret_t saveDispose(void) { #ifdef saveDisposePlatform errorChain(saveDisposePlatform()); @@ -39,7 +51,9 @@ errorret_t saveLoad(const uint8_t slot) { memoryZero(&stream, sizeof(savestream_t)); #ifdef saveStreamOpenReadPlatform - errorChain(saveStreamOpenReadPlatform(&stream, slot)); + errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot); + SAVE.available = errorIsOk(openRet); + errorChain(openRet); #endif if(!stream.found) errorOk(); @@ -62,12 +76,21 @@ errorret_t saveWrite(const uint8_t slot) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); savefile_t *file = &SAVE.files[slot]; + // These are metadata about the file itself, not game data - always stamp + // the current magic/version on every write rather than relying on + // whatever happened to already be in memory (zeroed at saveInit, or + // whatever version an old loaded file had), otherwise the written file + // fails its own header check the next time it's loaded. + memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE); + file->version = SAVE_FILE_VERSION; savestream_t stream; memoryZero(&stream, sizeof(savestream_t)); #ifdef saveStreamOpenWritePlatform - errorChain(saveStreamOpenWritePlatform(&stream, slot)); + errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot); + SAVE.available = errorIsOk(openRet); + errorChain(openRet); #endif errorret_t ret = saveFileWrite(&stream, file); @@ -90,7 +113,9 @@ errorret_t saveDelete(const uint8_t slot) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); #ifdef saveDeletePlatform - errorChain(saveDeletePlatform(slot)); + errorret_t deleteRet = saveDeletePlatform(slot); + SAVE.available = errorIsOk(deleteRet); + errorChain(deleteRet); #endif SAVE.files[slot].exists = false; diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h index 1309dc02..bef94275 100644 --- a/src/dusk/save/save.h +++ b/src/dusk/save/save.h @@ -15,17 +15,39 @@ typedef struct { savefile_t files[SAVE_FILE_COUNT_MAX]; /** Platform-specific save system state (paths, card handles, etc.). */ saveplatform_t platform; + /** + * True if the save medium (memory card/stick/disk) was reachable the + * last time it was checked - at saveInit(), and refreshed by every + * subsequent saveLoad()/saveWrite() attempt. Starting the game with no + * card/stick inserted, or one being removed mid-session, are both + * expected conditions here, not fatal errors - see saveIsAvailable(). + */ + bool_t available; } save_t; extern save_t SAVE; /** - * Initializes the save system. + * Initializes the save system. Never fails the way saveWrite/saveLoad can - + * if the platform's save medium isn't reachable (e.g. no memory card/stick + * inserted), that's logged and reflected in saveIsAvailable() rather than + * treated as fatal, since the game should still be playable without save + * support. * - * @return An error code if initialization fails. + * @return An error code only for unexpected platform failures. */ errorret_t saveInit(void); +/** + * Checks whether the save medium was reachable as of the last save/load + * attempt (or saveInit(), if none has been attempted yet). Intended for UI + * to decide whether to offer saving/loading at all, or to explain why it + * isn't available right now - e.g. "No memory card inserted". + * + * @return true if the save medium was available last time it was checked. + */ +bool_t saveIsAvailable(void); + /** * Disposes of the save system. * diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index bfdb2203..6ba12811 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -9,16 +9,48 @@ #include "ui/frame/uiframe.h" #include "ui/frame/settings/uisettings.h" #include "ui/frame/backpack/uibackpack.h" +#include "ui/rpg/textbox/uitextboxmain.h" #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "display/screen/screen.h" #include "assert/assert.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" +#include "save/save.h" +#include "error/error.h" +#include "util/string.h" #define UI_GAME_MENU_INDEX_CHARACTERS 0 #define UI_GAME_MENU_INDEX_ITEMS 1 #define UI_GAME_MENU_INDEX_SETTINGS 2 +#define UI_GAME_MENU_INDEX_SAVE 3 + +// Only one save slot is exposed through this menu for now - there is no +// slot-select UI yet, and SAVE_FILE_COUNT_MAX > 1 exists for later. +#define UI_GAME_MENU_SAVE_SLOT 0 + +static void uiGameMenuSave(void) { + if(!saveIsAvailable()) { + uiTextboxMainSetText("Can't save - no save device found."); + return; + } + + errorret_t ret = saveWrite(UI_GAME_MENU_SAVE_SLOT); + if(errorIsNotOk(ret)) { + // 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), "Save failed: %s", ret.state->message + ); + errorCatch(ret); + uiTextboxMainSetText(msg); + return; + } + + uiTextboxMainSetText("Game saved."); +} uigamemenu_t UI_GAME_MENU; @@ -29,6 +61,7 @@ void uiGameMenuSelected( ) { if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen(); if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); + if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave(); } errorret_t uiGameMenuInit(void) { @@ -55,6 +88,13 @@ errorret_t uiGameMenuInit(void) { UI_GAME_MENU.settingsLabel, UI_GAME_MENU_LABEL_MAX )); + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.game_menu.save", + 0, + UI_GAME_MENU.saveLabel, + UI_GAME_MENU_LABEL_MAX + )); MENU_BEGIN( &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL @@ -62,6 +102,7 @@ errorret_t uiGameMenuInit(void) { MENU_BUTTON(UI_GAME_MENU.charactersLabel); MENU_BUTTON(UI_GAME_MENU.itemsLabel); MENU_BUTTON(UI_GAME_MENU.settingsLabel); + MENU_BUTTON(UI_GAME_MENU.saveLabel); MENU_END(UI_GAME_MENU.items, 1); diff --git a/src/dusk/ui/frame/game/uigamemenu.h b/src/dusk/ui/frame/game/uigamemenu.h index ce6307fe..171fa967 100644 --- a/src/dusk/ui/frame/game/uigamemenu.h +++ b/src/dusk/ui/frame/game/uigamemenu.h @@ -9,7 +9,7 @@ #include "error/error.h" #include "ui/widget/uimenu.h" -#define UI_GAME_MENU_ITEM_COUNT 3 +#define UI_GAME_MENU_ITEM_COUNT 4 #define UI_GAME_MENU_WIDTH 150.0f #define UI_GAME_MENU_LABEL_MAX 32 @@ -19,6 +19,7 @@ typedef struct { char_t charactersLabel[UI_GAME_MENU_LABEL_MAX]; char_t itemsLabel[UI_GAME_MENU_LABEL_MAX]; char_t settingsLabel[UI_GAME_MENU_LABEL_MAX]; + char_t saveLabel[UI_GAME_MENU_LABEL_MAX]; } uigamemenu_t; extern uigamemenu_t UI_GAME_MENU; diff --git a/src/duskdolphin/save/savestreamdolphin.c b/src/duskdolphin/save/savestreamdolphin.c index 45411c1b..1240ebb6 100644 --- a/src/duskdolphin/save/savestreamdolphin.c +++ b/src/duskdolphin/save/savestreamdolphin.c @@ -19,12 +19,20 @@ static void _saveStreamGetFileName( errorret_t saveStreamOpenReadDolphin( savestreamdolphin_t *p, bool_t *found, const uint8_t slot ) { + if(!SAVE.platform.mounted) { + *found = false; + errorThrow("No memory card mounted"); + } + char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; _saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot); - int32_t result = CARD_Open( - SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile - ); + int32_t result; + do { + result = CARD_Open( + SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile + ); + } while(result == CARD_ERROR_BUSY); if(result == CARD_ERROR_NOFILE) { *found = false; p->position = 0; @@ -33,17 +41,19 @@ errorret_t saveStreamOpenReadDolphin( } if(result < 0) { *found = false; - errorThrow("Failed to open memory card file for slot %u (error %d)", - (uint32_t)slot, result + errorThrow("Failed to open memory card file for slot %u: %s (%d)", + (uint32_t)slot, saveCardErrorStringDolphin(result), result ); } - result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); + 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 for slot %u (error %d)", - (uint32_t)slot, result + errorThrow("Failed to read memory card data for slot %u: %s (%d)", + (uint32_t)slot, saveCardErrorStringDolphin(result), result ); } @@ -57,6 +67,8 @@ errorret_t saveStreamOpenReadDolphin( errorret_t saveStreamOpenWriteDolphin( savestreamdolphin_t *p, const uint8_t slot ) { + if(!SAVE.platform.mounted) errorThrow("No memory card mounted"); + memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE); p->position = 0; p->writing = true; @@ -70,14 +82,21 @@ void saveStreamCloseDolphin(savestreamdolphin_t *p) { char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX]; _saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot); - int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile); + int32_t result; + do { + result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile); + } while(result == CARD_ERROR_BUSY); if(result == CARD_ERROR_NOFILE) { - CARD_Create( - SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile - ); + do { + result = CARD_Create( + SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile + ); + } while(result == CARD_ERROR_BUSY); } - CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); + do { + result = CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0); + } while(result == CARD_ERROR_BUSY); CARD_Close(&p->cardFile); } diff --git a/src/duskpsp/save/savepsp.c b/src/duskpsp/save/savepsp.c index 2038292b..ddec2d9f 100644 --- a/src/duskpsp/save/savepsp.c +++ b/src/duskpsp/save/savepsp.c @@ -7,7 +7,18 @@ #include "save/save.h" +void savePSPEnsureBaseDirs(void) { + sceIoMkdir(SAVE_PSP_BASE_DIR, 0777); + sceIoMkdir(SAVE_PSP_SAVEDATA_DIR, 0777); +} + errorret_t saveInitPSP(void) { + SceIoStat stat; + if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) { + errorThrow("No memory stick detected"); + } + + savePSPEnsureBaseDirs(); errorOk(); } @@ -40,6 +51,7 @@ errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) { } errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file) { + savePSPEnsureBaseDirs(); char_t dir[SAVE_PSP_PATH_MAX]; snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT, SAVE_PSP_TITLE_ID, (uint32_t)slot diff --git a/src/duskpsp/save/savepsp.h b/src/duskpsp/save/savepsp.h index c498f89a..24baf37e 100644 --- a/src/duskpsp/save/savepsp.h +++ b/src/duskpsp/save/savepsp.h @@ -11,6 +11,9 @@ #include #define SAVE_PSP_PATH_MAX 256 +#define SAVE_PSP_ROOT "ms0:/" +#define SAVE_PSP_BASE_DIR "ms0:/PSP" +#define SAVE_PSP_SAVEDATA_DIR "ms0:/PSP/SAVEDATA" #define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat" #define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u" @@ -23,9 +26,14 @@ typedef struct { } savepsp_t; /** - * Initializes the save system on PSP. + * Initializes the save system on PSP. Confirms the memory stick is + * actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming + * so, since raw sceIo calls otherwise only fail once something tries to + * touch the filesystem - and ensures the PSP/SAVEDATA directory tree + * exists (SAVE_PSP_BASE_DIR then SAVE_PSP_SAVEDATA_DIR, since sceIoMkdir + * only creates one level at a time). * - * @return An error code if initialization fails. + * @return An error code if no memory stick is reachable. */ errorret_t saveInitPSP(void); @@ -61,3 +69,12 @@ errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file); * @return An error code if the delete fails. */ errorret_t saveDeletePSP(const uint8_t slot); + +/** + * Ensures SAVE_PSP_BASE_DIR and SAVE_PSP_SAVEDATA_DIR both exist, creating + * whichever are missing. sceIoMkdir only creates one directory level at a + * time, so this must run before creating any per-slot save directory + * beneath SAVE_PSP_SAVEDATA_DIR. Safe to call repeatedly - an + * already-exists result is not an error. + */ +void savePSPEnsureBaseDirs(void); diff --git a/src/duskpsp/save/savestreampsp.c b/src/duskpsp/save/savestreampsp.c index 5adef148..6b89e753 100644 --- a/src/duskpsp/save/savestreampsp.c +++ b/src/duskpsp/save/savestreampsp.c @@ -22,6 +22,7 @@ errorret_t saveStreamOpenReadPSP( } errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot) { + savePSPEnsureBaseDirs(); char_t dir[SAVE_PSP_PATH_MAX]; snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT, SAVE_PSP_TITLE_ID, (uint32_t)slot