uikeyboardopen_t grows: maxLength (auto-focuses confirm once full),
lineCount for multi-line entry with a NEWLINE key, trimmed/allowBlank
validation on confirm, and a second "are you sure" uiConfirm step.
Reworks uikeyboardqwerty from a packed flat list into a fixed 14x5 grid
shaped like a real keyboard - number row with -, =, ~ and backspace,
caps and shift (one-shot, XORed with caps for real-keyboard behavior),
and a bottom row with cancel/space/confirm in their normal corners.
Shorter rows are padded with invisible filler cells so every key lands
in its physical position; NEWLINE/CONFIRM/CANCEL are reserved slots
uikeyboard.c patches in conditionally, otherwise left as inert blanks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds out uikeyboard/uikeyboardqwerty from placeholders into a working
controller-navigable text entry dialog: title/current-text labels, a flat
QWERTY key grid (letters, space, backspace), and confirm/cancel buttons in
one combined menu so d-pad navigation flows across all of it.
uikeyboardopen_t configures each dialog: onInput/onKeyPress callbacks,
optional cancel button, maxLength (auto-focuses confirm once full),
lineCount for multi-line entry (adds a NEWLINE key, reserves vertical
space up front), a second "are you sure" uiConfirm step, and
trimmed/allowBlank validation on confirm.
Back now deletes the last character first, falling through to close only
when cancel is allowed and the buffer is empty - this needed a general
cancel-intercept callback added to uifocusitem_t/uimenu_t
(uiMenuSetCancelCallback), since the focus system previously only
supported popping or fully swallowing back.
Wired a temporary "LOAD GAME (TEST)" entry into the main menu with
console-logging test callbacks for interactive testing; marked TODO for
removal once the keyboard is wired up somewhere real.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New ASSET_LOADER_TYPE_CUTSCENE (assetcutsceneloader.c) reads a versioned
binary .cts format decoded into a heap-allocated cutsceneitem_t array + a
string/data pool, both sized to the file's actual content rather than a
fixed capacity, so the shared assetloaderoutput_t union doesn't bloat for
every asset slot regardless of type. Authoring pipeline mirrors the chunk
asset pattern: assetsraw/cutscenes/*.jsonc (JSON plus // and /* */
comments) -> tools/asset/cutscene -> assets/cutscenes/*.cts.
cutsceneSystemSetOnComplete() lets the caller arm a native callback that
fires when a cutscene finishes normally, so a file (which can't store a
function pointer) can end plainly and still hand off to native code -
cutsceneRestart() preserves it across a retry loop rather than clearing it,
since a restart is the same logical run trying again.
The initial and main-menu start-game cutscenes are now loaded from files
instead of compiled in via the CUTSCENE(...) macro.
Fixed a real bug found while converting these: the sync loader read the
file's total size from assetfile_t.size to locate the trailing pool
region, but assetFileDispose() (called at the end of the async phase)
zeroes that whole struct first, so the size was always 0 and the pool
offset computation underflowed into an out-of-bounds read - intermittent
depending on heap layout. Fixed by saving the size before disposal; also
fixed the read-completeness assert being checked after that same zeroing
(a no-op 0 == 0 check).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The load-all-slots/no-device cutscene and its helper callbacks belong to
scene-level game flow, not the uimainmenu UI panel. Moved them into
scenemainmenu.c/h behind a new sceneMainMenuStartGame() entry point;
uimainmenu.c's Start Game handler now just calls that.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uifocusitem_t/uimenu_t gain a disableBack flag that makes back/cancel a
no-op while that item is focused, set via uiMenuSetDisableBack. Applied
centrally in uimodal.c so every modal with option buttons (confirm
dialogs, cutscene modals) must be dismissed by picking an option rather
than backing out, and to the main menu so it no longer bounces back to
the initial scene on cancel - replacing the suppressClosedSceneChange
hack entirely. Updated docs that described the now-unreachable
back/cancel dismissal path. Also adds
cutsceneSystemStartCutsceneAndGoToMarker for starting a cutscene
straight at a given marker instead of its first item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CUTSCENE_SAVE_DEVICE_CHECK and CUTSCENE_SAVE_LOAD_ALL_SLOTS wrap the
existing device-lookup/slot-load calls and jump straight to a
success/failure marker, and CUTSCENE_MODAL_OPTIONS_ONE/TWO do the same
for one/two-option modals, removing the need for a hand-written
goTo-only callback per use. Main menu's "Start Game" now runs a
cutscene (mirroring the initial scene's save-check flow) that loads all
slots before opening the load-game picker, retrying on error; the
initial scene's own device check is migrated onto the same items.
Backing out of the main menu now returns to the initial scene.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uiselectsave is a Save/Load/Delete slot picker built on save/save.h's
SAVE_SLOT_COUNT: a scrolling list of framed uisaveslot_t rows (slot
number + file name, or an "Empty Slot" placeholder), navigated via the
same uimenu_t/focus-stack convention as every other menu. Load and
Delete modes add a trailing action row (switch to delete / cancel)
fixed below the scrollable area. Selecting a slot in Delete mode routes
through uiConfirmOpen before actually blanking the slot via
saveSlotInit + saveDeviceSlotWrite. uiScrollingEnsureVisible in
uiscrolling.c is now a real implementation instead of the earlier
placeholder.
Main menu collapses New Game/Load Game into a single Start Game entry
that opens uiselectsave in Load mode; backing out reopens the main
menu via uiselectsave's result callback.
Also fixes a real reentrancy bug in uiFocusPop: it invoked the popped
item's closed callback before decrementing UI_FOCUS.count, so a push
triggered from within that callback (e.g. reopening the main menu once
select-save reports no result) landed one slot past the true stack top
and got silently dropped, stranding the new item outside navigation.
Count is now decremented first.
Adds a uikeyboard placeholder widget, registered in uielementlist with
no behavior yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uiconfirm no longer duplicates its own backdrop/frame/text/menu drawing
- it now just opens the shared uimodal_t with a Confirm/Cancel option
pair and translates the result back to a bool. uimodal itself gains an
optional title (NULL skips the title row entirely) so a title-less
confirm dialog doesn't reserve blank space.
Main menu's Quit option now opens a confirm dialog instead of exiting
immediately.
Drops es_MX.po/jp_JP.po locale files.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uimodal.c/.h now live in ui/widget alongside the other widgets rather
than ui/frame. The main menu now has a real scene (scene/mainmenu)
whose init just calls the new uiMainMenuOpen(), replacing the
SCENE.current-polling uiMainMenuUpdate with an explicit open call like
uiGameMenuOpen/uiBackpackOpen. Also renames the ui.main_menu.* locale
keys to main_menu.* and groups them under a Main Menu Scene heading in
en_US.po, alongside the existing Initial Scene keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uiModalOpen now resolves title/message/option strings as locale
message IDs first, falling back to the literal string if no match is
found, and word-wraps the result to stay within the screen's scan
width via the new shared textWrap helper. sceneinitial.c's no-device
modal now uses locale keys instead of hardcoded English strings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sceIoGetstat on a bare device root ("ms0:/") returns EINVAL on real
PSP hardware even when a memory stick is present - confirmed via a
diagnostic print during hardware testing. Switched to sceIoDopen/
sceIoDclose, the SDK's own documented way to probe a device root,
which real hardware handles correctly.
Also adds a temporary unconditional settings write once a save device
is selected, to confirm the write path itself works on hardware now
that availability detection is fixed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uimodal gains an onOpen hook and lets uiModalClose take a one-shot
callback, since options are now stored as raw (uncopied) string
pointers instead of a fixed char buffer.
New cutscene items:
- CUTSCENE_MODAL / CUTSCENE_MODAL_OPTIONS / CUTSCENE_MODAL_CLOSE: opens
a message-only or option-driven uimodal. A message-only modal
advances the cutscene immediately; one with options blocks
indefinitely since only its option callback (or something it
triggers) should decide what happens next.
- CUTSCENE_MARKER + cutsceneGoTo: a named, otherwise no-op position
that execution can jump straight to from anywhere in the same
cutscene (e.g. from a CUTSCENE_CALLBACK), matched by name rather
than pointer identity.
- CUTSCENE_RESTART + cutsceneRestart: restarts the running cutscene
from its first item, preserving its interact/interacted entities.
- CUTSCENE_SCENE: requests a scene switch via sceneSet as a cutscene
step.
- CUTSCENE_PRINT: prints a line to the console as a cutscene step.
sceneinitial.c's boot-time save device check is rebuilt on top of
these: show a modal, kick off the async device search, then branch
via markers/goto to a retry/continue prompt depending on the result.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uibutton now embeds a uilabel instead of drawing its text directly,
caching glyph sprites since a button's label never changes after
init. uimenu.c's item pointer in uiMenuDraw drops its unnecessary
const so uiButtonDraw can update the button's cache.
Also fixes remaining #include "ui/frame/uiframe.h" references left
over from uiframe's move to ui/widget/.
uimodal is a new generalized dialog (title + message + up to
UI_MODAL_OPTIONS_MAX option buttons) built the same way uiconfirm is,
but driven by a caller-supplied option list instead of a fixed
confirm/cancel pair.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uiconsole, uifps, and uiplayerpos now render through uilabel instead
of calling textDraw per character every frame. Each owns its label(s)
plus backing text/sprite buffers and marks them dirty only when their
content or position actually changes. Also drops console.h's per-line
buffer from 512 to 128 chars, since the sprite cache backing each
console line label scales with it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
uilabel no longer owns fixed-size text/sprite arrays or copies text
internally - callers pass in their own buffers and write text
directly, then mark the label dirty. This drops SetText/GetText and
the textMax bound in favor of a single Rebuffer entry point, and lets
widgets like uibutton alias an existing (possibly immutable) label
string instead of duplicating it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
textDraw now batches glyphs via a new textBuffer helper instead of
buffering one sprite at a time. Fixes spriteBatchBuffer dropping/
duplicating sprites when a single call spans multiple internal
flush batches, which surfaced as cut-off characters. Adds uilabel,
a widget that caches its glyph sprites and only rebuilds/rebuffers
them when its text or position actually changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Memory sticks/cards don't suit N+1 separate save files the way Linux's
filesystem does, so add an opt-in SAVE_DEVICE_DATA_RAW mode: settings and
all save slots get serialized to JSON, concatenated, zlib-compressed, and
framed with a magic/version/checksum/generation header, then read/written
as a single blob through one combined platform hook instead of four.
- PSP and Dolphin's SD/NAND backends write via a temp file + atomic rename,
so a crash mid-write can never leave a half-written save behind.
- GameCube memory cards have no rename or resize primitive, so they
ping-pong between two fixed files instead, picking whichever is valid and
has the higher generation counter on load.
- Linux is untouched (still four separate JSON files); saveslot.h/
savesettings.h drop their now-unnecessary pack(1) now that nothing
persists them as raw bytes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduces saveslot/savesettings structs with a shared savejson.h macro
toolkit (has/require/write/read per common type) for populating and
parsing yyjson objects, wires saveManager save/load calls through new
saveDevice slot/settings write/read entry points (still stubbed at the
device level), and drops the old binary-format scaffolding in favor of
JSON only.
CARD_Mount() alone reliably reports CARD_ERROR_NOCARD for a card physically
present in slot B - unlike slot A, which the IPL polls automatically at
boot, slot B needs an explicit CARD_Probe() first (the standard pattern in
every official devkitPro CARD sample). Also fixes
saveDeviceDolphinCardHasFreeSpace() treating CARD_GetDirectory()'s
CARD_ERROR_NOFILE (a totally empty card, not an actual error) as a hard
failure, which made any card with zero existing save files - slot B's in
this case, since slot A already had leftover test data on it - get
reported as full.
Confirmed via Dolphin with slot A disabled/slot B set to a memory card:
previously reported "no save device found", now correctly detects it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PSP checks memory-stick reachability via sceIoGetstat. GameCube gets two
devices (CARD_SLOTA/CARD_SLOTB), each mounted via CARD_Init/CARD_Mount and
checked for free space via CARD_GetDirectory/CARD_GetBlockCount. Wii picks
between three storage methods at compile time (DUSK_SAVE_WII_METHOD =
NAND/CARD/SD in wii.cmake, default NAND via ISFS) since real hardware
behavior for the default is unverified.
Also fixes two pre-existing bugs found while build/runtime-testing this
across PPSSPP and Dolphin: wrong libogc language macros in
systemGetLocaleDolphin, and a missing SYS_STDIO_Report(true) call that
silently swallowed all guest console output in Dolphin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes the per-platform save/savestream/autosave implementations, the settings UI frames, and the autosave overlay, replacing them with a savemanager.c/h + savefile.h/savedevice.h scaffold to build the new save system on top of. Also separates the concrete UI_ELEMENTS registration into uielementlist.h/.c so uielement.c only holds the generic per-element lifecycle logic.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Main menu buttons, the confirm dialog, battle menu/HUD text, backpack
tab labels, the loading/autosave indicators, and the settings tab
placeholders were all raw English literals bypassing translation.
Loads each through assetLocaleGetString like the rest of the UI, adding
new Init hooks for uibattlehud/uiautosave/uiloading where none existed,
and adds the corresponding message ids to en_US/es_MX/jp_JP.po.
Also widens the settings placeholder buffers to 64 bytes - the es_MX
and jp_JP translations overflowed the previous 32-byte size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Keeps the temporary hardcoded-battle hook alongside the rest of the
battle module instead of the main menu UI, so the UI frame only
triggers it rather than owning battle setup code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splits the platform-agnostic network core out of dusk into a top-level
dusknetwork module, mirroring the duskgl/dusksdl2 pattern. Adds a
DUSK_NETWORK cmake option (default ON) so builds can exclude all
networking code, including the per-platform implementations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- animationUpdate now advances and resolves boundary crossings for
ANIMATION_FLAG_LOOP, ANIMATION_FLAG_PINGPONG, ANIMATION_FLAG_REVERSE, and
the STOP_BEGINNING/STOP_END flags, firing onLoop/onComplete appropriately;
guards against LOOP+PINGPONG being set together and moves the
duration-must-be-positive check into animationInit
- keyframeGetValue clamps to the last keyframe's value instead of dividing
by zero once time reaches it, and its keyframe walk drops a branch that's
unreachable after that clamp
- Adds test/animation/test_animation.c covering init, per-layer sampling,
and the full animationUpdate flag matrix
- keyframeGetValue now returns the last keyframe's value for times at or
beyond it, fixes a missing util/math.h include, and asserts keyframes are
sorted by time; adds test/animation/test_keyframe.c
- Adds mainmenu scene/UI and a battle HUD UI frame
- Adds save autosave-related fields and battle scene tweaks
- Adds headless test coverage for cutscenes, entities, and map areas
Replace the one-fighter-at-a-time turn model with an OPENING/PRE_ROUND/
PLAYER_SELECTION/AI_SELECTION/MOVES_EXECUTING/POST_ROUND/ENDED state
machine and a per-fighter action queue, so actions are decided before any
of them execute (needed for speed-ordered resolution) and so a cutscene
can pause the battle, wait for a specific state, and force a fighter's
action -- enabling automated, fully-scripted, and partially-scripted
battles. Adds CUTSCENE_PAUSE_BATTLE plus CUTSCENE_BATTLE_WAIT_STATE and
CUTSCENE_BATTLE_FORCE_ACTION cutscene items, and generic onStateChanged/
onActionDecided callbacks on battle_t. Re-enables the long-dormant
test/rpg suite and adds test/rpg/battle covering the state machine and
the new cutscene hooks end-to-end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The village square at the mountain's foot, party roles established
(tank/paladin leader, ranged ranger, support mage, melee rogue), the
funds bicker, and the cowgirl teleporter's too-good-to-question,
one-way-only offer up the mountain - the innocent-sounding line that
seeds the whole chapter's central complication. Adds character stubs
for the three other party members and the teleporter, and confirms
the bubbly ranger as the first to abandon the mission.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduces Strider (a merc who protects those who can't afford real
protection, and has found genuine peace in it), Downes (retired to
farming and family, forced back into one last fight), and Egor (Royal
Guard, honor curdled into blind devotion, dies still certain of
himself). Together with the party leader, they give the same
mentor/same night four distinct answers to what honor means. Updates
themes and open questions accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reveals him as the presence following the party since Chapter 1 - a
king's agent who frees the protagonist from execution in early
Chapter 2, drops a small, cagey lead to the newly-dismissed party
leader, and then vanishes to chase other threads. Documents these
early Chapter 2 beats, notes culinary magic is now branded "the
forbidden magic" per the king's cover story, and updates open
questions/themes accordingly. His entry is intentionally sparse -
the character is meant to stay a mystery for as long as possible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ties the party leader's rigid, orders-above-all sense of honor back
to a mentor he idolized as a young soldier - one who broke orders on
the night of the Great Explosion to keep his squadron out of the
massacre, then died in it. The party leader doesn't know why at the
time; his eventual realization that his mentor's disobedience was
itself the honorable act becomes his arc's throughline. Also notes
the rest of that squadron, now scattered, as a future plot thread.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the vague placeholder with the full account: the king's
personal, in-person purge of the culinary mages' hometown, the failed
hostage surrender, the town's collective final spell (built on a
shared folk song) that becomes the Great Explosion, and the
protagonist's survival via her mother's protective spell. Also adds
the king's official cover story (framing the mages as insurgents),
new characters (the Great Mage and the protagonist's mother), and
updates themes/open questions accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds story.md covering the overarching premise (a prophecy caused by
the attempt to prevent it), the eight-movement story structure, the
Great Explosion backstory, core characters, and a detailed breakdown
of Chapter 1's opening - the bounty party's retrieval of the
protagonist, the mountain teleportation-logistics trap that splits
the party, and the confrontation with the king.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves FONT_DEFAULT and its init/dispose into their own font.h/font.c,
and rebuilds it from a static glyph bit array + a runtime-generated
texture/tileset rather than loading ui/minogram.png/.dtf through the
asset system. The engine now always has a usable font to render with,
even if asset loading fails. The now-unused minogram assets are removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
autoSaveQueue() is a single callable entry point for requesting an
autosave; autoSaveUpdate() pumps the queue each frame and, if the write
fails (e.g. no memory card), forces the existing no-card modal back
open and pauses world simulation until the player retries or accepts a
temporary session. A small overlay shows "SAVING" while a write is in
flight.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the last hardcoded English strings (initial-scene modals, game
menu save status messages) with locale-loaded text, adds a shared
LOCALE_LIST so the settings dropdown and locale manager stay in sync,
and persists the chosen language into savemeta_t across all platforms.
Also fixes two message buffers that were too small for their longest
translations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Entities (and the player's pause-to-open-game-menu handling with them)
previously kept updating every frame regardless of the active scene,
matching an existing TODO in rpgUpdate(). Gating the entity loop itself
means the game menu (and any other entity-driven input) naturally can't
trigger mid-battle or before the initial scene hands off to the
overworld, without needing a scene check at each individual call site.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A new SCENE_TYPE_INITIAL now runs before the overworld: it checks
save-device availability and existing save data, then shows one of two
new dedicated modals - "no save device found" (Retry / Continue Anyway)
or "no save data found, create one?" (Yes / No) - before handing off to
the overworld. Both modals are self-contained UI elements mirroring
uiconfirm.h's shape, registered like any other global UI element.
Choosing "Continue Anyway" marks the session temporary (SAVE.temporary,
folded into saveIsAvailable()) so saving stays disabled for the rest of
the session instead of silently retrying, and the game menu's Save
action now reports that distinctly instead of the generic "no device"
message.
Also removes rpg.c's leftover TEST block (unconditional player-name
stamp + save write on every boot) now that this real flow owns save
creation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renames savefile_t to saveslot_t and folds last session's standalone
settings.h/.c module back in as savemeta_t, so there's one save system
(SAVE.slots[] + SAVE.meta) instead of two parallel ones - while letting
each platform pick its own physical format for the two concepts:
- Linux now writes human-editable JSON (slot0.json, settings.json, ...)
via yyjson's mutable writer API, so players can hand-fix a bad setting.
- PSP folds meta into the same sceUtilitySavedata binary payload as its
one save slot (SAVE_SLOT_COUNT_MAX=1 there - a future save picker will
let players manage multiple named saves via the OS's own browser).
- GameCube consolidates the 3 per-slot memory card files and the separate
settings file into one combined card file.
Also fixes two bugs surfaced while building this: the CRC finalize step
seeked to a hardcoded offset (only safe for one section per file, breaks
once meta+slots share a buffer), and save.c's async/sync dispatch left an
unconditional fallback call that doesn't exist on PSP-only platforms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deadzone (and future prefs like locale) now live in their own
settingsfile_t/settings.c, loaded eagerly at boot and saved immediately
on Apply, instead of inside savefile_t - a setting shouldn't reset or
diverge just because the player is on a different save slot, and this
also fixes settings changes not actually reaching disk until the next
full game Save.
PSP settings use a new plain sceIo path rather than sceUtilitySavedata,
since that dialog would flash its native icon on every settings tweak.
GameCube reuses the save system's existing memory card mount rather than
mounting it twice (settingsInit() now runs after saveInit()).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds three new pieces of save-file state, all following the same shape:
the save file is the single source of truth, not a separate live runtime
copy that gets synced in/out.
- globalitemstore.h/.c: per-global-entity-ID "collected" flags
(savefile_t.globalItemCollected), so a global item entity's init
callback can check whether it was already picked up in a prior session
without needing to keep the entity itself alive to remember that.
- Gamepad deadzone: removed input_t.deadzone entirely. The setting UI and
every platform's actual deadzone-applying code (inputGetDeadzoneDolphin/
SDL2, previously hardcoded per-platform literals that the settings menu
didn't actually affect) now read savefile_t.deadzone directly via
saveGet(SAVE_ACTIVE_SLOT). Default lives in savefile.h
(SAVE_DEADZONE_DEFAULT), stamped onto every slot in saveInit().
- Story flags: STORY_FLAG_VALUES (a live, codegen-initialized array) is
replaced by savefile_t.storyFlags, read/written via the existing
storyFlagGet()/storyFlagSet() call sites (now macros/functions over the
active save file instead of a separate array). tools/story.py now
generates STORY_FLAG_DEFAULTS (const) instead; storyFlagInitDefaults()
stamps those onto a save the first time it's used (file->exists false),
called from rpgInit().
Added SAVE_ACTIVE_SLOT (0) to savefile.h as the one shared "which slot is
actually being played" constant, replacing three different local/implicit
0s (uigamemenu.c, rpg.c, and now the settings/input call sites).
Verified round-trip on Linux (all three together in one save/load cycle);
both Linux and PSP build clean.
uiGameMenuSave() now attempts a real load first (via the existing generic
saveLoad()/saveExists() primitives) instead of writing blind. If a save
already exists, it saves straight over it as before. If not, it prompts
via the existing uiConfirm dialog ("No save data found. Create a new
save?") before writing - this is exactly the flow GameCube needs (no
native OS save browser to lean on, unlike PSP), but implemented generically
so it also applies correctly on every other platform without any
platform-specific UI code: saveIsAvailable()/saveExists() already reflect
each platform's real state (e.g. Dolphin's memory-card presence and
existing-file checks), so the same logic just does the right thing
everywhere.
Verified the two branches directly on Linux (temporarily wiring the same
saveLoad -> check -> uiConfirmOpen sequence into rpgInit): with no save
file, saveExists() is false and the confirm dialog opens; with one already
written, it's true and the confirm dialog is correctly skipped. Not
verified via actual menu navigation (no input-injection tooling available
here) or on Dolphin (no devkitPPC toolchain in this environment).
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.