Two independent gap sources, both confirmed fixed on Linux:
1. audiostream.c's Update() used if/else-if, so a loop restart
(clearing BUFFERED) couldn't re-trigger Buffer() until the *next*
frame's Update() noticed - up to one frame of dead air on every
loop, on every platform. Restructured to two sequential ifs so a
loop falls straight through into re-buffering in the same call.
2. audioStreamLinuxIsFinished only reported true once SDL's queue was
completely empty - meaning silence had already started by the time
"empty" could be observed, guaranteeing a gap by construction.
Changed it to report ready-to-refill once the queue drops to a
4096-frame lead margin (matching the SDL device's own internal
buffer size) instead of waiting for zero. SDL_QueueAudio only ever
appends to a FIFO, so refilling that early never causes overlap -
it just means the device's callback never runs dry.
Confirmed looping gaplessly on Linux.
audioStreamSetLooping() sets AUDIO_STREAM_STATE_LOOPING; when a
looping stream finishes, audioStreamUpdate() clears BUFFERED (not
PLAYING) and fires onLoop instead of onEnd, causing the next Update()
to naturally re-trigger the platform Buffer() call on the same
unmodified stream->data/dataSize - full restart from position 0,
with no platform-specific code needed. loopTo (looping to a point
other than the start) isn't honored yet - would need slicing the
buffer, flagged as a follow-up.
Also fixed the shared test tone's frequency (440Hz -> 441Hz): 44100Hz
doesn't divide evenly by 440Hz (100.23 samples/cycle), so the buffer's
last sample didn't exactly match its first - a small discontinuity at
every loop boundary independent of PSP's own click fixes. 441Hz
divides evenly into exactly 100 samples/cycle, closing that gap for
every platform, not just the ones with their own tail-handling.
Verified end-to-end on Linux (5 onLoop firings over ~9s for a 1s
tone, no errors) and confirmed compiling for PSP.
The previous fix (shrinking the final chunk's declared length via
sceAudioSetChannelDataLen) still clicked on real hardware. Reverted
that in favor of two changes that don't rely on that call's
undocumented mid-stream behavior: every call now sends a full,
constant AUDIO_PSP_CHUNK_FRAMES buffer with the tail simply
zero-padded (channel length is never changed after the initial
reserve), and a couple of extra all-silence chunks are fed after the
real audio + fade so the channel keeps being actively driven at zero
for a moment rather than stopping outright, in case some of the click
was the channel/DAC settling rather than a pure sample-domain
discontinuity.
Confirmed fixed on real PSP hardware.
The feeder's final chunk zero-padded up to the full 1024-frame
reserved size (e.g. 956 padding samples for a 68-sample remainder),
jumping straight from whatever amplitude the waveform ended at down
to silence - an audible click. Fixed two ways: fade the last 32 real
frames linearly to zero before any padding begins (also covers the
case where total length is an exact multiple of the chunk size, where
the waveform would otherwise just stop abruptly with no padding at
all), and declare only the real sample count via
sceAudioSetChannelDataLen (64-aligned) for the final chunk instead of
padding out to the full reserved length, shrinking the leftover
padding to under 64 samples. Channel length gets reset to the full
chunk size at the start of each feed pass in case a previous
playback left it shortened.
Verified via PPSSPP: sceAudioSetChannelDataLen(7, 1024) then (7, 128)
for the tone's 68-sample remainder, onEnd still fires once.
Root cause of the residual jitter (correlating with framerate, per
real-hardware testing): this project never overrides
PSP_MAIN_THREAD_PRIORITY, so the main/render thread runs at PSPSDK's
default of 32. A pthread created with default attributes (as
thread_t/threadStartRequest does) runs at priority 60 - numerically
higher, meaning LOWER scheduling priority on PSP's inverted scale.
Under load the feeder thread was structurally guaranteed to lose
scheduling contention to the main thread, starving the hardware
channel's double buffer and producing audible jitter that worsens
exactly when frames get slower - confirmed via disassembling
PSPSDK's precompiled pthread glue (pte_osThreadGetDefaultPriority
returns 60) rather than guessing.
audioStreamPSPThreadFeed now self-prioritizes via
sceKernelChangeThreadPriority(sceKernelGetThreadId(), 18) as the
first thing it does, matching the elevated-priority pattern PSP SDK
samples use for their own timing-sensitive auxiliary threads.
The original implementation reserved a channel sized to the whole
buffer (~44160 samples) and sent it in one sceAudioOutputPannedBlocking
call. Confirmed against the PSP SDK's own samples (pspaudiolib,
the mp3 sample) that this deviates from how PSP audio hardware is
actually meant to be driven: small fixed-size chunks (1024 frames,
matching pspaudiolib's own convention) fed continuously, decoupled
from the render loop. Reproduced as crackling on real hardware.
Now reserves a 1024-frame channel and runs a dedicated thread (the
project's existing thread_t abstraction, already used by asset.c and
already working on PSP via DUSK_THREAD_PTHREAD) that streams the
buffer chunk-by-chunk until exhausted, re-reading volume/pan every
chunk so live changes take effect mid-playback. Finish detection
switched from polling sceAudioGetChannelRestLength (which can't tell
"between chunks" from "actually done" once chunked) to a flag the
feeder thread sets on completion, same shape as the Dolphin voice
callback.
Verified via the dusk-psp toolchain and PPSSPP headless (channel now
reserved at 1024 frames instead of 44160, onEnd still fires once).
Real-hardware crackle-free confirmation pending.
Allocates an ansnd voice per stream and configures/starts it in
single-buffer mode (no continuous re-feed needed yet, matching the
PSP/Linux single-shot approach). Real linear panning from
directionality via left/right voice volume. ansnd has no polling API
for voice state, so finish detection uses a voice_callback (fired
from ansnd's own audio DMA interrupt, not the main thread) that sets
a flag audioStreamDolphinIsFinished() reads.
Links libansnd (new for this project) and wires duskdolphin/audio
into the CMake build for the first time.
Verified via the dusk-dolphin toolchain: both GameCube and Wii
targets compile and link cleanly. Runtime verification in headless
Dolphin was inconclusive - confirmed via an A/B test against a
pre-audio baseline build that a pre-existing MMIO flood (unrelated
to this change, reproduces identically with zero audio code present)
keeps the harness from reaching the game's own steady state before
timing out.
Reserves a hardware channel per stream (64-aligned sample count,
mono/stereo format) and outputs through sceAudioOutputPannedBlocking,
giving PSP real stereo panning from directionality. Enforces the
44100Hz hardware-channel constraint with a clear error instead of
silently mispitching, and drops the shared test tone to 44100Hz to
match. Verified via the dusk-psp toolchain and a headless PPSSPP run
(correct channel reservation, onEnd firing once, no errors).
Cross-platform audiostream/audio API with per-platform hooks for
PSP/Dolphin/Linux. Linux is fully wired end-to-end through SDL2
(device open, volume-mixed queueing, finish detection via
SDL_GetQueuedAudioSize) and verified playing a generated test tone
in engine.c. PSP and Dolphin hooks are stubbed for now.
assetLocaleGetString rewinds and linearly scans/re-decompresses the
whole locale file from byte 0 on every single call, with no caching -
a text-heavy screen can easily make 10+ of these in a row (e.g. opening
the game menu), and on PSP the containing archive is already fully
resident in RAM, so the repeated cost is pure CPU (decompression +
scanning), not I/O.
Adds a fixed 128-entry move-to-front LRU cache keyed by
(messageId, pluralCount), capped at 64/256 bytes per key/value (~40KB
total) so the cost stays bounded no matter how large the game's script
ends up being, rather than caching the whole locale file's text.
The cache is a lazily-allocated pointer on assetlocalefile_t, not
embedded inline - that struct lives inside the assetloaderoutput_t
union shared by every asset type, and all ASSET_ENTRY_COUNT_MAX asset
slots carry that union directly, so embedding it would have sized every
slot up by ~40KB regardless of what asset type occupies it.
Also fixes a bug this surfaced in test_assetlocale.c's own fixture:
locale_teardown zeroed the locale struct directly instead of going
through assetLocaleDispose, which would have leaked the new cache
allocation across tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers save.c (device discovery/orchestration), savedevice.c (the
generic device state machine and platform dispatch), and the Linux
platform backend (path building, availability checks, JSON read/write,
corrupt/missing-file handling), plus saveslot.c/savesettings.c JSON
round-trips. 88 tests across 5 files, all run against the real Linux
filesystem backend sandboxed to a temp $HOME (there's no mockable
platform layer - the hooks are compile-time macros, not function
pointers).
Deliberately locks in two existing behaviors rather than working around
them: saveSaveSettings() is a permanent no-op because nothing anywhere
ever sets SAVE.settingsDirty = true, and saveUpdate() unconditionally
rewrites settings back out the moment a device is found regardless of
that same dirty flag. Both are pre-existing, not introduced here.
Does not cover the SAVE_DEVICE_DATA_RAW blob codec (PSP/GameCube/Wii
only) or GameCube's 2-device fallback chain - neither compiles into the
Linux host test build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test/item/test_inventory.c was disabled via a commented-out
add_subdirectory(item) with no explanation - the actual cause was a stale
"item/inventory.h" include left over from before the rpg/ reorg (real path
is rpg/item/inventory.h). The current inventory.h/.c API it tests hasn't
drifted; only the include path had rotted.
Also fixes a copy-paste bug in test_inventorySort: the "sort by type"
assertions were calling INVENTORY_SORT_BY_ID again instead of
INVENTORY_SORT_BY_TYPE, so inventorySortByType/Reverse had zero real
coverage. Asserts on type grouping only (not the tied FOOD-vs-FOOD order),
since the underlying sort() is qsort and not stable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An async asset load failure crashed the whole game via errorThrow,
while the identical sync failure just logged and continued - a single
missing/corrupted asset could take down the process. assetUpdate now
handles the async error path the same way as sync (invoke onError,
keep running).
Also removes event.h/event.c and assetbatch, which existed only to
support multiple subscribers per asset event but had no real caller
that ever used more than one (assetbatch itself had zero callers
anywhere). Asset entries, uifullbox, and uiloading now use plain
single-callback + user-pointer fields instead of the generic
array-backed event_t.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EBOOT.PBP packing was a POST_BUILD step with no dependency on the asset
pak, so an assets-only rebuild could silently leave a stale dusk.dsk
embedded. cmake/targets/psp.cmake now repacks EBOOT.PBP via a properly
tracked custom command depending on the executable, PARAM.SFO, and
dusk.dsk (and correctly embeds Dusk.prx rather than the raw ELF when
BUILD_PRX is on).
Separately, libzip's zip_source_filep_create (lazy seeked FILE* reads)
proved unreliable on real PSP hardware, corrupting reads of the embedded
PSAR (first EINVAL, then zlib data errors) even though the packaged data
was verified byte-perfect. assetInitPBP now reads the whole PSAR into
memory once and uses zip_source_buffer_create instead. Confirmed working
on real hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- uimainmenu: removed the temporary keyboard test entry now that the
keyboard has a real caller.
- uiselectsave: picking an empty save slot now opens the keyboard to
name it (localized title "Enter game save file name"), writes the
named slot to disk (mirroring the existing delete-confirm write
pattern), and on a write failure opens the new fatal error overlay.
- uikeyboardopen_t gains a title override (NULL keeps the default
"ENTER YOUR TEXT").
- New ui/overlay/uifatalerror: a minimal fullscreen, non-dismissible
overlay (message + QUIT button, back disabled) for unrecoverable
errors - a nicer alternative to a raw assert crash. Draws above
everything else in the UI element list.
- New CUTSCENE_ITEM_TYPE_KEYBOARD cutscene item: opens the keyboard
with an embedded uikeyboardopen_t, blocks the cutscene until it
closes, then caches the entered text via a new general-purpose
cutscenesystem_t.textCache (CUTSCENE_TEXT_CACHE_MAX, with get/set
accessors) mirroring the existing entity/area/text-mini id caching
pattern.
- uikeyboardopen_t gains cancelConfirm/cancelConfirmLabel, mirroring
confirm/confirmLabel: cancelling (CANCEL button or back-with-nothing-
left-to-delete) can now open an "are you sure?" dialog before closing.
- Added a blinking text-entry cursor, rendered as a separate label
positioned at the end of the current text (accounting for newlines);
it now stops at the last character rather than past it once maxLength
is reached, matching uiKeyboardAppendChar's override-last-char
behavior there.
- uibutton_t gains an `active` flag, independent of the focus-driven
`highlighted` state, for a toggled-on indicator that survives focus
moving elsewhere (used by the keyboard's CAPS/SHIFT keys).
- Added several custom icon glyphs to the default font by repurposing
otherwise-blank/unused glyph slots (backtick, pipe, tilde, backslash,
and the DEL trailing tile) - up arrows for Caps Lock/Shift, a return
arrow for newline, a spacebar symbol, a left arrow for backspace, and
a real underscore - replacing the keyboard's old text-abbreviation key
labels ("CL"/"SL"/"NL"/"<") with single-glyph icons.
- Keyboard dialog now fills the whole screen (no dimming backdrop); key
cells use a fixed font-tile-based size instead of scaling to a
percentage of the screen.
- uiMenuFocusSkipBlanks: UP/DOWN now search every non-blank cell strictly
further in the pressed direction and land on the smallest combined
row+column distance (ties favor the smaller column distance), instead
of only scanning straight down the same column. Neither axis wraps
during that search - only once the edge row is reached with nothing
further to search does it fall back to wrapping to the opposite edge.
- Typing a new character while the keyboard's text is already at
maxLength now overrides the last character instead of being ignored.
Cutscene loader: a MODAL item's optionCount byte was read directly
inside an assertTrue() condition. On the PSP release build
(DUSK_ASSERTIONS_FAKED), assertTrue expands to a no-op and never
evaluates its argument, so that read - and the offset advance it was
responsible for - silently never happened, desyncing every item after
it in the stream. Moved the read out to its own statement and replaced
the assert with a real error throw, since this is untrusted file
content.
uifocus: fixed a wraparound bug in uiFocusMoveDirection - moving left/up
from position 0 truncated to uint8_t before the modulo wrap (0-1 => 255,
then 255 % cols), landing on the wrong column instead of the row's last
one. Only ever visible on a grid wider than a handful of columns, which
nothing but the new keyboard has.
Keyboard: merged uikeyboardqwerty into uikeyboard (only one layout
exists, so the split no longer earned its keep) and deleted the unused
numbers/symbols placeholder files. Reshaped the grid to a tighter 11x5
layout with per-mode key tables (unshifted/caps/shift) instead of
computing case transforms, and fixed the reserved NEWLINE/CONFIRM/CANCEL
slot indices to match. Added shift-symbols for digits and -/=, and
uimenu's directional navigation now skips blank filler cells (wrapping
around a row/column as if they weren't there) instead of landing on
them - a generic fix in uimenu.c, not keyboard-specific, since nothing
else uses blank cells.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>