Files
dusk/src/dusk/ui/frame/game/uigamemenu.c
T
YourWishes 9abf8101da 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.
2026-08-04 09:54:45 -05:00

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();
}