9abf8101da
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.
152 lines
3.9 KiB
C
152 lines
3.9 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "uigamemenu.h"
|
|
#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 uiGameMenuSaveComplete(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), "Save failed: %s", result.state->message
|
|
);
|
|
errorCatch(result);
|
|
uiTextboxMainSetText(msg);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
void uiGameMenuSelected(
|
|
const uimenu_t *menu,
|
|
const uint8_t index,
|
|
const uimenuitem_t *item
|
|
) {
|
|
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
|
|
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
|
|
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
|
|
}
|
|
|
|
errorret_t uiGameMenuInit(void) {
|
|
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
|
|
|
|
errorChain(assetLocaleGetString(
|
|
&LOCALE.entry->data.locale,
|
|
"ui.game_menu.characters",
|
|
0,
|
|
UI_GAME_MENU.charactersLabel,
|
|
UI_GAME_MENU_LABEL_MAX
|
|
));
|
|
errorChain(assetLocaleGetString(
|
|
&LOCALE.entry->data.locale,
|
|
"ui.game_menu.items",
|
|
0,
|
|
UI_GAME_MENU.itemsLabel,
|
|
UI_GAME_MENU_LABEL_MAX
|
|
));
|
|
errorChain(assetLocaleGetString(
|
|
&LOCALE.entry->data.locale,
|
|
"ui.game_menu.settings",
|
|
0,
|
|
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
|
|
);
|
|
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);
|
|
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t uiGameMenuDraw(void) {
|
|
if(!uiMenuIsActive(&UI_GAME_MENU.menu)) errorOk();
|
|
|
|
const float_t width = UI_GAME_MENU_WIDTH;
|
|
const float_t height = (float_t)SCREEN.scanHeight;
|
|
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
|
|
const float_t y = (float_t)SCREEN.scanY;
|
|
|
|
errorChain(uiFrameDraw(x, y, width, height));
|
|
errorChain(uiMenuDraw(
|
|
&UI_GAME_MENU.menu,
|
|
x + UI_FRAME_START_X,
|
|
y + UI_FRAME_START_Y,
|
|
width - (UI_FRAME_START_X * 2),
|
|
height - (UI_FRAME_START_Y * 2)
|
|
));
|
|
|
|
errorChain(spriteBatchFlush());
|
|
errorOk();
|
|
}
|
|
|
|
bool_t uiGameMenuIsOpen(void) {
|
|
return uiMenuIsActive(&UI_GAME_MENU.menu);
|
|
}
|
|
|
|
void uiGameMenuOpen() {
|
|
uiMenuOpen(&UI_GAME_MENU.menu);
|
|
}
|
|
|
|
void uiGameMenuClose() {
|
|
uiMenuClose(&UI_GAME_MENU.menu);
|
|
}
|
|
|
|
errorret_t uiGameMenuDispose(void) {
|
|
errorOk();
|
|
}
|