PSP: save through the real sceUtilitySavedata API, not raw file I/O
Rewrote savepsp.c/savestreampsp.c to use sceUtilitySavedataInitStart/ Update/GetStatus/ShutdownStart instead of sceIoOpen/Read/Write, so PSP saves get a proper OS-generated PARAM.SFO (title/savedataTitle/detail) and show up correctly in the native save browser. This dialog spans multiple frames and, per this project's prior experience with the network config dialog, must be pumped non-blocking one step per real engine frame rather than blocked on synchronously - a raw-sceGu blocking loop already froze the app on real hardware for that dialog, since pspGL owns the GU context. So save.h's saveWrite()/saveLoad() are now callback-based (savecallback_t onComplete) instead of returning a result directly, mirroring networkRequestConnection()'s shape, with a new saveUpdate() (wired into engineUpdate()) pumping the active op each frame. Linux/Dolphin behavior is unchanged - their fallback path in save.c still completes synchronously, just via an immediate callback call instead of a direct return. Two real bugs found via PPSSPP testing (not just code review): SAVE/LOAD modes show a confirm screen even for brand-new data, which blocks forever headlessly - switched to AUTOSAVE/AUTOLOAD, which write/read silently and generate the identical PARAM.SFO. And PPSSPP's dialog status goes straight from QUIT to NONE without a separately observable FINISHED in between, which the first version misread as "disappeared without a result" even on a successful save - fixed by tracking whether QUIT was already seen. Confirmed end-to-end in PPSSPP: write, dialog completes, PARAM.SFO + encrypted save.bin appear on the virtual memory stick, and a subsequent load decrypts/deserializes back to the exact original data. Not tested on real PSP hardware.
This commit is contained in:
@@ -62,6 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
|
errorChain(saveUpdate());
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
|
|||||||
+8
-2
@@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
|
|
||||||
|
static void rpgTestSaveComplete(errorret_t result, void *user) {
|
||||||
|
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t rpgInit(void) {
|
errorret_t rpgInit(void) {
|
||||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||||
@@ -54,10 +58,12 @@ errorret_t rpgInit(void) {
|
|||||||
backpackAdd(ITEM_ID_APPLE, 8);
|
backpackAdd(ITEM_ID_APPLE, 8);
|
||||||
|
|
||||||
// TEST: Verify the save system round-trips real game data, not just the
|
// TEST: Verify the save system round-trips real game data, not just the
|
||||||
// header/version. Remove once there's an actual name-entry flow.
|
// header/version. Remove once there's an actual name-entry flow. On PSP
|
||||||
|
// this shows the real native save dialog every boot - expected while
|
||||||
|
// testing that path, not something to ship as-is.
|
||||||
savefile_t *saveFile = saveGet(0);
|
savefile_t *saveFile = saveGet(0);
|
||||||
stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
|
stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
|
||||||
errorCatch(errorPrint(saveWrite(0)));
|
saveWrite(0, rpgTestSaveComplete, NULL);
|
||||||
|
|
||||||
// All Good!
|
// All Good!
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
+41
-15
@@ -41,22 +41,46 @@ errorret_t saveDispose(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoad(const uint8_t slot) {
|
errorret_t saveUpdate(void) {
|
||||||
|
#ifdef savePlatformUpdate
|
||||||
|
errorChain(savePlatformUpdate());
|
||||||
|
#endif
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t saveIsBusy(void) {
|
||||||
|
#ifdef saveIsBusyPlatform
|
||||||
|
return saveIsBusyPlatform();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
savefile_t *file = &SAVE.files[slot];
|
savefile_t *file = &SAVE.files[slot];
|
||||||
file->exists = false;
|
file->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.
|
||||||
|
#ifdef saveAsyncLoadPlatform
|
||||||
|
saveAsyncLoadPlatform(slot, onComplete, user);
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
savestream_t stream;
|
savestream_t stream;
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
#ifdef saveStreamOpenReadPlatform
|
#ifdef saveStreamOpenReadPlatform
|
||||||
errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot);
|
errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot);
|
||||||
SAVE.available = errorIsOk(openRet);
|
SAVE.available = errorIsOk(openRet);
|
||||||
errorChain(openRet);
|
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(!stream.found) errorOk();
|
if(!stream.found) { onComplete(errorOkImpl(), user); return; }
|
||||||
|
|
||||||
errorret_t ret = saveFileLoad(&stream, file);
|
errorret_t ret = saveFileLoad(&stream, file);
|
||||||
|
|
||||||
@@ -64,16 +88,14 @@ errorret_t saveLoad(const uint8_t slot) {
|
|||||||
saveStreamClosePlatform(&stream);
|
saveStreamClosePlatform(&stream);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(errorIsNotOk(ret)) return ret;
|
if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
|
||||||
|
file->exists = errorIsOk(ret);
|
||||||
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
|
onComplete(ret, user);
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveWrite(const uint8_t slot) {
|
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||||
|
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||||
|
|
||||||
savefile_t *file = &SAVE.files[slot];
|
savefile_t *file = &SAVE.files[slot];
|
||||||
// These are metadata about the file itself, not game data - always stamp
|
// These are metadata about the file itself, not game data - always stamp
|
||||||
@@ -84,13 +106,19 @@ errorret_t saveWrite(const uint8_t slot) {
|
|||||||
memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE);
|
memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE);
|
||||||
file->version = SAVE_FILE_VERSION;
|
file->version = SAVE_FILE_VERSION;
|
||||||
|
|
||||||
|
// See saveLoad() - some platforms take over and complete later.
|
||||||
|
#ifdef saveAsyncWritePlatform
|
||||||
|
saveAsyncWritePlatform(slot, onComplete, user);
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
savestream_t stream;
|
savestream_t stream;
|
||||||
memoryZero(&stream, sizeof(savestream_t));
|
memoryZero(&stream, sizeof(savestream_t));
|
||||||
|
|
||||||
#ifdef saveStreamOpenWritePlatform
|
#ifdef saveStreamOpenWritePlatform
|
||||||
errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot);
|
errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot);
|
||||||
SAVE.available = errorIsOk(openRet);
|
SAVE.available = errorIsOk(openRet);
|
||||||
errorChain(openRet);
|
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
errorret_t ret = saveFileWrite(&stream, file);
|
errorret_t ret = saveFileWrite(&stream, file);
|
||||||
@@ -103,10 +131,8 @@ errorret_t saveWrite(const uint8_t slot) {
|
|||||||
saveStreamClosePlatform(&stream);
|
saveStreamClosePlatform(&stream);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(errorIsNotOk(ret)) return ret;
|
file->exists = errorIsOk(ret);
|
||||||
|
onComplete(ret, user);
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveDelete(const uint8_t slot) {
|
errorret_t saveDelete(const uint8_t slot) {
|
||||||
|
|||||||
+43
-7
@@ -23,6 +23,13 @@ typedef struct {
|
|||||||
* expected conditions here, not fatal errors - see saveIsAvailable().
|
* expected conditions here, not fatal errors - see saveIsAvailable().
|
||||||
*/
|
*/
|
||||||
bool_t available;
|
bool_t available;
|
||||||
|
/**
|
||||||
|
* 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;
|
} save_t;
|
||||||
|
|
||||||
extern save_t SAVE;
|
extern save_t SAVE;
|
||||||
@@ -56,20 +63,49 @@ bool_t saveIsAvailable(void);
|
|||||||
errorret_t saveDispose(void);
|
errorret_t saveDispose(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the save file for a given slot from persistent storage.
|
* Updates the save manager, pumping any in-progress async save/load and
|
||||||
|
* dispatching its callback once complete. No-op on platforms where
|
||||||
|
* saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()).
|
||||||
|
* Must be called every engine frame for platforms that need it (PSP's
|
||||||
|
* native save dialog spans multiple frames).
|
||||||
*
|
*
|
||||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
* @return An error code indicating success or failure.
|
||||||
* @return An error code if the load fails.
|
|
||||||
*/
|
*/
|
||||||
errorret_t saveLoad(const uint8_t slot);
|
errorret_t saveUpdate(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the save file for a given slot to persistent storage.
|
* True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's
|
||||||
|
* native save dialog is open). Calling saveWrite()/saveLoad() 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 file for a given slot 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_FILE_COUNT_MAX - 1).
|
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||||
* @return An error code if the write fails.
|
* @param onComplete Callback invoked with the result once loading finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
*/
|
*/
|
||||||
errorret_t saveWrite(const uint8_t slot);
|
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the save file for a given slot 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_FILE_COUNT_MAX - 1).
|
||||||
|
* @param onComplete Callback invoked with the result once writing finishes.
|
||||||
|
* @param user User data passed through to onComplete.
|
||||||
|
*/
|
||||||
|
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the save file for a given slot from persistent storage.
|
* Deletes the save file for a given slot from persistent storage.
|
||||||
|
|||||||
@@ -33,3 +33,14 @@ typedef struct {
|
|||||||
/** The player's saved name. */
|
/** The player's saved name. */
|
||||||
char_t playerName[SAVE_PLAYER_NAME_MAX];
|
char_t playerName[SAVE_PLAYER_NAME_MAX];
|
||||||
} savefile_t;
|
} savefile_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when an async saveWrite()/saveLoad() 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);
|
||||||
|
|||||||
@@ -29,22 +29,16 @@
|
|||||||
// slot-select UI yet, and SAVE_FILE_COUNT_MAX > 1 exists for later.
|
// slot-select UI yet, and SAVE_FILE_COUNT_MAX > 1 exists for later.
|
||||||
#define UI_GAME_MENU_SAVE_SLOT 0
|
#define UI_GAME_MENU_SAVE_SLOT 0
|
||||||
|
|
||||||
static void uiGameMenuSave(void) {
|
static void uiGameMenuSaveComplete(errorret_t result, void *user) {
|
||||||
if(!saveIsAvailable()) {
|
if(errorIsNotOk(result)) {
|
||||||
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
|
// Generously sized - stringFormat asserts (crashes) rather than
|
||||||
// truncating if the message doesn't fit, so this must comfortably fit
|
// truncating if the message doesn't fit, so this must comfortably fit
|
||||||
// the longest platform save-error message plus this prefix.
|
// the longest platform save-error message plus this prefix.
|
||||||
char_t msg[256];
|
char_t msg[256];
|
||||||
stringFormat(
|
stringFormat(
|
||||||
msg, sizeof(msg), "Save failed: %s", ret.state->message
|
msg, sizeof(msg), "Save failed: %s", result.state->message
|
||||||
);
|
);
|
||||||
errorCatch(ret);
|
errorCatch(result);
|
||||||
uiTextboxMainSetText(msg);
|
uiTextboxMainSetText(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,6 +46,16 @@ static void uiGameMenuSave(void) {
|
|||||||
uiTextboxMainSetText("Game saved.");
|
uiTextboxMainSetText("Game saved.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void uiGameMenuSave(void) {
|
||||||
|
if(!saveIsAvailable()) {
|
||||||
|
uiTextboxMainSetText("Can't save - no save device found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
|
||||||
|
|
||||||
|
saveWrite(UI_GAME_MENU_SAVE_SLOT, uiGameMenuSaveComplete, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
uigamemenu_t UI_GAME_MENU;
|
uigamemenu_t UI_GAME_MENU;
|
||||||
|
|
||||||
void uiGameMenuSelected(
|
void uiGameMenuSelected(
|
||||||
|
|||||||
@@ -16,15 +16,22 @@ typedef savestreampsp_t saveplatformstream_t;
|
|||||||
#define saveDisposePlatform saveDisposePSP
|
#define saveDisposePlatform saveDisposePSP
|
||||||
#define saveDeletePlatform saveDeletePSP
|
#define saveDeletePlatform saveDeletePSP
|
||||||
|
|
||||||
#define saveStreamOpenReadPlatform(stream, slot) \
|
|
||||||
saveStreamOpenReadPSP(&(stream)->platform, &(stream)->found, slot)
|
|
||||||
#define saveStreamOpenWritePlatform(stream, slot) \
|
|
||||||
saveStreamOpenWritePSP(&(stream)->platform, slot)
|
|
||||||
#define saveStreamClosePlatform(stream) \
|
|
||||||
saveStreamClosePSP(&(stream)->platform)
|
|
||||||
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
#define saveStreamReadBytesPlatform(stream, buf, len) \
|
||||||
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
|
||||||
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
#define saveStreamWriteBytesPlatform(stream, buf, len) \
|
||||||
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
|
||||||
#define saveStreamSeekPlatform(stream, pos) \
|
#define saveStreamSeekPlatform(stream, pos) \
|
||||||
saveStreamSeekPSP(&(stream)->platform, pos)
|
saveStreamSeekPSP(&(stream)->platform, pos)
|
||||||
|
|
||||||
|
// 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 saveWrite()/
|
||||||
|
// saveLoad() actually call on this platform.
|
||||||
|
#define saveAsyncWritePlatform(slot, onComplete, user) \
|
||||||
|
savePSPBeginSave(slot, onComplete, user)
|
||||||
|
#define saveAsyncLoadPlatform(slot, onComplete, user) \
|
||||||
|
savePSPBeginLoad(slot, onComplete, user)
|
||||||
|
#define saveIsBusyPlatform() savePSPIsBusy()
|
||||||
|
#define savePlatformUpdate() savePSPUpdate()
|
||||||
|
|||||||
+256
-59
@@ -6,10 +6,31 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "save/save.h"
|
#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"
|
||||||
|
|
||||||
void savePSPEnsureBaseDirs(void) {
|
static void savePSPParamCommonInit(SceUtilitySavedataParam *param) {
|
||||||
sceIoMkdir(SAVE_PSP_BASE_DIR, 0777);
|
memoryZero(param, sizeof(SceUtilitySavedataParam));
|
||||||
sceIoMkdir(SAVE_PSP_SAVEDATA_DIR, 0777);
|
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) {
|
errorret_t saveInitPSP(void) {
|
||||||
@@ -17,8 +38,6 @@ errorret_t saveInitPSP(void) {
|
|||||||
if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) {
|
if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) {
|
||||||
errorThrow("No memory stick detected");
|
errorThrow("No memory stick detected");
|
||||||
}
|
}
|
||||||
|
|
||||||
savePSPEnsureBaseDirs();
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,62 +45,11 @@ errorret_t saveDisposePSP(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) {
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
|
||||||
if(fd < 0) {
|
|
||||||
file->exists = false;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t read = sceIoRead(fd, file, sizeof(savefile_t));
|
|
||||||
sceIoClose(fd);
|
|
||||||
|
|
||||||
if(read != (int32_t)sizeof(savefile_t)) {
|
|
||||||
file->exists = false;
|
|
||||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
file->exists = true;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
sceIoMkdir(dir, 0777);
|
|
||||||
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
SceUID fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
|
||||||
if(fd < 0) {
|
|
||||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t written = sceIoWrite(fd, file, sizeof(savefile_t));
|
|
||||||
sceIoClose(fd);
|
|
||||||
|
|
||||||
if(written != (int32_t)sizeof(savefile_t)) {
|
|
||||||
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveDeletePSP(const uint8_t slot) {
|
errorret_t saveDeletePSP(const uint8_t slot) {
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
char_t path[SAVE_PSP_PATH_MAX];
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
stringFormat(
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME,
|
||||||
|
(uint32_t)slot
|
||||||
);
|
);
|
||||||
|
|
||||||
int32_t result = sceIoRemove(path);
|
int32_t result = sceIoRemove(path);
|
||||||
@@ -89,5 +57,234 @@ errorret_t saveDeletePSP(const uint8_t slot) {
|
|||||||
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
|
errorThrow("Failed to delete save file 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");
|
||||||
|
|
||||||
|
savefile_t *file = &SAVE.files[slot];
|
||||||
|
|
||||||
|
// Serialize 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 = saveFileWrite(&stream, file);
|
||||||
|
if(errorIsOk(ret)) ret = saveStreamFinalizeWriteImpl(&stream);
|
||||||
|
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, file->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 for this slot 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 a slot the player has never saved to.
|
||||||
|
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) {
|
||||||
|
savefile_t *file = &SAVE.files[slot];
|
||||||
|
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 = saveFileLoad(&stream, file);
|
||||||
|
if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
|
||||||
|
file->exists = errorIsOk(ret);
|
||||||
|
cb(ret, user);
|
||||||
|
} else {
|
||||||
|
SAVE.files[slot].exists = true;
|
||||||
|
cb(errorOkImpl(), user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorThrow("Unknown savedata dialog status: %d", status);
|
||||||
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-36
@@ -9,29 +9,50 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "save/savefile.h"
|
#include "save/savefile.h"
|
||||||
#include <pspiofilemgr.h>
|
#include <pspiofilemgr.h>
|
||||||
|
#include <psputility.h>
|
||||||
|
|
||||||
#define SAVE_PSP_PATH_MAX 256
|
#define SAVE_PSP_PATH_MAX 256
|
||||||
#define SAVE_PSP_ROOT "ms0:/"
|
#define SAVE_PSP_ROOT "ms0:/"
|
||||||
#define SAVE_PSP_BASE_DIR "ms0:/PSP"
|
#define SAVE_PSP_FILE_NAME "save.bin"
|
||||||
#define SAVE_PSP_SAVEDATA_DIR "ms0:/PSP/SAVEDATA"
|
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/" SAVE_PSP_FILE_NAME
|
||||||
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat"
|
#define SAVE_PSP_DATA_BUFFER_SIZE 4096
|
||||||
#define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u"
|
|
||||||
|
|
||||||
#ifndef SAVE_PSP_TITLE_ID
|
#ifndef SAVE_PSP_GAME_NAME
|
||||||
#define SAVE_PSP_TITLE_ID "DUSK00001"
|
#define SAVE_PSP_GAME_NAME "DUSK00001"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SAVE_PSP_OP_NONE,
|
||||||
|
SAVE_PSP_OP_SAVE,
|
||||||
|
SAVE_PSP_OP_LOAD
|
||||||
|
} savepspop_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t unused;
|
SceUtilitySavedataParam param;
|
||||||
|
// Raw buffer sceUtilitySavedata reads/writes the whole save into/from -
|
||||||
|
// populated by our own savestream_t serialization (see savestreampsp.h)
|
||||||
|
// before a save starts, and deserialized from after a load finishes.
|
||||||
|
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;
|
} savepsp_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the save system on PSP. Confirms the memory stick is
|
* Initializes the save system on PSP. Confirms the memory stick is
|
||||||
* actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming
|
* actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming
|
||||||
* so, since raw sceIo calls otherwise only fail once something tries to
|
* so, since the savedata dialog otherwise only reports failure once a
|
||||||
* touch the filesystem - and ensures the PSP/SAVEDATA directory tree
|
* save/load is actually attempted.
|
||||||
* exists (SAVE_PSP_BASE_DIR then SAVE_PSP_SAVEDATA_DIR, since sceIoMkdir
|
|
||||||
* only creates one level at a time).
|
|
||||||
*
|
*
|
||||||
* @return An error code if no memory stick is reachable.
|
* @return An error code if no memory stick is reachable.
|
||||||
*/
|
*/
|
||||||
@@ -45,25 +66,7 @@ errorret_t saveInitPSP(void);
|
|||||||
errorret_t saveDisposePSP(void);
|
errorret_t saveDisposePSP(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a save file from PSP save data for the given slot.
|
* Deletes the save data folder for the given slot from the memory stick.
|
||||||
*
|
|
||||||
* @param slot The save slot index.
|
|
||||||
* @param file Output save file data.
|
|
||||||
* @return An error code if the load fails.
|
|
||||||
*/
|
|
||||||
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a save file to PSP save data for the given slot.
|
|
||||||
*
|
|
||||||
* @param slot The save slot index.
|
|
||||||
* @param file Save file data to write.
|
|
||||||
* @return An error code if the write fails.
|
|
||||||
*/
|
|
||||||
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the save file for the given slot from PSP save data.
|
|
||||||
*
|
*
|
||||||
* @param slot The save slot index.
|
* @param slot The save slot index.
|
||||||
* @return An error code if the delete fails.
|
* @return An error code if the delete fails.
|
||||||
@@ -71,10 +74,54 @@ errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file);
|
|||||||
errorret_t saveDeletePSP(const uint8_t slot);
|
errorret_t saveDeletePSP(const uint8_t slot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures SAVE_PSP_BASE_DIR and SAVE_PSP_SAVEDATA_DIR both exist, creating
|
* Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE -
|
||||||
* whichever are missing. sceIoMkdir only creates one directory level at a
|
* writes silently with just a brief icon flash, no confirm screen, since
|
||||||
* time, so this must run before creating any per-slot save directory
|
* SAVE mode shows one even for a slot with no existing data - but
|
||||||
* beneath SAVE_PSP_SAVEDATA_DIR. Safe to call repeatedly - an
|
* PARAM.SFO/title/description are generated identically regardless of
|
||||||
* already-exists result is not an error.
|
* mode, and the OS handles the save browser entry either way) for the
|
||||||
|
* given slot. Serializes SAVE.files[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 for this slot, 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 savePSPEnsureBaseDirs(void);
|
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) for the given
|
||||||
|
* slot, unless a quick sceIoGetstat check finds no save data for this slot
|
||||||
|
* yet - in which case onComplete is invoked immediately with
|
||||||
|
* SAVE.files[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);
|
||||||
|
|||||||
@@ -7,72 +7,35 @@
|
|||||||
|
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/savestreampsp.h"
|
#include "save/savestreampsp.h"
|
||||||
|
#include "util/memory.h"
|
||||||
errorret_t saveStreamOpenReadPSP(
|
|
||||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
|
||||||
) {
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
p->fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
|
||||||
*found = (p->fd >= 0);
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
sceIoMkdir(dir, 0777);
|
|
||||||
|
|
||||||
char_t path[SAVE_PSP_PATH_MAX];
|
|
||||||
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
|
|
||||||
SAVE_PSP_TITLE_ID, (uint32_t)slot
|
|
||||||
);
|
|
||||||
|
|
||||||
p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
|
|
||||||
if(p->fd < 0) {
|
|
||||||
errorThrow(
|
|
||||||
"Failed to open PSP save file for writing: slot %u", (uint32_t)slot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveStreamClosePSP(savestreampsp_t *p) {
|
|
||||||
if(p->fd >= 0) {
|
|
||||||
sceIoClose(p->fd);
|
|
||||||
p->fd = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t saveStreamReadBytesPSP(
|
errorret_t saveStreamReadBytesPSP(
|
||||||
savestreampsp_t *p, void *buf, const size_t len
|
savestreampsp_t *p, void *buf, const size_t len
|
||||||
) {
|
) {
|
||||||
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
|
if(p->position + len > p->length) {
|
||||||
if(read != (int32_t)len) {
|
errorThrow("Save stream read exceeds buffer length");
|
||||||
errorThrow("Unexpected end of PSP save file");
|
|
||||||
}
|
}
|
||||||
|
memoryCopy(buf, p->buffer + p->position, len);
|
||||||
|
p->position += len;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamWriteBytesPSP(
|
errorret_t saveStreamWriteBytesPSP(
|
||||||
savestreampsp_t *p, const void *buf, const size_t len
|
savestreampsp_t *p, const void *buf, const size_t len
|
||||||
) {
|
) {
|
||||||
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
|
if(p->position + len > p->bufferSize) {
|
||||||
if(written != (int32_t)len) {
|
errorThrow("Save stream write exceeds buffer size");
|
||||||
errorThrow("Failed to write PSP save data");
|
|
||||||
}
|
}
|
||||||
|
memoryCopy(p->buffer + p->position, buf, len);
|
||||||
|
p->position += len;
|
||||||
|
if(p->position > p->length) p->length = p->position;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
|
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
|
||||||
if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) {
|
if(pos > p->bufferSize) {
|
||||||
errorThrow("Failed to seek in PSP save file");
|
errorThrow("Save stream seek out of range");
|
||||||
}
|
}
|
||||||
|
p->position = pos;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,71 +7,49 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include <pspiofilemgr.h>
|
|
||||||
#include <stddef.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 {
|
typedef struct {
|
||||||
SceUID fd;
|
uint8_t *buffer;
|
||||||
|
size_t bufferSize;
|
||||||
|
size_t position;
|
||||||
|
size_t length;
|
||||||
} savestreampsp_t;
|
} savestreampsp_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a PSP save data file for reading.
|
* Copies len bytes from the buffer at the current position into buf.
|
||||||
*
|
|
||||||
* @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 saveStreamOpenReadPSP(
|
|
||||||
savestreampsp_t *p, bool_t *found, const uint8_t slot
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a PSP 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 saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the file descriptor held by the stream.
|
|
||||||
*
|
|
||||||
* @param p Stream to close.
|
|
||||||
*/
|
|
||||||
void saveStreamClosePSP(savestreampsp_t *p);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads len bytes from the stream into buf.
|
|
||||||
*
|
*
|
||||||
* @param p Active stream.
|
* @param p Active stream.
|
||||||
* @param buf Destination buffer.
|
* @param buf Destination buffer.
|
||||||
* @param len Number of bytes to read.
|
* @param len Number of bytes to read.
|
||||||
* @return An error if fewer than len bytes are available.
|
* @return An error if the read would exceed the populated data length.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamReadBytesPSP(
|
errorret_t saveStreamReadBytesPSP(
|
||||||
savestreampsp_t *p, void *buf, const size_t len
|
savestreampsp_t *p, void *buf, const size_t len
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes len bytes from buf into the stream.
|
* 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 p Active stream.
|
||||||
* @param buf Source buffer.
|
* @param buf Source buffer.
|
||||||
* @param len Number of bytes to write.
|
* @param len Number of bytes to write.
|
||||||
* @return An error if the write fails.
|
* @return An error if the write would exceed bufferSize.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamWriteBytesPSP(
|
errorret_t saveStreamWriteBytesPSP(
|
||||||
savestreampsp_t *p, const void *buf, const size_t len
|
savestreampsp_t *p, const void *buf, const size_t len
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to an absolute byte position within the stream.
|
* Sets the current read/write position within the buffer.
|
||||||
*
|
*
|
||||||
* @param p Active stream.
|
* @param p Active stream.
|
||||||
* @param pos Target byte offset from the start of the file.
|
* @param pos Target byte offset from the start of the buffer.
|
||||||
* @return An error if the seek fails.
|
* @return An error if pos is out of range.
|
||||||
*/
|
*/
|
||||||
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
|
||||||
|
|||||||
Reference in New Issue
Block a user