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.
This commit is contained in:
2026-08-04 08:30:24 -05:00
parent f3ea507313
commit 7a03ef8eaf
12 changed files with 174 additions and 24 deletions
+4
View File
@@ -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"
+4
View File
@@ -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"
+4
View File
@@ -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 "ポーション"
+2 -2
View File
@@ -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();
+29 -4
View File
@@ -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;
+24 -2
View File
@@ -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.
*
+41
View File
@@ -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);
+2 -1
View File
@@ -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;
+32 -13
View File
@@ -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);
}
+12
View File
@@ -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
+19 -2
View File
@@ -11,6 +11,9 @@
#include <pspiofilemgr.h>
#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);
+1
View File
@@ -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