21 Commits

Author SHA1 Message Date
YourWishes 7a858cc424 Add a real test suite for the save system (previously had none)
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>
2026-08-30 12:33:20 -05:00
YourWishes c0292842a5 Fix stale include re-enabling test/item, fix dead sort-by-type coverage
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>
2026-08-30 12:33:00 -05:00
YourWishes 717902462b Fix fatal async asset load errors, remove unused event system
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>
2026-08-30 10:14:38 -05:00
YourWishes 1d73b9d224 Fix PSP asset loading: stale EBOOT.PBP packing, unreliable zip file reads
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>
2026-08-28 09:31:44 -05:00
YourWishes 51f262efa0 Wire keyboard into save naming, add fatal error overlay, cutscene keyboard item
- 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.
2026-08-28 08:29:07 -05:00
YourWishes a6e4e3f71f Add cancel confirmation, blinking cursor, custom keyboard icon glyphs
- 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.
2026-08-27 22:17:00 -05:00
YourWishes 4387d223b9 Fullscreen keyboard dialog, nearest-cell vertical navigation, overwrite-at-max-length
- 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.
2026-08-27 21:14:19 -05:00
YourWishes 00bfaf6360 Fix cutscene/focus bugs found on PSP hardware, rework keyboard layout
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>
2026-08-27 19:26:07 -05:00
YourWishes 709bd7be52 Add validation options and a full physical layout to the keyboard
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>
2026-08-26 18:44:07 -05:00
YourWishes 80f4348e21 Implement on-screen keyboard dialog with QWERTY layout
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>
2026-08-26 15:26:57 -05:00
YourWishes e630827b34 Add runtime-loaded cutscene files, convert initial/main menu to use them
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>
2026-08-25 09:51:26 -05:00
YourWishes 106d9b0fc0 Move main menu start-game cutscene from ui panel to scene
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>
2026-08-24 20:30:33 -05:00
YourWishes 4a26f79945 Add uimenu disableBack flag, apply to main menu and all modal options
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>
2026-08-23 22:11:27 -05:00
YourWishes 1a199f6ce3 Add declarative cutscene items for save checks and marker-based modals
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>
2026-08-23 20:29:57 -05:00
YourWishes ec59e77867 Moved files around 2026-08-23 10:14:39 -05:00
YourWishes cb28d2b611 Add save slot picker (uiselectsave/uisaveslot), fix focus stack reentrancy
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>
2026-08-21 14:16:39 -05:00
YourWishes 27b6ddf5cb Rebuild uiconfirm on top of uimodal, add quit confirmation
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>
2026-08-20 22:41:06 -05:00
YourWishes 475c865e33 Move uimodal to ui/widget, make main menu its own scene
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>
2026-08-20 19:18:50 -05:00
YourWishes 0b4ba062bb Translate and word-wrap cutscene modal text
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>
2026-08-20 18:53:47 -05:00
YourWishes 28f5e66662 Fix PSP memory stick availability check on real hardware
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>
2026-08-20 17:22:24 -05:00
YourWishes e225a076f0 Add cutscene flow control and a scriptable modal dialog item
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>
2026-08-20 14:35:41 -05:00
130 changed files with 7607 additions and 1911 deletions
Binary file not shown.
Binary file not shown.
+93 -16
View File
@@ -5,6 +5,99 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
# Initial Scene
msgid "initial.checking_save.title"
msgstr "Checking for save data"
msgid "initial.checking_save.message"
msgstr "Please wait..."
msgid "initial.no_device.title"
msgstr "No Save Device Found"
msgid "initial.no_device.message"
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
msgid "initial.no_device.retry"
msgstr "Try again"
msgid "initial.no_device.continue"
msgstr "Continue without saving"
# Main Menu Scene
msgid "main_menu.start_game"
msgstr "Start Game"
msgid "main_menu.options"
msgstr "Options"
msgid "main_menu.quit"
msgstr "Quit Game"
msgid "main_menu.quit_confirm"
msgstr "Are you sure you want to quit?"
msgid "main_menu.checking_save.title"
msgstr "Checking for save data"
msgid "main_menu.checking_save.message"
msgstr "Please wait..."
msgid "main_menu.no_device.title"
msgstr "No Save Device Found"
msgid "main_menu.no_device.message"
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
msgid "main_menu.no_device.retry"
msgstr "Try again"
msgid "main_menu.no_device.continue"
msgstr "Continue without saving"
msgid "main_menu.save_load_error.title"
msgstr "Error"
msgid "main_menu.save_load_error.message"
msgstr "Failed to load save data. Please try again."
msgid "main_menu.save_load_error.retry"
msgstr "Try Again"
# Select Save Screen
msgid "ui.select_save.title"
msgstr "Select Save"
msgid "ui.select_save.empty"
msgstr "Empty Slot"
msgid "ui.select_save.slot_format"
msgstr "%s Lv.%d %s"
msgid "ui.select_save.delete_mode"
msgstr "Delete a Save"
msgid "ui.select_save.delete_confirm"
msgstr "Are you sure you want to delete this save?"
msgid "ui.select_save.name_title"
msgstr "Enter game save file name"
msgid "ui.save_slot.number_format"
msgstr "Slot %d"
#: ui/menu.c:10 #: ui/menu.c:10
msgid "ui.title" msgid "ui.title"
msgstr "" msgstr ""
@@ -115,22 +208,6 @@ msgstr "Yes"
msgid "ui.initial.create_save.no" msgid "ui.initial.create_save.no"
msgstr "No" msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "New Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Load Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Options"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Quit Game"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm" msgid "ui.confirm.confirm"
msgstr "Confirm" msgstr "Confirm"
-194
View File
@@ -1,194 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: es\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Bienvenido"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Entrada"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Pantalla"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Idioma"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "Se aplica después de reiniciar la aplicación."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Personajes"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Objetos"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Configuración"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Guardar"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Partida guardada."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Guardado cancelado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Error al guardar: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "No se puede guardar: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Reintentar"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continuar de todos modos"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Sí"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "Nueva Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Cargar Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Opciones"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Salir del Juego"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirmar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancelar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Atacar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Huir"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemigo %u (%u/%u PS)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "PS %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "PM %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "Aún no hay opciones de audio"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "Aún no hay opciones de pantalla"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "Aún no hay opciones de entrada"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Categoría %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "cargando"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "GUARDANDO"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "Poción"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "Papa"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "Manzana"
-194
View File
@@ -1,194 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=1; plural=(0);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"歓迎"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "一般"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "入力"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "表示"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "オーディオ"
msgid "ui.settings.input.deadzone"
msgstr "デッドゾーン"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "言語"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "アプリケーションを再起動すると適用されます。"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "キャラクター"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "アイテム"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "設定"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "セーブ"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "セーブしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "セーブをキャンセルしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "セーブできません - セーブデバイスが見つかりません。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "セーブに失敗しました: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "セーブできません: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "再試行"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "続行する"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "はい"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "いいえ"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "ニューゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "ロードゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "オプション"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "ゲームを終了"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "確認"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "キャンセル"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "攻撃"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "逃げる"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "敵%u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "オーディオ設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "表示設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "入力設定はまだありません"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "カテゴリー%u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "読み込み中"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "保存中"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "ポーション"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "ジャガイモ"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "リンゴ"
+69
View File
@@ -0,0 +1,69 @@
{
"items": [
// Boot check: make sure a save device is available before handing off
// to the main menu.
{
"type": "MODAL",
"title": "initial.checking_save.title",
"message": "initial.checking_save.message"
},
{
"type": "WAIT",
"seconds": 0.2
},
{
"type": "SAVE_DEVICE_CHECK",
"successMarker": "CONTINUE",
"failureMarker": "NO_DEVICE"
},
// No save device found - offer to retry or continue without saving.
{
"type": "MARKER",
"name": "NO_DEVICE"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "MODAL_OPTIONS_MARKERS",
"title": "initial.no_device.title",
"message": "initial.no_device.message",
"options": [
{
"text": "initial.no_device.retry",
"marker": "RETRY"
},
{
"text": "initial.no_device.continue",
"marker": "CONTINUE"
}
]
},
{
"type": "MARKER",
"name": "RETRY"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "RESTART"
},
// Save device found (or continuing without one) - hand off to the
// main menu scene.
{
"type": "MARKER",
"name": "CONTINUE"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "SCENE",
"sceneType": "MAIN_MENU"
}
]
}
@@ -0,0 +1,92 @@
{
"items": [
{
"type": "MODAL",
"title": "main_menu.checking_save.title",
"message": "main_menu.checking_save.message"
},
{
"type": "WAIT",
"seconds": 0.2
},
{
"type": "SAVE_DEVICE_CHECK",
"successMarker": "CONTINUE",
"failureMarker": "NO_DEVICE"
},
// No save device found - offer to retry or continue without saving.
{
"type": "MARKER",
"name": "NO_DEVICE"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "MODAL_OPTIONS_MARKERS",
"title": "main_menu.no_device.title",
"message": "main_menu.no_device.message",
"options": [
{
"text": "main_menu.no_device.retry",
"marker": "RETRY"
},
{
"text": "main_menu.no_device.continue",
"marker": "CONTINUE"
}
]
},
// Retry
{
"type": "MARKER",
"name": "RETRY"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "RESTART"
},
// Save device found - attempt to load all save slots.
{
"type": "MARKER",
"name": "CONTINUE"
},
{
"type": "MODAL_CLOSE"
},
{
"type": "SAVE_LOAD_ALL_SLOTS",
"successMarker": "LOADED",
"failureMarker": "LOAD_ERROR"
},
// Save data failed to load (e.g. corrupt/unreadable) - only option is
// to retry, no "continue without saving" here since we already know a
// device is present.
{
"type": "MARKER",
"name": "LOAD_ERROR"
},
{
"type": "MODAL_OPTIONS_MARKERS",
"title": "main_menu.save_load_error.title",
"message": "main_menu.save_load_error.message",
"options": [
{
"text": "main_menu.save_load_error.retry",
"marker": "RETRY"
}
]
},
{
"type": "MARKER",
"name": "LOADED"
}
]
}
+46
View File
@@ -65,6 +65,23 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
) )
endif() endif()
# Generate PARAM.SFO as a normal tracked build output (instead of letting
# create_pbp_file() auto-generate + delete it) so it can be reused below by
# a properly dependency-tracked EBOOT.PBP repack step.
set(DUSK_PSP_PARAM_SFO "${DUSK_BUILD_DIR}/PARAM.SFO")
add_custom_command(
OUTPUT "${DUSK_PSP_PARAM_SFO}"
COMMAND "$ENV{PSPDEV}/bin/mksfoex" "-d" "MEMSIZE=1" "-s" "APP_VER=01.00"
"${DUSK_BINARY_TARGET_NAME}" "${DUSK_PSP_PARAM_SFO}"
COMMENT "Generating PARAM.SFO for ${DUSK_BINARY_TARGET_NAME}"
VERBATIM
)
add_custom_target(DuskPspParamSfo DEPENDS "${DUSK_PSP_PARAM_SFO}")
# create_pbp_file()'s own POST_BUILD chain (below) also consumes
# DUSK_PSP_PARAM_SFO, so make sure it exists before that chain runs.
add_dependencies(${DUSK_BINARY_TARGET_NAME} DuskPspParamSfo)
# Postbuild, create .pbp file for PSP. # Postbuild, create .pbp file for PSP.
create_pbp_file( create_pbp_file(
TARGET "${DUSK_BINARY_TARGET_NAME}" TARGET "${DUSK_BINARY_TARGET_NAME}"
@@ -74,4 +91,33 @@ create_pbp_file(
TITLE "${DUSK_BINARY_TARGET_NAME}" TITLE "${DUSK_BINARY_TARGET_NAME}"
PSAR_PATH ${DUSK_ASSETS_ZIP} PSAR_PATH ${DUSK_ASSETS_ZIP}
VERSION 01.00 VERSION 01.00
SFO_PATH "${DUSK_PSP_PARAM_SFO}"
OUTPUT_DIR "${DUSK_BUILD_DIR}"
) )
# CreatePBP.cmake's pack-pbp step is a POST_BUILD command tied to the
# executable target, so it only reruns when the ELF itself relinks. That
# means regenerating dusk.dsk (assets) alone, without touching any C
# source, silently leaves EBOOT.PBP embedding a stale asset pak. Repack it
# here as a normal file-tracked custom command depending on both the
# executable and the asset zip, so EBOOT.PBP always reflects the current
# assets even when nothing else about the build changed.
set(DUSK_PSP_EBOOT "${DUSK_BUILD_DIR}/EBOOT.PBP")
if(BUILD_PRX)
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>.prx")
else()
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>")
endif()
add_custom_command(
OUTPUT "${DUSK_PSP_EBOOT}"
COMMAND "$ENV{PSPDEV}/bin/pack-pbp" "${DUSK_PSP_EBOOT}" "${DUSK_PSP_PARAM_SFO}"
"NULL" "NULL" "NULL" "NULL" "NULL"
"${DUSK_PSP_EXECUTABLE}" "${DUSK_ASSETS_ZIP}"
DEPENDS
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
"${DUSK_PSP_PARAM_SFO}"
"${DUSK_ASSETS_ZIP}"
COMMENT "Repacking EBOOT.PBP (tracks executable + asset pak freshness)"
VERBATIM
)
add_custom_target(DuskPspEbootRepack ALL DEPENDS "${DUSK_PSP_EBOOT}")
-1
View File
@@ -53,7 +53,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
# Subdirs # Subdirs
add_subdirectory(animation) add_subdirectory(animation)
add_subdirectory(event)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(console) add_subdirectory(console)
-1
View File
@@ -7,7 +7,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
asset.c asset.c
assetbatch.c
assetfile.c assetfile.c
) )
+5 -3
View File
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) { } else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
assetentry_t *loadedEntry = loading->entry; assetentry_t *loadedEntry = loading->entry;
loading->entry = NULL; loading->entry = NULL;
eventInvoke(&loadedEntry->onLoaded, loadedEntry); if(loadedEntry->onLoaded) {
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
}
} }
loading++; loading++;
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
assetentry_t *errEntry = loading->entry; assetentry_t *errEntry = loading->entry;
loading->entry = NULL; loading->entry = NULL;
threadMutexUnlock(&loading->mutex); threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry); if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
errorThrow("Failed to load asset asynchronously."); loading++;
break; break;
} }
-165
View File
@@ -1,165 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
const uint16_t count,
const assetbatchdesc_t *descs
) {
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
);
memoryZero(batch, sizeof(assetbatch_t));
batch->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks,
batch->onEntryLoadedUsers,
ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks,
batch->onEntryErrorUsers,
ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) {
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(
descs[i].path, descs[i].type, &batch->inputs[i]
);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
// Already loaded (cached) - count it now, no subscription needed.
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
}
}
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]) {
// Unsubscribe while we still hold a lock so the entry is live.
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
}
memoryZero(batch, sizeof(assetbatch_t));
}
void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
-124
View File
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry loaded. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry loads. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch Batch to unlock.
*/
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
/**
* Event trampoline invoked when a batch entry finishes loading.
* Increments the loaded counter and fires batch-level events.
*
* @param params The loaded assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnLoadedCb(void *params, void *user);
/**
* Event trampoline invoked when a batch entry fails to load.
* Increments the error counter and fires batch-level events.
*
* @param params The errored assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnErrorCb(void *params, void *user);
+22 -5
View File
@@ -87,13 +87,30 @@ errorret_t assetFileRead(
errorOk(); errorOk();
} }
// I assume zip_fread takes buffer NULL for skipping? // Some zip_fread() implementations (seen on PSP) reject a single call
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize); // asking for the entire (potentially large) file at once with EINVAL;
// the line reader above only ever asks for up to 1024 bytes per call and
// works fine, so read in bounded chunks here too.
size_t totalRead = 0;
uint8_t *dest = (uint8_t *)buffer;
while(totalRead < bufferSize) {
size_t chunkSize = mathMin(
bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
);
zip_int64_t bytesRead = zip_fread(
file->zipFile, dest + totalRead, chunkSize
);
if(bytesRead < 0) { if(bytesRead < 0) {
errorThrow("Failed to read from asset file: %s", file->filename); errorThrow(
"Failed to read from asset file: %s (%s)",
file->filename, zip_file_strerror(file->zipFile)
);
} }
file->position += bytesRead; if(bytesRead == 0) break;
file->lastRead = bytesRead; totalRead += (size_t)bytesRead;
}
file->position += totalRead;
file->lastRead = totalRead;
errorOk(); errorOk();
} }
+6
View File
@@ -11,6 +11,12 @@
#define ASSET_FILE_NAME_MAX 48 #define ASSET_FILE_NAME_MAX 48
// Max bytes requested per zip_fread() call in assetFileRead(). Some
// zip_fread() implementations (seen on PSP) reject a single call asking
// for very large amounts of data at once; the locale line reader has
// always used 1024-byte reads successfully, so that's the proven-safe cap.
#define ASSET_FILE_READ_CHUNK_MAX 1024
typedef struct assetfile_s assetfile_t; typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file); typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
+1
View File
@@ -17,3 +17,4 @@ add_subdirectory(locale)
add_subdirectory(json) add_subdirectory(json)
add_subdirectory(chunk) add_subdirectory(chunk)
add_subdirectory(dmf) add_subdirectory(dmf)
add_subdirectory(cutscene)
+1 -17
View File
@@ -35,22 +35,6 @@ void assetEntryInit(
entry->input = NULL; entry->input = NULL;
} }
refInit(&entry->refs, entry, NULL, NULL, NULL); refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
} }
void assetEntryLock(assetentry_t *entry) { void assetEntryLock(assetentry_t *entry) {
@@ -97,7 +81,7 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
"Asset entry still refed at dispose time." "Asset entry still refed at dispose time."
); );
eventInvoke(&entry->onUnloaded, entry); if(entry->onUnloaded) entry->onUnloaded(entry, entry->onUnloadedUser);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry)); errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t)); memoryZero(entry, sizeof(assetentry_t));
errorOk(); errorOk();
+22 -20
View File
@@ -7,7 +7,6 @@
#pragma once #pragma once
#include "asset/loader/assetloading.h" #include "asset/loader/assetloading.h"
#include "event/event.h"
#include "util/ref.h" #include "util/ref.h"
typedef enum { typedef enum {
@@ -20,11 +19,17 @@ typedef enum {
ASSET_ENTRY_STATE_ERROR ASSET_ENTRY_STATE_ERROR
} assetentrystate_t; } assetentrystate_t;
/** Maximum number of subscribers for each per-entry event. */
#define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
/**
* A single asset entry callback. Each entry supports at most one subscriber
* per event - a second assignment without clearing the first is a bug.
*
* @param entry The assetentry_t the event fired on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*assetentrycallback_t)(assetentry_t *entry, void *user);
struct assetentry_s { struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX]; char_t name[ASSET_FILE_NAME_MAX];
assetloadertype_t type; assetloadertype_t type;
@@ -33,30 +38,27 @@ struct assetentry_s {
ref_t refs; ref_t refs;
assetloaderinput_t *input; assetloaderinput_t *input;
assetloaderinput_t inputData; assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/** /**
* Fired once when the entry is disposed/reaped (params = assetentry_t *). * Fired once when loading completes successfully.
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread. * Always invoked on the main thread.
*/ */
event_t onUnloaded; assetentrycallback_t onLoaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX]; void *onLoadedUser;
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/** /**
* Fired once when loading fails (params = assetentry_t *). * Fired once when the entry is disposed/reaped. The asset data is still
* accessible when the callback runs. Always invoked on the main thread.
*/
assetentrycallback_t onUnloaded;
void *onUnloadedUser;
/**
* Fired once when loading fails.
* Always invoked on the main thread. * Always invoked on the main thread.
*/ */
event_t onError; assetentrycallback_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX]; void *onErrorUser;
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
}; };
/** /**
+6
View File
@@ -51,4 +51,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.loadAsync = assetChunkLoaderAsync, .loadAsync = assetChunkLoaderAsync,
.dispose = assetChunkDispose .dispose = assetChunkDispose
}, },
[ASSET_LOADER_TYPE_CUTSCENE] = {
.loadSync = assetCutsceneLoaderSync,
.loadAsync = assetCutsceneLoaderAsync,
.dispose = assetCutsceneDispose
},
}; };
+5
View File
@@ -13,6 +13,7 @@
#include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h" #include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/chunk/assetchunkloader.h" #include "asset/loader/chunk/assetchunkloader.h"
#include "asset/loader/cutscene/assetcutsceneloader.h"
typedef enum { typedef enum {
ASSET_LOADER_TYPE_NULL, ASSET_LOADER_TYPE_NULL,
@@ -24,6 +25,7 @@ typedef enum {
ASSET_LOADER_TYPE_LOCALE, ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON, ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_CHUNK, ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_CUTSCENE,
ASSET_LOADER_TYPE_COUNT ASSET_LOADER_TYPE_COUNT
} assetloadertype_t; } assetloadertype_t;
@@ -36,6 +38,7 @@ typedef union {
assetlocaleloaderloading_t locale; assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json; assetjsonloaderloading_t json;
assetchunkloaderloading_t chunk; assetchunkloaderloading_t chunk;
assetcutsceneloaderloading_t cutscene;
} assetloaderloading_t; } assetloaderloading_t;
typedef union { typedef union {
@@ -46,6 +49,7 @@ typedef union {
assetlocaleoutput_t locale; assetlocaleoutput_t locale;
assetjsonoutput_t json; assetjsonoutput_t json;
assetchunkoutput_t chunk; assetchunkoutput_t chunk;
assetcutsceneoutput_t cutscene;
} assetloaderoutput_t; } assetloaderoutput_t;
typedef union { typedef union {
@@ -54,6 +58,7 @@ typedef union {
assetlocaleloaderinput_t locale; assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json; assetjsonloaderinput_t json;
assetchunkloaderinput_t chunk; assetchunkloaderinput_t chunk;
assetcutsceneloaderinput_t cutscene;
} assetloaderinput_t; } assetloaderinput_t;
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
@@ -3,7 +3,7 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
include(dusktest) target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
# Tests assetcutsceneloader.c
dusktest(test_event.c) )
@@ -0,0 +1,467 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetcutsceneloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "asset/asset.h"
// DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1),
// itemCount u8 (1), poolSize u16 LE (2) = 12 bytes.
#define ASSET_CUTSCENE_HEADER_SIZE 12
static uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset) {
uint8_t value = data[*offset];
*offset += sizeof(uint8_t);
return value;
}
static uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset) {
uint16_t value;
memoryCopy(&value, data + *offset, sizeof(uint16_t));
*offset += sizeof(uint16_t);
return endianLittleToHost16(value);
}
static uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset) {
uint32_t value;
memoryCopy(&value, data + *offset, sizeof(uint32_t));
*offset += sizeof(uint32_t);
return endianLittleToHost32(value);
}
static float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset) {
float_t value;
memoryCopy(&value, data + *offset, sizeof(float_t));
*offset += sizeof(float_t);
return endianLittleToHostFloat(value);
}
static worldunit_t assetCutsceneReadWorldUnit(
const uint8_t *data,
size_t *offset
) {
uint16_t value = assetCutsceneReadU16(data, offset);
return (worldunit_t)value;
}
static worldpos_t assetCutsceneReadWorldPos(
const uint8_t *data,
size_t *offset
) {
worldpos_t pos;
pos.x = assetCutsceneReadWorldUnit(data, offset);
pos.y = assetCutsceneReadWorldUnit(data, offset);
pos.z = assetCutsceneReadWorldUnit(data, offset);
return pos;
}
// Copies a length-prefixed string directly into an item's own embedded
// char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
// are never pool references, see the item-field inventory in the runtime
// cutscene file design.
static void assetCutsceneReadEmbeddedString(
const uint8_t *data,
size_t *offset,
char_t *dest,
const size_t destCapacity
) {
uint8_t len = assetCutsceneReadU8(data, offset);
assertTrue(len < destCapacity, "Cutscene string exceeds field capacity");
memoryCopy(dest, data + *offset, len);
dest[len] = '\0';
*offset += len;
}
// Resolves a u16 pool offset (read from the item stream) to a real pointer
// into the entry's own persistent pool allocation.
static const char_t * assetCutsceneReadPoolString(
const uint8_t *data,
size_t *offset,
const char_t *pool
) {
uint16_t poolOffset = assetCutsceneReadU16(data, offset);
return pool + poolOffset;
}
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.cutscene.state != ASSET_CUTSCENE_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.cutscene.data, "Data already defined?");
assetfile_t *file = &loading->loading.cutscene.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assertTrue(
file->lastRead == file->size,
"Failed to read entire cutscene file."
);
// Saved before assetFileDispose zeroes the whole assetfile_t struct
// (including .size) - the sync phase needs the file's total length to
// locate the pool region, which starts poolSize bytes before the end.
loading->loading.cutscene.dataSize = (size_t)file->size;
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.cutscene.data = data;
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
}
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
uint8_t *data = loading->loading.cutscene.data;
assertNotNull(data, "Cutscene data should have been loaded by now.");
size_t fileSize = loading->loading.cutscene.dataSize;
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'T' || data[3] != 'S') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid cutscene file header");
}
size_t offset = 4;
uint32_t version = assetCutsceneReadU32(data, &offset);
if(version != ASSET_CUTSCENE_FILE_VERSION) {
memoryFree(data);
assetLoaderErrorThrow(
loading, "Unsupported cutscene file version %u", version
);
}
cutscenepause_t pauseType = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
uint8_t itemCount = assetCutsceneReadU8(data, &offset);
uint16_t poolSize = assetCutsceneReadU16(data, &offset);
assertTrue(offset == ASSET_CUTSCENE_HEADER_SIZE, "Cutscene header size mismatch");
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
if(poolSize > 0) {
size_t poolStart = fileSize - (size_t)poolSize;
memoryCopy(out->pool, data + poolStart, poolSize);
}
const char_t *pool = out->pool;
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
for(uint8_t i = 0; i < itemCount; i++) {
cutsceneitem_t *item = &out->items[i];
item->type = (cutsceneitemtype_t)assetCutsceneReadU8(data, &offset);
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
assetCutsceneReadEmbeddedString(
data, &offset, item->text.text, CUTSCENE_TEXT_MAX_CHARS
);
break;
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
assetCutsceneReadEmbeddedString(
data, &offset, item->textMini.text, CUTSCENE_TEXT_MINI_MAX_CHARS
);
item->textMini.position[0] = assetCutsceneReadFloat(data, &offset);
item->textMini.position[1] = assetCutsceneReadFloat(data, &offset);
item->textMini.position[2] = assetCutsceneReadFloat(data, &offset);
item->textMini.duration = assetCutsceneReadFloat(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE:
item->textMiniHide.index = assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_WAIT:
item->wait = assetCutsceneReadFloat(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset);
item->entityTeleport.target = assetCutsceneReadWorldPos(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
item->entityWalkTo.entityIndex = assetCutsceneReadU8(data, &offset);
item->entityWalkTo.walkAround = assetCutsceneReadU8(data, &offset) != 0;
uint8_t count = assetCutsceneReadU8(data, &offset);
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
item->entityWalkTo.count = count;
item->entityWalkTo.positions = (const worldpos_t *)(pool + poolOffset);
break;
}
case CUTSCENE_ITEM_TYPE_FADE:
item->fade.from.r = assetCutsceneReadU8(data, &offset);
item->fade.from.g = assetCutsceneReadU8(data, &offset);
item->fade.from.b = assetCutsceneReadU8(data, &offset);
item->fade.from.a = assetCutsceneReadU8(data, &offset);
item->fade.to.r = assetCutsceneReadU8(data, &offset);
item->fade.to.g = assetCutsceneReadU8(data, &offset);
item->fade.to.b = assetCutsceneReadU8(data, &offset);
item->fade.to.a = assetCutsceneReadU8(data, &offset);
item->fade.duration = assetCutsceneReadFloat(data, &offset);
item->fade.easing = (easingtype_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
item->setPause = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
item->itemGive.item = (itemid_t)assetCutsceneReadU16(data, &offset);
item->itemGive.quantity = assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
item->entityRemove.entityIndex = assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
item->entityAdd.entityType = assetCutsceneReadU8(data, &offset);
item->entityAdd.position = assetCutsceneReadWorldPos(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
item->entityTurn.entityIndex = assetCutsceneReadU8(data, &offset);
item->entityTurn.direction =
(entitydir_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
item->entityWalkToEntity.entityIndex =
assetCutsceneReadU8(data, &offset);
item->entityWalkToEntity.targetEntityIndex =
assetCutsceneReadU8(data, &offset);
item->entityWalkToEntity.offsetX =
assetCutsceneReadWorldUnit(data, &offset);
item->entityWalkToEntity.offsetY =
assetCutsceneReadWorldUnit(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
item->mapAreaRemove.areaId = assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: {
uint8_t count = assetCutsceneReadU8(data, &offset);
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
assertTrue(
count <= CUTSCENE_MAP_AREA_WAIT_MAX,
"Cutscene map area wait count exceeds maximum"
);
item->mapAreaWait.count = count;
item->mapAreaWait.areaIds = (const uint8_t *)(pool + poolOffset);
break;
}
case CUTSCENE_ITEM_TYPE_START_BATTLE: {
item->startBattle.encounterType =
(battleencountertype_t)assetCutsceneReadU8(data, &offset);
item->startBattle.fleeAvailable =
assetCutsceneReadU8(data, &offset) != 0;
uint8_t enemyCount = assetCutsceneReadU8(data, &offset);
assertTrue(
enemyCount <= CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX,
"Cutscene battle enemy count exceeds maximum"
);
item->startBattle.enemyCount = enemyCount;
for(uint8_t e = 0; e < enemyCount; e++) {
cutscenestartbattleenemy_t *enemy = &item->startBattle.enemies[e];
enemy->stats.attack = assetCutsceneReadU16(data, &offset);
enemy->stats.defense = assetCutsceneReadU16(data, &offset);
enemy->stats.magic = assetCutsceneReadU16(data, &offset);
enemy->stats.speed = assetCutsceneReadU16(data, &offset);
enemy->stats.luck = assetCutsceneReadU16(data, &offset);
enemy->healthMax = assetCutsceneReadU16(data, &offset);
enemy->mpMax = assetCutsceneReadU16(data, &offset);
}
break;
}
case CUTSCENE_ITEM_TYPE_EMOJI:
item->emoji.entityIndex = assetCutsceneReadU8(data, &offset);
item->emoji.duration = assetCutsceneReadFloat(data, &offset);
item->emoji.emojiType = (uiemojitype_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_SHAKE:
item->shake.amount = assetCutsceneReadU8(data, &offset);
item->shake.duration = assetCutsceneReadFloat(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE:
item->battleWaitState.state =
(battlestate_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION:
item->battleForceAction.fighterIndex = assetCutsceneReadU8(data, &offset);
item->battleForceAction.targetIndex = assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_MODAL: {
assetCutsceneReadEmbeddedString(
data, &offset, item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS
);
assetCutsceneReadEmbeddedString(
data, &offset, item->modal.message, CUTSCENE_MODAL_MESSAGE_MAX_CHARS
);
// v1 only supports the message-only form: MODAL and MODAL_OPTIONS
// share this same tag with no separate discriminator, and there is
// no native-callback registry yet to resolve an options callback.
// Read into a local first (not inline in the check below) - on a
// release build with DUSK_ASSERTIONS_FAKED, an assert's condition
// is never evaluated at all, so a byte-consuming call inside one
// would silently desync every item after it. This is untrusted
// file content anyway, so it gets a real error, not an assert.
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
if(optionCount != 0) {
memoryFree(data);
memoryFree(out->items);
out->items = NULL;
if(out->pool != NULL) {
memoryFree(out->pool);
out->pool = NULL;
}
assetLoaderErrorThrow(
loading,
"Cutscene MODAL item with options is not supported in "
"file-based cutscenes yet - use MODAL_OPTIONS_MARKERS instead"
);
}
break;
}
case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: {
assetCutsceneReadEmbeddedString(
data, &offset, item->modalOptionsMarkers.title,
CUTSCENE_MODAL_TITLE_MAX_CHARS
);
assetCutsceneReadEmbeddedString(
data, &offset, item->modalOptionsMarkers.message,
CUTSCENE_MODAL_MESSAGE_MAX_CHARS
);
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
assertTrue(
optionCount <= CUTSCENE_MODAL_OPTIONS_MARKERS_MAX,
"Cutscene modal option count exceeds maximum"
);
item->modalOptionsMarkers.optionCount = optionCount;
for(uint8_t o = 0; o < optionCount; o++) {
item->modalOptionsMarkers.options[o] =
assetCutsceneReadPoolString(data, &offset, pool);
item->modalOptionsMarkers.markers[o] =
assetCutsceneReadPoolString(data, &offset, pool);
}
break;
}
case CUTSCENE_ITEM_TYPE_MODAL_CLOSE:
case CUTSCENE_ITEM_TYPE_RESTART:
break;
case CUTSCENE_ITEM_TYPE_PRINT:
assetCutsceneReadEmbeddedString(
data, &offset, item->print.text, CUTSCENE_PRINT_MAX_CHARS
);
break;
case CUTSCENE_ITEM_TYPE_MARKER:
item->marker.name = assetCutsceneReadPoolString(data, &offset, pool);
break;
case CUTSCENE_ITEM_TYPE_SCENE:
item->sceneChange.type = (scenetype_t)assetCutsceneReadU8(data, &offset);
break;
case CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK:
item->saveDeviceCheck.successMarker =
assetCutsceneReadPoolString(data, &offset, pool);
item->saveDeviceCheck.failureMarker =
assetCutsceneReadPoolString(data, &offset, pool);
break;
case CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS:
item->saveLoadAllSlots.successMarker =
assetCutsceneReadPoolString(data, &offset, pool);
item->saveLoadAllSlots.failureMarker =
assetCutsceneReadPoolString(data, &offset, pool);
break;
default:
memoryFree(data);
memoryFree(out->items);
out->items = NULL;
if(out->pool != NULL) {
memoryFree(out->pool);
out->pool = NULL;
}
assetLoaderErrorThrow(
loading,
"Cutscene item type %u is not supported in file-based cutscenes "
"(item %u/%u, offset %u/%u, poolSize %u)",
(uint32_t)item->type, (uint32_t)i, (uint32_t)itemCount,
(uint32_t)offset, (uint32_t)fileSize, (uint32_t)poolSize
);
}
}
memoryFree(data);
loading->loading.cutscene.data = NULL;
out->cutscene.items = out->items;
out->cutscene.itemCount = itemCount;
out->cutscene.pause = pauseType;
out->cutscene.dataSize = 0;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetCutsceneDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetcutsceneoutput_t *out = &entry->data.cutscene;
if(out->items != NULL) {
memoryFree(out->items);
out->items = NULL;
}
if(out->pool != NULL) {
memoryFree(out->pool);
out->pool = NULL;
}
errorOk();
}
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "rpg/cutscene/cutscene.h"
#include "rpg/cutscene/item/cutsceneitem.h"
#define ASSET_CUTSCENE_FILE_VERSION 1
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assetcutsceneloaderinput_t;
typedef enum {
ASSET_CUTSCENE_LOADING_STATE_INITIAL,
ASSET_CUTSCENE_LOADING_STATE_READ_FILE,
ASSET_CUTSCENE_LOADING_STATE_PARSE
} assetcutsceneloadingstate_t;
typedef struct {
assetfile_t file;
assetcutsceneloadingstate_t state;
uint8_t *data;
size_t dataSize;// Saved before assetFileDispose zeroes file.size.
} assetcutsceneloaderloading_t;
// Runtime-loaded cutscene: items/pool are heap-allocated to the file's
// actual declared sizes (not fixed-capacity), so an entry that never holds
// a cutscene costs nothing extra in the shared assetloaderoutput_t union -
// see assetchunkoutput_t.tiles for the same pattern.
typedef struct {
cutscene_t cutscene; // .items points at the items array below
cutsceneitem_t *items;
char_t *pool;
} assetcutsceneoutput_t;
/**
* Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes
* into the loading buffer so the sync phase can parse without blocking the
* main thread on I/O.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for cutscene assets. Validates the DCTS binary
* previously read by the async phase and decodes it into a heap-allocated
* cutsceneitem_t array + string/data pool.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetCutsceneLoaderSync(assetloading_t *loading);
/**
* Disposer for cutscene assets.
*
* @param entry Asset entry containing the cutscene data to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetCutsceneDispose(assetentry_t *entry);
+33 -6
View File
@@ -76,11 +76,23 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y { 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z { 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [ { 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font) // Custom icon glyph, not a real backslash - a backspace symbol: Caps
// Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to
// point left instead, adapted to this font's fixed 6x10 tile (each of
// Caps Lock's row widths becomes a column height here, centered
// vertically). Backslash was never drawn anyway, and isn't a key this
// virtual keyboard can type - see FONT_ICON_BACKSPACE.
{ 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, // FONT_ICON_BACKSPACE
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ] { 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font) // Custom icon glyph, not a real backtick - an up arrow for Shift, same
// head as Caps Lock's (see FONT_ICON_CAPSLOCK) but with a shaft 2px
// thinner, plus one empty row near the bottom of it, so it reads as a
// lighter/broken version of Caps Lock's thicker uninterrupted one.
// Backtick was never drawn anyway, and isn't a key this virtual
// keyboard can type - see FONT_ICON_SHIFT.
{ 0x0C, 0x1E, 0x3F, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x00 }, // FONT_ICON_SHIFT
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a { 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b { 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c { 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
@@ -108,10 +120,25 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y { 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z { 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // { { 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font) // Custom icon glyph, not a real pipe - a "return" arrow for the
// keyboard's newline key: a vertical riser down the right side that
// hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated
// into an L. Pipe was never drawn anyway, and isn't a key this
// virtual keyboard can type - see FONT_ICON_NEWLINE.
{ 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, // FONT_ICON_NEWLINE
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // } { 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font) // Custom icon glyph, not a real tilde - a spacebar symbol for the
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile // keyboard's space key: an underscore with a tick at each end, like
// "|___|". Tilde was never drawn anyway, and was explicitly dropped
// from this virtual keyboard's own key set - see FONT_ICON_SPACE.
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x3F }, // FONT_ICON_SPACE
// Custom icon glyph, not a real ASCII character - a thick, solid,
// uninterrupted up arrow for Caps Lock (contrast FONT_ICON_SHIFT's
// same head but a thinner, gapped shaft). Assigned to char code 127
// (DEL) since that codepoint is never legitimately typed text and
// (unlike 128+) is still a positive value regardless of whether this
// platform's plain `char` is signed - see FONT_ICON_CAPSLOCK.
{ 0x0C, 0x1E, 0x3F, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x00 }, // FONT_ICON_CAPSLOCK
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
}; };
+36 -1
View File
@@ -28,10 +28,45 @@ typedef struct {
/** /**
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS * * Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at * FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles. * TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile
* (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile.
* FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within
* the printable range instead of more trailing tiles - see their own
* comments.
*/ */
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS) #define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
/**
* Custom (non-ASCII-meaning) icon glyphs baked into FONT_DEFAULT_GLYPHS
* at otherwise-unused codepoints within the range this font covers -
* safe to use anywhere a char_t string is expected, e.g. a uibutton_t
* label. Kept below 128: char_t is a plain `char`, whose signedness
* varies by platform, so a codepoint of 128 or above isn't safely
* representable everywhere this engine targets.
*/
// Thick, solid, uninterrupted up arrow - Caps Lock. A trailing tile
// (char code 127/DEL) that was never a real character to begin with.
#define FONT_ICON_CAPSLOCK "\x7F"
// Same up arrow, but with a thinner and gapped shaft - Shift. Reuses
// the backtick's glyph slot: backtick was never drawn by this font
// anyway, and isn't a key this engine's virtual keyboard can type.
#define FONT_ICON_SHIFT "`"
// A "return" arrow (down then left, with a leftward arrowhead) - the
// keyboard's newline key. Reuses the pipe's glyph slot, for the same
// reason as FONT_ICON_SHIFT.
#define FONT_ICON_NEWLINE "|"
// An underscore with a tick at each end ("|___|") - the keyboard's
// space key. Reuses the tilde's glyph slot: tilde was never drawn by
// this font, and was explicitly dropped from this engine's virtual
// keyboard's own key set.
#define FONT_ICON_SPACE "~"
// Caps Lock's arrow rotated to point left - the keyboard's backspace
// key. Reuses the backslash's glyph slot, for the same reason as
// FONT_ICON_SHIFT/FONT_ICON_NEWLINE/FONT_ICON_SPACE. Last free reused
// slot below 128 - one more custom icon after this needs
// FONT_DEFAULT_COLUMNS/ROWS grown to make room.
#define FONT_ICON_BACKSPACE "\\"
extern font_t FONT_DEFAULT; extern font_t FONT_DEFAULT;
/** /**
+34
View File
@@ -187,3 +187,37 @@ int32_t textMeasure(
return spriteCount; return spriteCount;
} }
void textWrap(char_t *text, const font_t *font, const float_t maxWidth) {
assertNotNull(text, "Text cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
float_t fontWidth = (float_t)font->tileset->tileWidth;
if(fontWidth <= 0.0f) return;
int32_t charsPerLine = (int32_t)(maxWidth / fontWidth);
if(charsPerLine <= 0) return;
int32_t lineWidth = 0;
int32_t lastSpace = -1;
for(int32_t i = 0; text[i] != '\0'; i++) {
if(text[i] == '\n') {
lineWidth = 0;
lastSpace = -1;
continue;
}
if(text[i] == ' ') {
lastSpace = i;
}
lineWidth++;
if(lineWidth > charsPerLine && lastSpace != -1) {
text[lastSpace] = '\n';
lineWidth = i - lastSpace;
lastSpace = -1;
}
}
}
+14
View File
@@ -104,3 +104,17 @@ int32_t textMeasure(
int32_t *outWidth, int32_t *outWidth,
int32_t *outHeight int32_t *outHeight
); );
/**
* Word-wraps text in place for display at up to maxWidth pixels wide,
* by replacing the space nearest each overflow point with a newline.
* Length is unchanged - this only ever swaps existing spaces for
* newlines, never inserts characters - so it's always safe to call on a
* fixed-size buffer. A single word wider than maxWidth on its own is
* left unbroken.
*
* @param text Null-terminated, caller-owned buffer to wrap in place.
* @param font Font to measure character width with.
* @param maxWidth Maximum line width, in pixels.
*/
void textWrap(char_t *text, const font_t *font, const float_t maxWidth);
-76
View File
@@ -1,76 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "event.h"
#include "assert/assert.h"
#include "util/memory.h"
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
) {
assertNotNull(event, "event must not be NULL");
assertNotNull((void *)callbacks, "callbacks must not be NULL");
assertTrue(size > 0, "size must be greater than 0");
event->callbacks = callbacks;
event->users = users;
event->size = size;
event->count = 0;
memoryZero(callbacks, sizeof(eventcallback_t) * size);
if(users) memoryZero(users, sizeof(void *) * size);
}
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
assertNotNull(event, "event must not be NULL");
assertNotNull(callback, "callback must not be NULL");
// Ensure callback isn't already susbcribed
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
assertUnreachable("Callback already registered, cannot subscribe twice.");
}
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
event->callbacks[event->count] = callback;
if(user) {
assertNotNull(event->users, "Cannot add user pointer.");
event->users[event->count] = user;
}
event->count++;
}
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
assertNotNull(event, "event must not be NULL");
assertNotNull(callback, "callback must not be NULL");
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
uint32_t last = event->count - 1;
if(i != last) {
event->callbacks[i] = event->callbacks[last];
if(event->users) event->users[i] = event->users[last];
}
event->callbacks[last] = NULL;
if(event->users) event->users[last] = NULL;
event->count--;
return;
}
}
void eventInvoke(const event_t *event, void *params) {
assertNotNull(event, "event must not be NULL");
for(uint32_t i = 0; i < event->count; i++) {
void *u = event->users ? event->users[i] : NULL;
event->callbacks[i](params, u);
}
}
-64
View File
@@ -1,64 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef void (*eventcallback_t)(void *params, void *user);
typedef struct {
eventcallback_t *callbacks;
void **users;
size_t size;
uint32_t count;
} event_t;
/**
* Initializes an event, binding it to the provided backing arrays and clearing
* all subscribers. May also be called to reset an event (re-clears subscribers
* without changing the backing arrays or size).
*
* @param event The event to initialize.
* @param callbacks Caller-owned array of at least `size` callback slots.
* @param users Array of user pointers, matching each callback, or NULL.
* @param size Capacity of both arrays, must match.
*/
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
);
/**
* Subscribes a callback to an event. The callback is invoked with params and
* the provided user pointer each time the event fires. The same (callback,
* user) pair may only be subscribed once.
*
* @param event The event to subscribe to.
* @param callback The function to call when the event fires.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
/**
* Removes a previously subscribed (callback, user) pair. Does nothing if the
* pair is not currently subscribed.
*
* @param event The event to unsubscribe from.
* @param callback The callback that was passed to eventSubscribe.
*/
void eventUnsubscribe(event_t *event, eventcallback_t callback);
/**
* Invokes all subscribed callbacks, passing params and each subscriber's user
* pointer.
*
* @param event The event to invoke.
* @param params Arbitrary pointer forwarded to every callback unchanged.
*/
void eventInvoke(const event_t *event, void *params);
-1
View File
@@ -11,7 +11,6 @@
#include "util/string.h" #include "util/string.h"
#include "util/math.h" #include "util/math.h"
#include "time/time.h" #include "time/time.h"
#include "event/event.h"
input_t INPUT; input_t INPUT;
+137
View File
@@ -53,6 +53,25 @@ typedef struct cutscene_s {
#define CUTSCENE_WAIT(WAIT) \ #define CUTSCENE_WAIT(WAIT) \
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT } { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
// A named, otherwise no-op position in the item list that cutsceneGoTo
// can jump execution straight to. NAME is matched with stringEquals,
// not pointer identity, so it's safe to use separate string literals
// with the same contents at the marker and at each call site.
#define CUTSCENE_MARKER(NAME) \
{ .type = CUTSCENE_ITEM_TYPE_MARKER, .marker = { .name = NAME } }
// Restarts the currently running cutscene from its first item,
// preserving whatever interact/interacted entities triggered it.
#define CUTSCENE_RESTART() \
{ .type = CUTSCENE_ITEM_TYPE_RESTART }
// Requests a switch to a different SCENE_TYPE via sceneSet, then
// immediately continues on to whatever follows this item - the switch
// itself doesn't happen until the next sceneUpdate() tick, so it does
// not take effect this frame.
#define CUTSCENE_SCENE(TYPE) \
{ .type = CUTSCENE_ITEM_TYPE_SCENE, .sceneChange = { .type = TYPE } }
#define CUTSCENE_CUTSCENE(CUTSCENE) \ #define CUTSCENE_CUTSCENE(CUTSCENE) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \ .type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
@@ -62,6 +81,124 @@ typedef struct cutscene_s {
#define CUTSCENE_CALLBACK(CALLBACK) \ #define CUTSCENE_CALLBACK(CALLBACK) \
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
#define CUTSCENE_PRINT(TEXT) \
{ .type = CUTSCENE_ITEM_TYPE_PRINT, .print = { .text = TEXT } }
// Shows a message-only modal (no option buttons) and immediately
// continues on to whatever follows this item - it does not wait for
// the dialog to be dismissed. Script the rest of the interaction (e.g.
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
// TITLE and MESSAGE are each displayed as-is unless they match a
// locale message ID, in which case the translated string is shown
// instead - see uiModalLocalize.
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL, \
.modal = { .title = TITLE, .message = MESSAGE } \
}
// Shows a modal with option buttons and immediately continues on, same
// as CUTSCENE_MODAL - it does not block waiting for a selection.
// Back/cancel input is disabled while it's open (see
// uiMenuSetDisableBack), so it must be dismissed by picking one; CALLBACK
// then fires with the selected option index once the dialog closes.
// Option labels are passed as trailing arguments, e.g.
// CUTSCENE_MODAL_OPTIONS(title, message, callback, "Retry", "Cancel") -
// their strings are not copied by this item, so they must stay valid
// until the modal opens (string literals are fine). Like TITLE and
// MESSAGE, each option is translated if it matches a locale message ID.
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL, \
.modal = { \
.title = TITLE, .message = MESSAGE, \
.options = (const char_t *[]){ __VA_ARGS__ }, \
.optionCount = (uint8_t)( \
sizeof((const char_t *[]){ __VA_ARGS__ }) / sizeof(const char_t *) \
), \
.callback = CALLBACK \
} \
}
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and
// MARKER1 to jump straight to via cutsceneGoTo once it's selected - no
// callback function to write. Does not fall through to whatever follows
// this item, so MARKER1 must be scripted elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
.modalOptionsMarkers = { \
.title = TITLE, .message = MESSAGE, \
.options = { OPTION1 }, \
.markers = { MARKER1 }, \
.optionCount = 1 \
} \
}
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options,
// jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo
// once the corresponding option is selected - no callback function to
// write. Does not fall through to whatever follows this item, so both
// markers must be scripted elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_TWO( \
TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
.modalOptionsMarkers = { \
.title = TITLE, .message = MESSAGE, \
.options = { OPTION1, OPTION2 }, \
.markers = { OPTION1_MARKER, OPTION2_MARKER }, \
.optionCount = 2 \
} \
}
// Closes the currently open modal (if any). Useful when a modal was
// opened outside of a blocking CUTSCENE_MODAL item (e.g. directly via
// uiModalOpen) and this cutscene just needs to dismiss it and continue
// on to whatever follows this item in the sequence.
#define CUTSCENE_MODAL_CLOSE() \
{ .type = CUTSCENE_ITEM_TYPE_MODAL_CLOSE }
// Opens the on-screen keyboard and blocks the cutscene until it closes,
// then caches whatever was typed - see cutsceneSystemGetTextCache to
// read it back afterwards. __VA_ARGS__ are uikeyboardopen_t designated
// initializers, e.g. CUTSCENE_KEYBOARD(.cancel = true, .maxLength = 8).
#define CUTSCENE_KEYBOARD(...) \
{ \
.type = CUTSCENE_ITEM_TYPE_KEYBOARD, \
.keyboard = { .open = { __VA_ARGS__ } } \
}
// (Re)requests an available save device and jumps straight to
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same as a
// CUTSCENE_MODAL_OPTIONS callback - it does not fall through to
// whatever follows this item, so both markers must be scripted
// elsewhere in the same cutscene.
#define CUTSCENE_SAVE_DEVICE_CHECK(SUCCESS_MARKER, FAILURE_MARKER) \
{ \
.type = CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, \
.saveDeviceCheck = { \
.successMarker = SUCCESS_MARKER, \
.failureMarker = FAILURE_MARKER \
} \
}
// (Re)loads every save slot via saveLoadAllSlots() and jumps straight to
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same shape as
// CUTSCENE_SAVE_DEVICE_CHECK - it does not fall through to whatever
// follows this item, so both markers must be scripted elsewhere in the
// same cutscene.
#define CUTSCENE_SAVE_LOAD_ALL_SLOTS(SUCCESS_MARKER, FAILURE_MARKER) \
{ \
.type = CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, \
.saveLoadAllSlots = { \
.successMarker = SUCCESS_MARKER, \
.failureMarker = FAILURE_MARKER \
} \
}
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \ #define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \ .type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
+93 -5
View File
@@ -8,6 +8,7 @@
#include "cutscenesystem.h" #include "cutscenesystem.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
cutscenesystem_t CUTSCENE_SYSTEM; cutscenesystem_t CUTSCENE_SYSTEM;
@@ -16,11 +17,7 @@ void cutsceneSystemInit() {
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
} }
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { void cutsceneSystemPrepare(
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
}
void cutsceneSystemStartCutsceneWith(
const cutscene_t *cutscene, const cutscene_t *cutscene,
entity_t *interact, entity_t *interact,
entity_t *interacted entity_t *interacted
@@ -38,10 +35,49 @@ void cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.textCache[0] = '\0';
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
CUTSCENE_SYSTEM.onComplete = NULL;
}
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
}
void cutsceneSystemStartCutsceneWith(
const cutscene_t *cutscene,
entity_t *interact,
entity_t *interacted
) {
cutsceneSystemPrepare(cutscene, interact, interacted);
cutsceneSystemNext(); cutsceneSystemNext();
} }
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
) {
cutsceneSystemPrepare(cutscene, NULL, NULL);
cutsceneGoTo(marker);
}
void cutsceneRestart(void) {
assertNotNull(
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
);
// A restart is the same logical run trying again (e.g. retrying a failed
// save-device check), not a fresh unrelated start, so it should not
// silently drop a completion callback the caller already armed.
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.scene,
CUTSCENE_SYSTEM.entityInteract,
CUTSCENE_SYSTEM.entityInteracted
);
CUTSCENE_SYSTEM.onComplete = onComplete;
}
void cutsceneSystemUpdate() { void cutsceneSystemUpdate() {
if(CUTSCENE_SYSTEM.scene == NULL) return; if(CUTSCENE_SYSTEM.scene == NULL) return;
@@ -58,6 +94,13 @@ void cutsceneSystemNext() {
if( if(
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
) { ) {
// Saved and cleared before firing so a callback that immediately
// starts another cutscene (or sets its own onComplete) isn't clobbered
// by this function's own cleanup running after it - same reentrancy
// hazard as uiFocusPop, see src/dusk/ui/focus/uifocus.c.
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
void *userData = CUTSCENE_SYSTEM.userData;
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
@@ -67,6 +110,10 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.textCache[0] = '\0';
CUTSCENE_SYSTEM.onComplete = NULL;
if(onComplete != NULL) onComplete(userData);
return; return;
} }
@@ -76,6 +123,35 @@ void cutsceneSystemNext() {
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
} }
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
assertNotNull(
CUTSCENE_SYSTEM.scene,
"cutsceneSystemSetOnComplete called with no cutscene running"
);
CUTSCENE_SYSTEM.onComplete = onComplete;
}
void cutsceneGoTo(const char_t *name) {
assertNotNull(
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
);
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
if(
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
stringEquals(item->marker.name, name)
) {
CUTSCENE_SYSTEM.currentItem = i;
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
return;
}
}
assertTrue(false, "cutsceneGoTo: no marker found with that name");
}
const cutsceneitem_t * cutsceneSystemGetCurrentItem() { const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
if(CUTSCENE_SYSTEM.scene == NULL) return NULL; if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
@@ -144,6 +220,16 @@ uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
return index; return index;
} }
const char_t * cutsceneSystemGetTextCache(void) {
return CUTSCENE_SYSTEM.textCache;
}
void cutsceneSystemSetTextCache(const char_t *text) {
stringCopy(
CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1
);
}
void cutsceneSystemDispose() { void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
@@ -154,4 +240,6 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.textCache[0] = '\0';
CUTSCENE_SYSTEM.onComplete = NULL;
} }
+88
View File
@@ -21,6 +21,11 @@ typedef struct entity_s entity_t;
// cutscene_t.dataSize. // cutscene_t.dataSize.
#define CUTSCENE_SYSTEM_SIZE_MAX 8192 #define CUTSCENE_SYSTEM_SIZE_MAX 8192
// Size of CUTSCENE_SYSTEM.textCache - keep >= UI_KEYBOARD_TEXT_MAX (see
// ui/dialog/keyboard/uikeyboard.h) so text entered via a
// CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here.
#define CUTSCENE_TEXT_CACHE_MAX 64
typedef struct { typedef struct {
const cutscene_t *scene; const cutscene_t *scene;
uint8_t currentItem; uint8_t currentItem;
@@ -32,12 +37,21 @@ typedef struct {
uint8_t areaLastCreated; uint8_t areaLastCreated;
uint8_t textMiniLastCreated; uint8_t textMiniLastCreated;
// Free-form text cache, e.g. holding whatever was last typed via a
// CUTSCENE_ITEM_TYPE_KEYBOARD item - see cutsceneSystemGetTextCache/
// cutsceneSystemSetTextCache. Not tied to any one item type; any item
// may read or write it.
char_t textCache[CUTSCENE_TEXT_CACHE_MAX];
// Data (used by the current item). // Data (used by the current item).
cutsceneitemdata_t data; cutsceneitemdata_t data;
// Custom user data for the running cutscene, sized per-scene by // Custom user data for the running cutscene, sized per-scene by
// cutscene_t.dataSize. // cutscene_t.dataSize.
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX]; uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
// See cutsceneSystemSetOnComplete.
cutscenecallback_t onComplete;
} cutscenesystem_t; } cutscenesystem_t;
extern cutscenesystem_t CUTSCENE_SYSTEM; extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -67,6 +81,47 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interacted entity_t *interacted
); );
/**
* Starts a cutscene with no bound entities, jumping straight to the
* CUTSCENE_MARKER item with the given name instead of running from the
* first item - as if cutsceneGoTo(marker) had been called immediately
* after cutsceneSystemStartCutscene. Asserts if no marker with that
* name exists in the cutscene.
*
* @param cutscene Pointer to the cutscene to start.
* @param marker Marker name to jump to, matched with stringEquals.
*/
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
);
/**
* Restarts the currently running cutscene from its first item,
* preserving whatever interact/interacted entities triggered it.
* Asserts if no cutscene is running.
*/
void cutsceneRestart(void);
/**
* Sets a native callback to fire once when the currently running cutscene
* finishes by running off the end of its item list. A fresh
* cutsceneSystemStartCutscene* call clears any previously set callback, so
* call this again after starting a new cutscene to arm it - but
* cutsceneRestart() preserves whatever was armed, since a restart (e.g.
* retrying a failed check) is the same logical run trying again, not a new
* one. Invoked with CUTSCENE_SYSTEM.userData, same as CUTSCENE_CALLBACK.
*
* Exists so a runtime-loaded cutscene file (which can't store a native
* function pointer) can still hand off to native code once it's done,
* without needing a whole name->function registry: the file just ends
* normally, and whoever started it supplies what happens next.
*
* @param onComplete Callback to fire on natural completion. May be NULL
* to clear a previously set one.
*/
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
/** /**
* Resolves a raw entity index (or sentinel) to an entity pointer. * Resolves a raw entity index (or sentinel) to an entity pointer.
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED, * Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
@@ -97,11 +152,44 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
*/ */
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index); uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
/**
* Returns CUTSCENE_SYSTEM.textCache - whatever was last written there via
* cutsceneSystemSetTextCache (e.g. by a CUTSCENE_ITEM_TYPE_KEYBOARD item
* once its keyboard closes). Empty ("") if nothing has been cached yet
* for the running cutscene.
*
* @returns The cached text.
*/
const char_t * cutsceneSystemGetTextCache(void);
/**
* Overwrites CUTSCENE_SYSTEM.textCache with a copy of text. Any item may
* call this - it isn't tied to any one item type - so later items can
* read back whatever the caller wants to pass along, up to
* CUTSCENE_TEXT_CACHE_MAX - 1 characters.
*
* @param text The text to cache; copied internally, safe to be
* transient. Must not exceed CUTSCENE_TEXT_CACHE_MAX - 1 characters.
*/
void cutsceneSystemSetTextCache(const char_t *text);
/** /**
* Advance to the next item in the cutscene. * Advance to the next item in the cutscene.
*/ */
void cutsceneSystemNext(); void cutsceneSystemNext();
/**
* Jumps the running cutscene directly to the CUTSCENE_MARKER item with
* the given name and starts it immediately, as if cutsceneSystemNext()
* had advanced straight to it. Intended to be called from within
* another item's start/update (e.g. a CUTSCENE_CALLBACK) to implement
* flow control. Asserts if no cutscene is running or no marker with
* that name exists in it.
*
* @param name Marker name to search for, matched with stringEquals.
*/
void cutsceneGoTo(const char_t *name);
/** /**
* Update the cutscene system for one frame. * Update the cutscene system for one frame.
*/ */
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
cutsceneitem.c cutsceneitem.c
cutscenecallback.c cutscenecallback.c
cutsceneprint.c
) )
add_subdirectory(control) add_subdirectory(control)
@@ -15,3 +16,4 @@ add_subdirectory(item)
add_subdirectory(maparea) add_subdirectory(maparea)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(battle) add_subdirectory(battle)
add_subdirectory(save)
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutscenewait.c cutscenewait.c
cutscenesetpause.c cutscenesetpause.c
cutsceneconcurrent.c cutsceneconcurrent.c
cutscenemarker.c
cutscenerestart.c
cutscenescene.c
) )
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
void cutsceneMarkerStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneMarkerUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
const char_t *name;
} cutscenemarker_t;
/**
* Starts a marker item. A marker does nothing on its own - it exists
* purely as a named position for cutsceneGoTo to jump to.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMarkerStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a marker item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMarkerUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneRestartStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
cutsceneRestart();
}
bool_t cutsceneRestartUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* Starts a restart item (restarts the currently running cutscene from
* its first item via cutsceneRestart).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneRestartStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a restart item. By the time this would run, the cutscene has
* already restarted from its first item, so this always reports
* incomplete (mirrors cutsceneCutsceneUpdate).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneRestartUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "scene/scene.h"
void cutsceneSceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
sceneSet(item->sceneChange.type);
}
bool_t cutsceneSceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "scene/scenetype.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
scenetype_t type;
} cutscenescene_t;
/**
* Starts a scene item (requests a switch to the given scene via
* sceneSet). The switch itself doesn't happen until the next
* sceneUpdate() tick, so the rest of the current frame - including
* whatever else this cutscene does after this item - still runs
* against the scene that's being left.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneSceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a scene item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneSceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+50
View File
@@ -127,6 +127,56 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = { [CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
.init = cutsceneBattleForceActionStart, .init = cutsceneBattleForceActionStart,
.update = cutsceneBattleForceActionUpdate .update = cutsceneBattleForceActionUpdate
},
[CUTSCENE_ITEM_TYPE_MODAL] = {
.init = cutsceneModalStart,
.update = cutsceneModalUpdate
},
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
.init = cutsceneModalOptionsMarkersStart,
.update = cutsceneModalOptionsMarkersUpdate
},
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
.init = cutsceneModalCloseStart,
.update = cutsceneModalCloseUpdate
},
[CUTSCENE_ITEM_TYPE_PRINT] = {
.init = cutscenePrintStart,
.update = cutscenePrintUpdate
},
[CUTSCENE_ITEM_TYPE_MARKER] = {
.init = cutsceneMarkerStart,
.update = cutsceneMarkerUpdate
},
[CUTSCENE_ITEM_TYPE_RESTART] = {
.init = cutsceneRestartStart,
.update = cutsceneRestartUpdate
},
[CUTSCENE_ITEM_TYPE_SCENE] = {
.init = cutsceneSceneStart,
.update = cutsceneSceneUpdate
},
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
.init = cutsceneSaveDeviceCheckStart,
.update = cutsceneSaveDeviceCheckUpdate
},
[CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = {
.init = cutsceneSaveLoadAllSlotsStart,
.update = cutsceneSaveLoadAllSlotsUpdate
},
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
.init = cutsceneKeyboardStart,
.update = cutsceneKeyboardUpdate
} }
}; };
+27
View File
@@ -7,9 +7,13 @@
#pragma once #pragma once
#include "cutscenecallback.h" #include "cutscenecallback.h"
#include "cutsceneprint.h"
#include "control/cutscenewait.h" #include "control/cutscenewait.h"
#include "control/cutscenesetpause.h" #include "control/cutscenesetpause.h"
#include "control/cutsceneconcurrent.h" #include "control/cutsceneconcurrent.h"
#include "control/cutscenemarker.h"
#include "control/cutscenerestart.h"
#include "control/cutscenescene.h"
#include "entity/cutsceneentityteleport.h" #include "entity/cutsceneentityteleport.h"
#include "entity/cutsceneentitywalkto.h" #include "entity/cutsceneentitywalkto.h"
#include "entity/cutsceneentityremove.h" #include "entity/cutsceneentityremove.h"
@@ -22,6 +26,9 @@
#include "ui/cutscenefade.h" #include "ui/cutscenefade.h"
#include "ui/cutsceneemoji.h" #include "ui/cutsceneemoji.h"
#include "ui/cutsceneshake.h" #include "ui/cutsceneshake.h"
#include "ui/cutscenemodal.h"
#include "ui/cutscenemodaloptionsmarkers.h"
#include "ui/cutscenekeyboard.h"
#include "item/cutsceneitemgive.h" #include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemaparearemove.h"
@@ -29,6 +36,8 @@
#include "battle/cutscenestartbattle.h" #include "battle/cutscenestartbattle.h"
#include "battle/cutscenebattlewaitstate.h" #include "battle/cutscenebattlewaitstate.h"
#include "battle/cutscenebattleforceaction.h" #include "battle/cutscenebattleforceaction.h"
#include "save/cutscenesavedevicecheck.h"
#include "save/cutscenesaveloadallslots.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
@@ -59,6 +68,16 @@ typedef enum {
CUTSCENE_ITEM_TYPE_SHAKE, CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
CUTSCENE_ITEM_TYPE_MODAL,
CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
CUTSCENE_ITEM_TYPE_PRINT,
CUTSCENE_ITEM_TYPE_MARKER,
CUTSCENE_ITEM_TYPE_RESTART,
CUTSCENE_ITEM_TYPE_SCENE,
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
CUTSCENE_ITEM_TYPE_KEYBOARD,
CUTSCENE_ITEM_TYPE_COUNT CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t; } cutsceneitemtype_t;
@@ -91,6 +110,14 @@ struct cutsceneitem_s {
cutsceneshake_t shake; cutsceneshake_t shake;
cutscenebattlewaitstate_t battleWaitState; cutscenebattlewaitstate_t battleWaitState;
cutscenebattleforceaction_t battleForceAction; cutscenebattleforceaction_t battleForceAction;
cutscenemodal_t modal;
cutscenemodaloptionsmarkers_t modalOptionsMarkers;
cutsceneprint_t print;
cutscenemarker_t marker;
cutscenescene_t sceneChange;
cutscenesavedevicecheck_t saveDeviceCheck;
cutscenesaveloadallslots_t saveLoadAllSlots;
cutscenekeyboard_t keyboard;
}; };
}; };
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "cutsceneitem.h"
#include "console/console.h"
void cutscenePrintStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
consolePrint("%s", item->print.text);
}
bool_t cutscenePrintUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_PRINT_MAX_CHARS 128
typedef struct {
char_t text[CUTSCENE_PRINT_MAX_CHARS];
} cutsceneprint_t;
/**
* Starts a print item (prints the item's text to the console).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutscenePrintStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a print item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutscenePrintUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenesavedevicecheck.c
cutscenesaveloadallslots.c
)
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "save/save.h"
void cutsceneSaveDeviceCheckCallback(savedevice_t *device, void *user) {
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
cutsceneGoTo(
device != NULL ?
item->saveDeviceCheck.successMarker :
item->saveDeviceCheck.failureMarker
);
}
void cutsceneSaveDeviceCheckStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
saveFindAvailableDevice(cutsceneSaveDeviceCheckCallback, NULL);
}
bool_t cutsceneSaveDeviceCheckUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
const char_t *successMarker;
const char_t *failureMarker;
} cutscenesavedevicecheck_t;
/**
* Starts a save-device-check item: (re)requests an available save
* device via saveFindAvailableDevice. Never completes on its own - see
* cutsceneSaveDeviceCheckUpdate.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneSaveDeviceCheckStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a save-device-check item. Like a CUTSCENE_MODAL_OPTIONS item,
* this never completes on its own - once saveFindAvailableDevice's
* callback fires, it jumps straight to the item's successMarker or
* failureMarker via cutsceneGoTo.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneSaveDeviceCheckUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "save/save.h"
void cutsceneSaveLoadAllSlotsStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
errorret_t result = saveLoadAllSlots();
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
cutsceneGoTo(item->saveLoadAllSlots.failureMarker);
return;
}
cutsceneGoTo(item->saveLoadAllSlots.successMarker);
}
bool_t cutsceneSaveLoadAllSlotsUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
const char_t *successMarker;
const char_t *failureMarker;
} cutscenesaveloadallslots_t;
/**
* Starts a save-load-all-slots item: calls saveLoadAllSlots() and jumps
* straight to the item's successMarker or failureMarker via
* cutsceneGoTo, depending on the result.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneSaveLoadAllSlotsStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a save-load-all-slots item. Never completes on its own -
* Start already jumps to whichever marker applies before this would
* ever run.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneSaveLoadAllSlotsUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -11,4 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutscenefade.c cutscenefade.c
cutsceneemoji.c cutsceneemoji.c
cutsceneshake.c cutsceneshake.c
cutscenemodal.c
cutscenemodaloptionsmarkers.c
cutscenekeyboard.c
) )
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/dialog/keyboard/uikeyboard.h"
void cutsceneKeyboardStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uiKeyboardOpen(&item->keyboard.open);
}
bool_t cutsceneKeyboardUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(uiKeyboardIsOpen()) return false;
cutsceneSystemSetTextCache(UI_KEYBOARD.text);
return true;
}
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "ui/dialog/keyboard/uikeyboard.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
// Passed straight through to uiKeyboardOpen - see uikeyboardopen_t.
uikeyboardopen_t open;
} cutscenekeyboard_t;
/**
* Starts a keyboard item (opens the on-screen keyboard with the item's
* open parameters, unmodified - see uiKeyboardOpen).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneKeyboardStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a keyboard item. Blocks the cutscene while the keyboard is
* open; once it closes (confirmed or cancelled), caches its resulting
* text via cutsceneSystemSetTextCache (see cutsceneSystemGetTextCache)
* before completing, regardless of whether confirm or cancel was
* picked - check UI_KEYBOARD.confirmed from the item's own onInput
* callback if that distinction matters.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the keyboard has closed; false while still open.
*/
bool_t cutsceneKeyboardUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "assert/assert.h"
#include "ui/widget/uimodal.h"
void cutsceneModalStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
const cutscenemodal_t *modal = &item->modal;
assertTrue(
modal->optionCount <= CUTSCENE_MODAL_OPTIONS_MAX,
"Too many options for cutscene modal"
);
uiModalOpen(
modal->title,
modal->message,
modal->options,
modal->optionCount,
modal->callback,
NULL,
CUTSCENE_SYSTEM.userData
);
}
bool_t cutsceneModalUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return item->modal.optionCount == 0;
}
void cutsceneModalCloseStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uiModalClose(NULL);
}
bool_t cutsceneModalCloseUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_MODAL_TITLE_MAX_CHARS 64
#define CUTSCENE_MODAL_MESSAGE_MAX_CHARS 256
#define CUTSCENE_MODAL_OPTIONS_MAX 4
/**
* Callback invoked with the selected option once a cutscene modal
* item's dialog closes. Back/cancel input is disabled while it's open
* (see uiMenuSetDisableBack), so it must be dismissed by picking one.
*
* @param optionIndex Index into the options array that was selected.
* @param userData CUTSCENE_SYSTEM.userData for the running cutscene.
*/
typedef void (*cutscenemodaloptioncallback_t)(
const uint8_t optionIndex, void *userData
);
typedef struct {
// Display text, or a locale message ID to translate - see
// uiModalLocalize.
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
// NOT copied by this struct - these pointers are only read once,
// while uiModalOpen is opening the modal in cutsceneModalStart, so
// they only need to stay valid until then (e.g. string literals, as
// CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
// locale message ID, same as title/message.
const char_t **options;
uint8_t optionCount;
cutscenemodaloptioncallback_t callback;
} cutscenemodal_t;
/**
* Starts a modal item (shows the modal dialog with the item's title,
* message, and options, wiring callback to fire with the selected
* option once it closes). optionCount may be 0 for a message-only
* dialog with no option buttons.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneModalStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a modal item. A message-only modal (optionCount == 0, e.g.
* from CUTSCENE_MODAL) always completes immediately - the cutscene
* continues on to whatever follows this item right away, it does not
* wait for the dialog to be dismissed. Script what should happen while
* it's up, and how it gets closed, as later items in the same cutscene
* (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE).
*
* A modal with options (optionCount > 0, e.g. from
* CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene
* blocks here indefinitely. The option callback fires once the user
* picks something, and it alone is responsible for moving the cutscene
* on from there (typically via cutsceneGoTo or cutsceneRestart).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once immediately for a message-only modal; false
* always for a modal with options.
*/
bool_t cutsceneModalUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Starts a modal-close item (closes the currently open modal, if any).
* Whatever should happen next belongs in the cutscene's own item
* sequence (e.g. a CUTSCENE_CALLBACK or CUTSCENE_PRINT placed right
* after this item), not on this item itself.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneModalCloseStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a modal-close item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneModalCloseUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/widget/uimodal.h"
void cutsceneModalOptionsMarkersCallback(
const uint8_t optionIndex,
void *userData
) {
const cutscenemodaloptionsmarkers_t *options =
&cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
// uiModalOpen disables back/cancel for a dialog with options, so
// UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
// the last option's marker only matters if something ever force-closes
// the modal directly (uiModalClose) before an option is picked.
const uint8_t index =
optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
cutsceneGoTo(options->markers[index]);
}
void cutsceneModalOptionsMarkersStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
const cutscenemodaloptionsmarkers_t *options = &item->modalOptionsMarkers;
uiModalOpen(
options->title,
options->message,
options->options,
options->optionCount,
cutsceneModalOptionsMarkersCallback,
NULL,
CUTSCENE_SYSTEM.userData
);
}
bool_t cutsceneModalOptionsMarkersUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
// Backs CUTSCENE_MODAL_OPTIONS_ONE and CUTSCENE_MODAL_OPTIONS_TWO, so two
// is the most either macro ever needs.
#define CUTSCENE_MODAL_OPTIONS_MARKERS_MAX 2
typedef struct {
// Display text, or a locale message ID to translate - see
// uiModalLocalize.
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
// Each option's display text/locale message ID, and the marker to
// cutsceneGoTo when that option is selected.
const char_t *options[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
const char_t *markers[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
uint8_t optionCount;
} cutscenemodaloptionsmarkers_t;
/**
* Starts a modal-options-markers item (shows the modal dialog with the
* item's title, message, and options).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneModalOptionsMarkersStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a modal-options-markers item. Never completes on its own -
* the cutscene blocks here indefinitely. Back/cancel input is disabled
* while it's open (see uiMenuSetDisableBack), so it must be dismissed
* by picking an option, which jumps straight to that option's marker
* via cutsceneGoTo.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneModalOptionsMarkersUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+1 -1
View File
@@ -11,7 +11,7 @@
#include "util/memory.h" #include "util/memory.h"
#include "time/time.h" #include "time/time.h"
#include "ui/focus/uifocus.h" #include "ui/focus/uifocus.h"
#include "ui/frame/game/uigamemenu.h" #include "ui/screen/game/uigamemenu.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
void playerInit(entity_t *entity) { void playerInit(entity_t *entity) {
+26 -17
View File
@@ -11,7 +11,6 @@
#include "asset/asset.h" #include "asset/asset.h"
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "console/console.h" #include "console/console.h"
#include "event/event.h"
#include "util/string.h" #include "util/string.h"
#include "rpg/entity/global/entityglobal.h" #include "rpg/entity/global/entityglobal.h"
#include "rpg/entity/item/entityitem.h" #include "rpg/entity/item/entityitem.h"
@@ -137,8 +136,10 @@ void mapChunkUnload(chunk_t *chunk) {
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas)); memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); chunk->dcfEntry->onLoaded = NULL;
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); chunk->dcfEntry->onLoadedUser = NULL;
chunk->dcfEntry->onError = NULL;
chunk->dcfEntry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
@@ -159,8 +160,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
mapChunkLoadingSlotClear(chunk); mapChunkLoadingSlotClear(chunk);
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); chunk->dcfEntry->onLoaded = NULL;
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); chunk->dcfEntry->onLoadedUser = NULL;
chunk->dcfEntry->onError = NULL;
chunk->dcfEntry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
@@ -244,8 +247,12 @@ void mapChunkLoadNext() {
continue; continue;
} }
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk); assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber.");
eventSubscribe(&entry->onError, mapChunkLoadError, chunk); assertNull(entry->onError, "Entry already has an onError subscriber.");
entry->onLoaded = mapChunkLoaded;
entry->onLoadedUser = chunk;
entry->onError = mapChunkLoadError;
entry->onErrorUser = chunk;
} }
} }
@@ -385,10 +392,9 @@ void mapRebuildChunkOrder() {
} }
} }
void mapChunkLoadError(void *params, void *user) { void mapChunkLoadError(assetentry_t *entry, void *user) {
assertNotNull(params, "mapChunkLoadError: params cannot be NULL"); assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL");
assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != entry) return; if(chunk->dcfEntry != entry) return;
consolePrint( consolePrint(
@@ -397,8 +403,10 @@ void mapChunkLoadError(void *params, void *user) {
(int32_t)chunk->position.y, (int32_t)chunk->position.y,
(int32_t)chunk->position.z (int32_t)chunk->position.z
); );
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); entry->onLoaded = NULL;
eventUnsubscribe(&entry->onError, mapChunkLoadError); entry->onLoadedUser = NULL;
entry->onError = NULL;
entry->onErrorUser = NULL;
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
@@ -407,10 +415,9 @@ void mapChunkLoadError(void *params, void *user) {
mapChunkLoadNext(); mapChunkLoadNext();
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(assetentry_t *entry, void *user) {
assertNotNull(params, "mapChunkLoaded: params cannot be NULL"); assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL");
assertNotNull(user, "mapChunkLoaded: user cannot be NULL"); assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != entry) return; if(chunk->dcfEntry != entry) return;
// consolePrint( // consolePrint(
@@ -457,8 +464,10 @@ void mapChunkLoaded(void *params, void *user) {
// modelEntries must still be intact for that next reuse to copy from. // modelEntries must still be intact for that next reuse to copy from.
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
} }
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); entry->onLoaded = NULL;
eventUnsubscribe(&entry->onError, mapChunkLoadError); entry->onLoadedUser = NULL;
entry->onError = NULL;
entry->onErrorUser = NULL;
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
// chunk asset entry (and therefore its model locks) alive for as long as // chunk asset entry (and therefore its model locks) alive for as long as
// this chunk_t is displaying it. Released in mapChunkUnload instead. // this chunk_t is displaying it. Released in mapChunkUnload instead.
+4 -4
View File
@@ -101,19 +101,19 @@ void mapChunkLoadQueueRemove(chunk_t *chunk);
* chunk tiles with TILE_SHAPE_GROUND as a fallback. * chunk tiles with TILE_SHAPE_GROUND as a fallback.
* Always invoked on the main thread. * Always invoked on the main thread.
* *
* @param params The failed assetentry_t. * @param entry The failed assetentry_t.
* @param user The chunk_t that owns the entry. * @param user The chunk_t that owns the entry.
*/ */
void mapChunkLoadError(void *params, void *user); void mapChunkLoadError(assetentry_t *entry, void *user);
/** /**
* Callback invoked when a chunk DCF asset finishes loading. * Callback invoked when a chunk DCF asset finishes loading.
* Always invoked on the main thread. * Always invoked on the main thread.
* *
* @param params The loaded assetentry_t. * @param entry The loaded assetentry_t.
* @param user The chunk_t that owns the entry. * @param user The chunk_t that owns the entry.
*/ */
void mapChunkLoaded(void *params, void *user); void mapChunkLoaded(assetentry_t *entry, void *user);
/** /**
* Rebuilds chunkOrder from the loaded chunks that fall within the * Rebuilds chunkOrder from the loaded chunks that fall within the
+18 -6
View File
@@ -65,16 +65,18 @@ errorret_t saveUpdate() {
} else { } else {
// Device was found, cache in the data. // Device was found, cache in the data.
errorChain(saveLoadSettings()); errorChain(saveLoadSettings());
// Reset each slot and try load from file, updating cache along the way. errorChain(saveLoadAllSlots());
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
SAVE.slotCurrent = i;
saveSlotInit(&SAVE.slot);
errorChain(saveLoadSlot());// Load slot updates the cache.
}
// Reset slot, implying no slot has been selected. // Reset slot, implying no slot has been selected.
SAVE.slotCurrent = 0xFF; SAVE.slotCurrent = 0xFF;
// TEST: write settings back out immediately once a device is
// selected, to confirm the write path actually works on hardware.
errorChain(saveDeviceSettingsWrite(
&SAVE.devices[SAVE.deviceCurrent],
&SAVE.settings
));
SAVE.findingAvailableDevice = false; SAVE.findingAvailableDevice = false;
SAVE.findAvailableCallback( SAVE.findAvailableCallback(
&SAVE.devices[SAVE.deviceCurrent], &SAVE.devices[SAVE.deviceCurrent],
@@ -222,6 +224,16 @@ errorret_t saveLoadSlot() {
errorOk(); errorOk();
} }
errorret_t saveLoadAllSlots() {
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
SAVE.slotCurrent = i;
saveSlotInit(&SAVE.slot);
errorChain(saveLoadSlot());// Load slot updates the cache.
}
errorOk();
}
errorret_t saveDispose() { errorret_t saveDispose() {
// Dispose each device. // Dispose each device.
savedevice_t *device = &SAVE.devices[0]; savedevice_t *device = &SAVE.devices[0];
+9
View File
@@ -95,6 +95,15 @@ errorret_t saveSaveSlot();
*/ */
errorret_t saveLoadSlot(); errorret_t saveLoadSlot();
/**
* Resets and loads every slot from the current device, updating the caches
* along the way. Leaves SAVE.slotCurrent pointing at the last slot index
* once done.
*
* @return Error state if any.
*/
errorret_t saveLoadAllSlots();
/** /**
* Disposes of the save system. * Disposes of the save system.
* *
+2
View File
@@ -11,7 +11,9 @@
#include "yyjson.h" #include "yyjson.h"
#define SAVE_SLOT_NAME_LENGTH 8 #define SAVE_SLOT_NAME_LENGTH 8
#ifndef SAVE_SLOT_COUNT
#define SAVE_SLOT_COUNT 3 #define SAVE_SLOT_COUNT 3
#endif
typedef struct { typedef struct {
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
+26 -28
View File
@@ -9,42 +9,40 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "error/error.h" #include "error/error.h"
#include "display/screen/screen.h"
#include "console/console.h" #include "console/console.h"
#include "save/save.h" #include "rpg/cutscene/cutscenesystem.h"
#include "asset/asset.h"
int32_t testData = 69; // Loaded lazily and kept resident for the rest of the process - this scene
// only ever runs once at boot, but there's no reason to unlock it (same
void testCallback(savedevice_t *device, void *user) { // lifetime convention as e.g. LOCALE.entry).
if(device == NULL) { static assetentry_t *INITIAL_CUTSCENE_ENTRY = NULL;
consolePrint("No save device found.");
} else {
uint8_t index = (uint8_t)(device - &SAVE.devices[0]);
consolePrint(
"Found save device %u: %s", (uint32_t)index, device->reasonKey
);
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
if(saveSlotInUse(&SAVE.caches[i])) {
consolePrint(
"Slot %u is in use: %s", (uint32_t)i, SAVE.caches[i].name
);
} else {
consolePrint("Slot %u is not in use.", (uint32_t)i);
}
}
}
}
errorret_t sceneInitialInit(scenedata_t *sceneData) { errorret_t sceneInitialInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
memoryZero(&sceneData->initial, sizeof(sceneinitial_t)); memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
consolePrint("Going to find a save device."); // Set background color to black for the initial scene
saveFindAvailableDevice( SCREEN.background = COLOR_BLACK;
testCallback,
&testData // Runtime-loaded from assets/cutscenes/initial.cts (authored at
// assetsraw/cutscenes/initial.jsonc via `python3 -m tools.asset.cutscene`)
// - checks for a save device, retrying on failure, then hands off to the
// main menu scene via a plain CUTSCENE_SCENE item (no native callback
// needed here, unlike the main menu's own start-game cutscene).
if(INITIAL_CUTSCENE_ENTRY == NULL) {
INITIAL_CUTSCENE_ENTRY = assetLock(
"cutscenes/initial.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
); );
}
errorret_t result = assetRequireLoaded(INITIAL_CUTSCENE_ENTRY);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
assertTrue(false, "Failed to load initial scene cutscene asset");
}
cutsceneSystemStartCutscene(&INITIAL_CUTSCENE_ENTRY->data.cutscene.cutscene);
errorOk(); errorOk();
} }
+2 -4
View File
@@ -8,11 +8,9 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
// No per-scene state needed - the save globals and the two modal UI
// elements (see ui/frame/initial/) carry everything this scene cares
// about. A byte placeholder keeps the struct non-empty for portability.
typedef struct { typedef struct {
uint8_t reserved; // uint32_t callbackState;
void *nothing;
} sceneinitial_t; } sceneinitial_t;
/** /**
+57
View File
@@ -6,8 +6,65 @@
*/ */
#include "scenemainmenu.h" #include "scenemainmenu.h"
#include "ui/screen/mainmenu/uimainmenu.h"
#include "ui/dialog/save/uiselectsave.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "scene/scene.h"
#include "asset/asset.h"
#include "assert/assert.h"
// Loaded lazily on first Start Game click and kept resident for the rest
// of the process - it's tiny and reused every time, so there's no benefit
// to unlocking/reloading it between attempts (same lifetime convention as
// e.g. LOCALE.entry).
static assetentry_t *MAIN_MENU_START_GAME_CUTSCENE_ENTRY = NULL;
void sceneMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) {
uiMainMenuOpen();
return;
}
// TODO: load/start the game using the chosen save slot.
sceneSet(SCENE_TYPE_OVERWORLD);
}
void sceneMainMenuOpenSelectSave(void *userData) {
uiSelectSaveOpen(
UI_SELECT_SAVE_TYPE_LOAD, sceneMainMenuSelectSaveResult, NULL
);
}
// Loads every save slot before opening the load-game picker (checking/
// retry-on-error shape mirroring the initial scene's device lookup, see
// scene/initial/sceneinitial.c) - runtime-loaded from
// assets/cutscenes/main_menu_start_game.cts (authored at
// assetsraw/cutscenes/main_menu_start_game.json via
// `python3 -m tools.asset.cutscene`) rather than compiled in, since it's
// player-facing flow rather than core engine wiring. The file ends at the
// LOADED marker with no further action - sceneMainMenuOpenSelectSave is
// armed as the completion callback below instead of being baked into the
// cutscene itself, since a file can't store a native function pointer.
void sceneMainMenuStartGame(void) {
if(MAIN_MENU_START_GAME_CUTSCENE_ENTRY == NULL) {
MAIN_MENU_START_GAME_CUTSCENE_ENTRY = assetLock(
"cutscenes/main_menu_start_game.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
);
}
errorret_t result = assetRequireLoaded(MAIN_MENU_START_GAME_CUTSCENE_ENTRY);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
assertTrue(false, "Failed to load main menu start-game cutscene asset");
}
cutsceneSystemStartCutscene(
&MAIN_MENU_START_GAME_CUTSCENE_ENTRY->data.cutscene.cutscene
);
cutsceneSystemSetOnComplete(sceneMainMenuOpenSelectSave);
}
errorret_t sceneMainMenuInit(scenedata_t *sceneData) { errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
uiMainMenuOpen();
errorOk(); errorOk();
} }
+13 -7
View File
@@ -8,16 +8,14 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
// Empty for now -- the menu itself lives in ui/frame/mainmenu/uimainmenu.c, // Empty - the menu itself lives in ui/screen/mainmenu/uimainmenu.c. A byte
// driven by the global UI element pipeline (see uimainmenu.c's own // placeholder keeps the struct non-empty for portability.
// SCENE.current check for when it shows itself). A byte placeholder keeps
// the struct non-empty for portability.
typedef struct { typedef struct {
uint8_t reserved; uint8_t reserved;
} scenemainmenu_t; } scenemainmenu_t;
/** /**
* Initializes the main menu scene. * Initializes the main menu scene by opening the main menu panel.
* *
* @param sceneData The scene data used for this scene. * @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded. * @return An error if the init failed, or errorOk() if it succeeded.
@@ -25,7 +23,15 @@ typedef struct {
errorret_t sceneMainMenuInit(scenedata_t *sceneData); errorret_t sceneMainMenuInit(scenedata_t *sceneData);
/** /**
* Updates the main menu scene. Currently a no-op -- the menu drives * Starts the "Start Game" flow: runs a cutscene that loads every save
* slot (retrying on error, prompting if no save device is found) and
* opens the load-game picker once it succeeds. Called by
* ui/screen/mainmenu/uimainmenu.c when the player selects Start Game.
*/
void sceneMainMenuStartGame(void);
/**
* Updates the main menu scene. Currently a no-op - the menu drives
* itself via the global UI element pipeline. * itself via the global UI element pipeline.
* *
* @param sceneData The scene data used for this scene. * @param sceneData The scene data used for this scene.
@@ -34,7 +40,7 @@ errorret_t sceneMainMenuInit(scenedata_t *sceneData);
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData); errorret_t sceneMainMenuUpdate(scenedata_t *sceneData);
/** /**
* Renders the main menu scene. Currently a no-op -- the menu draws * Renders the main menu scene. Currently a no-op - the menu draws
* itself via the global UI element pipeline. * itself via the global UI element pipeline.
* *
* @param sceneData The scene data used for this scene. * @param sceneData The scene data used for this scene.
+2 -1
View File
@@ -4,7 +4,8 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
add_subdirectory(debug) add_subdirectory(debug)
add_subdirectory(frame) add_subdirectory(dialog)
add_subdirectory(screen)
add_subdirectory(focus) add_subdirectory(focus)
add_subdirectory(overlay) add_subdirectory(overlay)
add_subdirectory(rpg) add_subdirectory(rpg)
+12
View File
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uiconfirm.c
)
add_subdirectory(save)
add_subdirectory(keyboard)
@@ -5,5 +5,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
event.c uikeyboard.c
) )
+601
View File
@@ -0,0 +1,601 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uikeyboard.h"
#include "ui/widget/uiframe.h"
#include "ui/widget/uibutton.h"
#include "ui/dialog/uiconfirm.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/color.h"
#include "display/text/font.h"
#include "display/spritebatch/spritebatch.h"
#include "time/time.h"
// Key cell size, expressed in font tiles so it scales if the font ever
// does. Row height is derived from this ratio in uiKeyboardDraw rather
// than being its own fixed constant, so a key cell keeps its proportions
// even when contentWidth is stretched wider by a long title/text label.
#define UI_KEYBOARD_KEY_WIDTH ((float_t)(FONT_DEFAULT_TILE_WIDTH * 4))
#define UI_KEYBOARD_KEY_HEIGHT ((float_t)(FONT_DEFAULT_TILE_HEIGHT * 2))
uikeyboard_t UI_KEYBOARD;
// String literals have static storage duration, so these stay valid for
// as long as the process runs - safe for uiButtonInit's non-copying
// label contract. "" cells are blank filler/reserved slots, NULL is
// shorthand for the same thing where there's no key at all - see
// uikeyboard.h and uiKeyboardNormalizeKey.
//
// Unshifted, caps off - also the canonical table (see
// uiKeyboardGetCanonicalKey).
static const char_t *const UI_KEYBOARD_KEYS_NONE[UI_KEYBOARD_KEY_COUNT] = {
// 1 2 3 4 5 6 7 8 9 0 DEL
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
UI_KEYBOARD_KEY_BACKSPACE,
// q w e r t y u i o p
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", NULL,
// CAPS a s d f g h j k l (NEWLINE)
UI_KEYBOARD_KEY_CAPS,
"a", "s", "d", "f", "g", "h", "j", "k", "l", NULL,
// SHIFT z x c v b n m SHIFT
UI_KEYBOARD_KEY_SHIFT,
NULL,
"z", "x", "c", "v", "b", "n", "m",
NULL,
UI_KEYBOARD_KEY_SHIFT,
// (CANCEL) SPACE (CONFIRM)
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
NULL
};
// Caps on - letters uppercase; caps doesn't affect digits/symbols on a
// physical keyboard, so this table is otherwise identical to _NONE.
static const char_t *const UI_KEYBOARD_KEYS_CAPS[UI_KEYBOARD_KEY_COUNT] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
UI_KEYBOARD_KEY_BACKSPACE,
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
UI_KEYBOARD_KEY_CAPS,
"A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
UI_KEYBOARD_KEY_SHIFT,
NULL,
"Z", "X", "C", "V", "B", "N", "M",
NULL,
UI_KEYBOARD_KEY_SHIFT,
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
NULL
};
// Shift held - letters uppercase, digits become their US-layout shift
// symbols, '-'/'=' become '_'/'+'. Takes priority over caps (see
// uiKeyboardGetChar).
static const char_t *const UI_KEYBOARD_KEYS_SHIFT[UI_KEYBOARD_KEY_COUNT] = {
"!", "@", "#", "$", "%", "^", "&", "*", "(", ")",
UI_KEYBOARD_KEY_BACKSPACE,
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
UI_KEYBOARD_KEY_CAPS,
"A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
UI_KEYBOARD_KEY_SHIFT,
NULL,
"Z", "X", "C", "V", "B", "N", "M",
NULL,
UI_KEYBOARD_KEY_SHIFT,
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
NULL
};
// Collapses a table entry's NULL ("no key here") down to "" (blank
// filler), the only form of "nothing" callers need to handle.
const char_t * uiKeyboardNormalizeKey(const char_t *key) {
return key != NULL ? key : "";
}
// Shift takes priority over caps - there's no fourth table for "shift
// while caps is on", unlike a physical keyboard.
const char_t *const * uiKeyboardSelectTable(
const bool_t shift,
const bool_t caps
) {
if(shift) return UI_KEYBOARD_KEYS_SHIFT;
if(caps) return UI_KEYBOARD_KEYS_CAPS;
return UI_KEYBOARD_KEYS_NONE;
}
uint8_t uiKeyboardBuildItems(
uimenuitem_t *items,
const bool_t shift,
const bool_t caps
) {
assertNotNull(items, "Items cannot be NULL");
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
items[i].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&items[i].button, uiKeyboardNormalizeKey(table[i]));
}
return UI_KEYBOARD_KEY_COUNT;
}
void uiKeyboardUpdateDisplay(
uimenuitem_t *items,
const bool_t shift,
const bool_t caps
) {
assertNotNull(items, "Items cannot be NULL");
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
// These reserved slots are blank in every shift/caps table - their
// real labels (NEWLINE/CANCEL/CONFIRM) are patched in once at open
// time and aren't affected by shift/caps, so leave them alone here.
if(
i == UI_KEYBOARD_NEWLINE_INDEX ||
i == UI_KEYBOARD_CANCEL_INDEX ||
i == UI_KEYBOARD_CONFIRM_INDEX
) continue;
items[i].button.label.text = uiKeyboardNormalizeKey(table[i]);
items[i].button.label.dirty = true;
const char_t *canonical = uiKeyboardGetCanonicalKey(i);
if(stringCompare(canonical, UI_KEYBOARD_KEY_CAPS) == 0) {
uiButtonSetActive(&items[i].button, caps);
} else if(stringCompare(canonical, UI_KEYBOARD_KEY_SHIFT) == 0) {
uiButtonSetActive(&items[i].button, shift);
}
}
}
const char_t * uiKeyboardGetCanonicalKey(const uint8_t index) {
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
return uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]);
}
bool_t uiKeyboardIsShiftSensitive(const uint8_t index) {
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
return stringCompare(
uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]),
uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_SHIFT[index])
) != 0;
}
char_t uiKeyboardGetChar(
const uint8_t index,
const bool_t shift,
const bool_t caps
) {
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
return uiKeyboardNormalizeKey(table[index])[0];
}
void uiKeyboardFocusConfirm(void) {
uiMenuSetPosition(
&UI_KEYBOARD.menu,
UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS,
UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS
);
}
bool_t uiKeyboardTextIsBlank(void) {
for(const char_t *c = UI_KEYBOARD.text; *c != '\0'; c++) {
if(!stringIsWhitespace(*c)) return false;
}
return true;
}
void uiKeyboardAppendChar(const char_t c) {
size_t length = strlen(UI_KEYBOARD.text);
// Full - drop the last character instead of ignoring the keypress, so
// the new one overrides it. Falls through to the normal append below,
// which now has room again.
if(length >= UI_KEYBOARD.maxLength) {
length--;
UI_KEYBOARD.text[length] = '\0';
}
if(c == '\n') {
uint8_t lines = 1;
for(size_t i = 0; i < length; i++) {
if(UI_KEYBOARD.text[i] == '\n') lines++;
}
if(lines >= UI_KEYBOARD.lineCount) return;
}
UI_KEYBOARD.text[length] = c;
UI_KEYBOARD.text[length + 1] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
// Nothing more can be typed - jump focus to confirm so accepting is a
// single button press away.
if(length + 1 >= UI_KEYBOARD.maxLength) uiKeyboardFocusConfirm();
}
void uiKeyboardBackspace(void) {
size_t length = strlen(UI_KEYBOARD.text);
if(length == 0) return;
UI_KEYBOARD.text[length - 1] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
}
void uiKeyboardMenuClosed(const uimenu_t *menu) {
if(UI_KEYBOARD.onInput != NULL) {
UI_KEYBOARD.onInput(
UI_KEYBOARD.text, UI_KEYBOARD.confirmed, UI_KEYBOARD.user
);
}
}
bool_t uiKeyboardMenuCancel(const uimenu_t *menu) {
if(strlen(UI_KEYBOARD.text) > 0) {
uiKeyboardBackspace();
return true;
}
// Nothing left to delete - close, but only if this dialog is even
// allowed to be cancelled. Closed explicitly (rather than returning
// false to fall through to uiFocusUpdate's default pop) since that
// path only pops the focus stack - it never clears UI_KEYBOARD.open,
// which would leave uiKeyboardDraw drawing a focus-less dialog forever
// and the next uiKeyboardOpen() tripping its already-open assert.
if(!UI_KEYBOARD.cancel) return true;
uiKeyboardDoCancel();
return true;
}
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user) {
if(!result) return;
UI_KEYBOARD.confirmed = true;
uiKeyboardClose();
}
void uiKeyboardDoCancel(void) {
if(UI_KEYBOARD.cancelConfirm) {
uiConfirmOpen(
UI_KEYBOARD.cancelConfirmQuestion,
uiKeyboardCancelConfirmSecondaryResult, NULL
);
return;
}
UI_KEYBOARD.confirmed = false;
uiKeyboardClose();
}
void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user) {
if(!result) return;
UI_KEYBOARD.confirmed = false;
uiKeyboardClose();
}
void uiKeyboardMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(index == UI_KEYBOARD_CONFIRM_INDEX) {
if(UI_KEYBOARD.trimmed) {
stringTrim(UI_KEYBOARD.text);
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
}
if(!UI_KEYBOARD.allowBlank && uiKeyboardTextIsBlank()) return;
if(UI_KEYBOARD.confirm) {
uiConfirmOpen(
UI_KEYBOARD.confirmQuestion, uiKeyboardConfirmSecondaryResult, NULL
);
return;
}
UI_KEYBOARD.confirmed = true;
uiKeyboardClose();
return;
}
if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) {
uiKeyboardDoCancel();
return;
}
if(index == UI_KEYBOARD_NEWLINE_INDEX && UI_KEYBOARD.lineCount > 1) {
if(
UI_KEYBOARD.onKeyPress != NULL &&
!UI_KEYBOARD.onKeyPress('\n', UI_KEYBOARD.user)
) return;
uiKeyboardAppendChar('\n');
return;
}
// The canonical (unshifted) key, independent of whatever character
// it's currently drawn as - see uiKeyboardGetCanonicalKey. Used to
// identify the key, not item->button.label.text, which changes with
// shift/caps.
const char_t *key = uiKeyboardGetCanonicalKey(index);
// Blank filler/reserved cell (includes an unused cancel/newline slot).
if(key[0] == '\0') return;
if(stringCompare(key, UI_KEYBOARD_KEY_BACKSPACE) == 0) {
uiKeyboardBackspace();
return;
}
if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) {
UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
uiKeyboardUpdateDisplay(
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
);
return;
}
if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) {
UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
uiKeyboardUpdateDisplay(
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
);
return;
}
char_t c;
if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) {
c = ' ';
} else {
c = uiKeyboardGetChar(index, UI_KEYBOARD.shift, UI_KEYBOARD.caps);
// Shift is a one-shot, consumed by any letter/digit/-/= it actually
// affected (and the display refreshed to drop back to whatever caps
// alone says); caps is a persistent toggle.
if(UI_KEYBOARD.shift && uiKeyboardIsShiftSensitive(index)) {
UI_KEYBOARD.shift = false;
uiKeyboardUpdateDisplay(
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
);
}
}
if(UI_KEYBOARD.onKeyPress != NULL && !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user)) {
return;
}
uiKeyboardAppendChar(c);
}
errorret_t uiKeyboardInit(void) {
memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t));
uiLabelInit(
&UI_KEYBOARD.titleLabel, UI_KEYBOARD.titleText,
UI_KEYBOARD.titleSprites, UI_KEYBOARD_TITLE_SPRITES_MAX
);
uiLabelInit(
&UI_KEYBOARD.textLabel, UI_KEYBOARD.text,
UI_KEYBOARD.textSprites, UI_KEYBOARD_TEXT_SPRITES_MAX
);
uiLabelInit(
&UI_KEYBOARD.cursorLabel, UI_KEYBOARD_CURSOR_TEXT,
UI_KEYBOARD.cursorSprites, UI_KEYBOARD_CURSOR_SPRITES_MAX
);
UI_KEYBOARD.cursorLabel.dirty = true;
errorOk();
}
bool_t uiKeyboardIsOpen(void) {
return UI_KEYBOARD.open;
}
void uiKeyboardOpen(const uikeyboardopen_t *open) {
assertNotNull(open, "Open parameters cannot be NULL");
assertFalse(UI_KEYBOARD.open, "Keyboard is already open");
assertTrue(
open->maxLength <= UI_KEYBOARD_TEXT_MAX - 1,
"maxLength exceeds UI_KEYBOARD_TEXT_MAX"
);
assertTrue(
open->lineCount <= UI_KEYBOARD_LINE_COUNT_MAX,
"lineCount exceeds UI_KEYBOARD_LINE_COUNT_MAX"
);
stringCopy(
UI_KEYBOARD.titleText,
open->title != NULL ? open->title : UI_KEYBOARD_TITLE_DEFAULT,
UI_KEYBOARD_TITLE_TEXT_MAX - 1
);
UI_KEYBOARD.titleLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.titleLabel);
UI_KEYBOARD.text[0] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
UI_KEYBOARD.onInput = open->onInput;
UI_KEYBOARD.onKeyPress = open->onKeyPress;
UI_KEYBOARD.cancel = open->cancel;
UI_KEYBOARD.maxLength = open->maxLength == 0 ?
UI_KEYBOARD_TEXT_MAX - 1 : open->maxLength;
UI_KEYBOARD.lineCount = open->lineCount == 0 ? 1 : open->lineCount;
UI_KEYBOARD.confirm = open->confirm;
stringCopy(
UI_KEYBOARD.confirmQuestion,
open->confirmLabel != NULL ?
open->confirmLabel : UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT,
UI_KEYBOARD_CONFIRM_QUESTION_MAX - 1
);
UI_KEYBOARD.trimmed = open->trimmed;
UI_KEYBOARD.allowBlank = open->allowBlank;
UI_KEYBOARD.user = open->user;
UI_KEYBOARD.confirmed = false;
UI_KEYBOARD.caps = false;
UI_KEYBOARD.shift = false;
UI_KEYBOARD.open = true;
// Builds the full fixed grid, including blank filler at the
// newline/confirm/cancel reserved slots - patched with real buttons
// below where applicable. Letters start lowercase and digits
// unshifted, matching caps/shift both being reset to false above.
uiKeyboardBuildItems(UI_KEYBOARD.items, false, false);
if(UI_KEYBOARD.lineCount > 1) {
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button,
UI_KEYBOARD_KEY_NEWLINE
);
}
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button,
UI_KEYBOARD_CONFIRM_LABEL
);
if(UI_KEYBOARD.cancel) {
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button,
UI_KEYBOARD_CANCEL_LABEL
);
}
uiMenuInit(
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
);
uiMenuSetItems(
&UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT,
UI_KEYBOARD_COLUMNS
);
// Back deletes a character rather than closing the dialog outright -
// see uiKeyboardMenuCancel.
uiMenuSetCancelCallback(&UI_KEYBOARD.menu, uiKeyboardMenuCancel);
uiMenuOpen(&UI_KEYBOARD.menu);
}
void uiKeyboardClose(void) {
if(!UI_KEYBOARD.open) return;
UI_KEYBOARD.open = false;
if(uiMenuIsActive(&UI_KEYBOARD.menu)) {
uiMenuClose(&UI_KEYBOARD.menu);
} else {
uiKeyboardMenuClosed(NULL);
}
}
errorret_t uiKeyboardDraw(void) {
if(!UI_KEYBOARD.open) errorOk();
float_t x = (float_t)SCREEN.scanX;
float_t y = (float_t)SCREEN.scanY;
float_t width = (float_t)SCREEN.scanWidth;
float_t height = (float_t)SCREEN.scanHeight;
errorChain(uiFrameDraw(x, y, width, height));
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
// When multi-line, reserve the full lineCount rows up front so the
// dialog doesn't resize as newlines are typed - otherwise fall back to
// the label's own measured height (floored at one row for an empty
// string, which measures to zero).
float_t textHeight = UI_KEYBOARD.lineCount > 1 ?
(float_t)UI_KEYBOARD.lineCount * rowHeight :
mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
uint8_t columns = UI_KEYBOARD.menu.columns;
uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns;
float_t contentWidth = mathMax(
mathMax((float_t)UI_KEYBOARD.titleLabel.width, (float_t)UI_KEYBOARD.textLabel.width),
(float_t)columns * UI_KEYBOARD_KEY_WIDTH
);
float_t colStep = contentWidth / (float_t)columns;
float_t keyRowHeight = colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH);
float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y
+ textHeight + UI_FRAME_PADDING_Y
+ ((float_t)rows * keyRowHeight);
// The frame now fills the whole screen, but the title/text/keys still
// lay out as one content block - center that block within the screen.
float_t contentX = x + (width - contentWidth) * 0.5f;
float_t contentY = y + (height - contentHeight) * 0.5f;
uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
errorChain(uiLabelRender(&UI_KEYBOARD.titleLabel, COLOR_WHITE));
float_t textY = contentY + rowHeight + UI_FRAME_PADDING_Y;
uiLabelSetX(&UI_KEYBOARD.textLabel, contentX);
uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE));
// Insertion point is always the end of text - there's no mid-string
// cursor movement - so find it by counting characters since the last
// newline (column) and newlines overall (line). Once maxLength is
// reached, a new key overrides the last character (see
// uiKeyboardAppendChar) rather than appending one, so stop one
// character short here too - the cursor should sit on that last
// character, not past it, to reflect what typing will actually do.
size_t textLength = strlen(UI_KEYBOARD.text);
size_t cursorLimit = textLength >= UI_KEYBOARD.maxLength ?
textLength - 1 : textLength;
uint8_t cursorLine = 0;
uint8_t cursorCol = 0;
for(size_t i = 0; i < cursorLimit; i++) {
if(UI_KEYBOARD.text[i] == '\n') { cursorLine++; cursorCol = 0; }
else cursorCol++;
}
if(mathModFloat(TIME.time, UI_KEYBOARD_CURSOR_BLINK_RATE * 2.0f) <
UI_KEYBOARD_CURSOR_BLINK_RATE
) {
uiLabelSetX(
&UI_KEYBOARD.cursorLabel,
contentX + (float_t)cursorCol * (float_t)FONT_DEFAULT_TILE_WIDTH
);
uiLabelSetY(&UI_KEYBOARD.cursorLabel, textY + (float_t)cursorLine * rowHeight);
errorChain(uiLabelRender(&UI_KEYBOARD.cursorLabel, COLOR_WHITE));
}
// Drawn directly rather than via uiMenuDraw - that helper hardcodes row
// spacing to the font's native tile height, which would keep the grid
// vertically compact no matter how much contentWidth grows. Focus
// highlighting is unaffected: it's applied to item->button.highlighted
// by the focus system independently of whoever calls uiButtonDraw.
float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y;
for(uint8_t i = 0; i < UI_KEYBOARD.menu.itemCount; i++) {
float_t ix = contentX + (float_t)(i % columns) * colStep;
float_t iy = menuY + (float_t)(i / columns) * keyRowHeight;
errorChain(uiButtonDraw(&UI_KEYBOARD.items[i].button, ix, iy));
}
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiKeyboardDispose(void) {
errorOk();
}
+490
View File
@@ -0,0 +1,490 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uimenu.h"
#include "display/text/font.h"
// A fixed 11x5 QWERTY grid - the only layout that exists (numbers/
// symbols layouts were removed as separate concepts; digits already
// live on this one):
// 1 2 3 4 5 6 7 8 9 0 DEL
// q w e r t y u i o p
// CAPS a s d f g h j k l (NEWLINE)
// SHIFT z x c v b n m SHIFT
// (CANCEL) SPACE (CONFIRM)
// Rows shorter than 11 columns are padded with blank filler buttons
// (empty label) so every key lands in its intended position - the menu/
// focus grid only supports uniform columns, it has no concept of a key
// spanning multiple cells or a row being narrower than the grid, so this
// is the only way to get real positioning out of it. A filler cell is
// still a focusable/navigable-to grid slot, it just does nothing when
// selected - see uiKeyboardMenuSelected's blank-label check. A
// UI_KEYBOARD_KEYS_* table may use NULL for a slot with no key at all -
// see uiKeyboardGetCanonicalKey, which normalizes it to the same blank
// filler behavior.
//
// NEWLINE/CANCEL/CONFIRM are reserved slots the tables always leave
// blank - uiKeyboardOpen patches them with real buttons depending on
// lineCount/cancel (CONFIRM is unconditional, just needed as a fixed
// slot addressable by constant).
#define UI_KEYBOARD_COLUMNS 11
#define UI_KEYBOARD_ROWS 5
#define UI_KEYBOARD_KEY_COUNT (UI_KEYBOARD_COLUMNS * UI_KEYBOARD_ROWS)
// Reserved slot indices uiKeyboardOpen patches with real buttons.
#define UI_KEYBOARD_NEWLINE_INDEX 32
#define UI_KEYBOARD_CANCEL_INDEX 44
#define UI_KEYBOARD_CONFIRM_INDEX 54
// Labels uiKeyboardMenuSelected checks for by pointer/string compare to
// tell special keys apart from an ordinary single-character key. A blank
// ("") label is a filler cell - see above. Identical across every
// UI_KEYBOARD_KEYS_* table, since these don't change with shift/caps.
#define UI_KEYBOARD_KEY_SPACE FONT_ICON_SPACE
#define UI_KEYBOARD_KEY_BACKSPACE FONT_ICON_BACKSPACE
#define UI_KEYBOARD_KEY_SHIFT FONT_ICON_SHIFT
#define UI_KEYBOARD_KEY_CAPS FONT_ICON_CAPSLOCK
// Only patched into UI_KEYBOARD_NEWLINE_INDEX when
// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank.
#define UI_KEYBOARD_KEY_NEWLINE FONT_ICON_NEWLINE
#define UI_KEYBOARD_TITLE_TEXT_MAX 64
#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
#define UI_KEYBOARD_TEXT_MAX 64
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
// The grid is a fixed size - only one layout exists today, so this is
// just its key count.
#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT
// Hardcoded for now - a uiModalOpen-style title/button text override is
// planned (see uikeyboardopen_t) but not built yet. Patched into the
// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than
// appended, since the grid is a fixed shape.
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
// Used when uikeyboardopen_t.confirmLabel is NULL.
#define UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT "Is this correct?"
#define UI_KEYBOARD_CANCEL_QUESTION_MAX 64
// Used when uikeyboardopen_t.cancelConfirmLabel is NULL.
#define UI_KEYBOARD_CANCEL_QUESTION_DEFAULT "Discard your changes?"
// Maximum number of lines uikeyboardopen_t.lineCount may request.
#define UI_KEYBOARD_LINE_COUNT_MAX 4
// Marks where the next typed character will be inserted (always the end
// of the current text - there's no mid-string cursor movement).
#define UI_KEYBOARD_CURSOR_TEXT "_"
#define UI_KEYBOARD_CURSOR_SPRITES_MAX 1
// Seconds per on/off half-cycle of the cursor blink.
#define UI_KEYBOARD_CURSOR_BLINK_RATE 0.5f
/**
* Callback invoked once the keyboard dialog is dismissed.
*
* @param text The entered text. Only meaningful when confirmed is true;
* points at UI_KEYBOARD's own buffer, so copy it out if it needs to
* outlive the next uiKeyboardOpen call.
* @param confirmed True if the confirm button was picked, false if
* cancelled.
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
*/
typedef void (*uikeyboardinputcallback_t)(
const char_t *text,
const bool_t confirmed,
void *user
);
/**
* Callback invoked before a key press is applied, giving the caller a
* chance to reject it - e.g. an email field rejecting "%".
*
* @param c The character about to be entered (SPACE included, DEL/
* confirm/cancel excluded - those aren't text being entered).
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
* @returns True to allow the character, false to reject it (ignored,
* nothing is entered).
*/
typedef bool_t (*uikeyboardkeypresscallback_t)(const char_t c, void *user);
/**
* Parameters for uiKeyboardOpen. Expect this to grow (e.g. button text
* overrides) as more dialogs start using the keyboard.
*/
typedef struct {
// Title text shown above the entered text; copied internally, safe to
// be transient. Displayed as-is - resolve any locale string yourself
// before passing it in. NULL uses UI_KEYBOARD_TITLE_DEFAULT ("ENTER
// YOUR TEXT").
const char_t *title;
// Fired once the dialog is dismissed, confirmed or cancelled. May be
// NULL.
uikeyboardinputcallback_t onInput;
// Fired for each key press before it's applied. May be NULL to allow
// everything.
uikeyboardkeypresscallback_t onKeyPress;
// Whether the cancel button is shown, and whether pressing back with
// no text entered closes the dialog - see uiKeyboardMenuCancel.
bool_t cancel;
// Maximum length of the entered text, in characters excluding the
// null terminator. 0 defaults to the buffer's full capacity
// (UI_KEYBOARD_TEXT_MAX - 1). Must not exceed that.
size_t maxLength;
// When true, picking confirm opens a second uiConfirm "are you sure"
// dialog (see confirmLabel) on top of the keyboard before actually
// closing it - picking Cancel there returns to the keyboard with the
// text untouched.
bool_t confirm;
// Question text/locale message ID for the second confirm dialog when
// confirm is true; copied internally, safe to be transient. NULL uses
// UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT. Ignored when confirm is false.
const char_t *confirmLabel;
// When true, actually cancelling (the CANCEL button, or back with
// nothing left to delete - see uiKeyboardMenuCancel) opens a second
// uiConfirm "are you sure" dialog (see cancelConfirmLabel) before the
// keyboard closes - picking Cancel there returns to the keyboard with
// the text untouched. Ignored when cancel is false, since there's
// nothing to confirm.
bool_t cancelConfirm;
// Question text/locale message ID for the cancel confirm dialog when
// cancelConfirm is true; copied internally, safe to be transient. NULL
// uses UI_KEYBOARD_CANCEL_QUESTION_DEFAULT. Ignored when cancelConfirm
// is false.
const char_t *cancelConfirmLabel;
// If true, leading/trailing whitespace is trimmed from the entered
// text when confirm is pressed, before it's checked against
// allowBlank or passed to onInput.
bool_t trimmed;
// If true, a blank result is accepted when confirm is pressed - blank
// means an empty string, or (when trimmed is false) a string of only
// spaces. When false, pressing confirm does nothing while the text is
// blank - see uiKeyboardTextIsBlank.
bool_t allowBlank;
// Maximum number of lines the entered text can span, from 1 to
// UI_KEYBOARD_LINE_COUNT_MAX. 0 defaults to 1. When > 1, a NEWLINE key
// is added to the grid, capped at inserting lineCount - 1 newlines.
uint8_t lineCount;
// Arbitrary pointer passed to onInput/onKeyPress.
void *user;
} uikeyboardopen_t;
typedef struct {
uilabel_t titleLabel;
char_t titleText[UI_KEYBOARD_TITLE_TEXT_MAX];
spritebatchsprite_t titleSprites[UI_KEYBOARD_TITLE_SPRITES_MAX];
uilabel_t textLabel;
char_t text[UI_KEYBOARD_TEXT_MAX];
spritebatchsprite_t textSprites[UI_KEYBOARD_TEXT_SPRITES_MAX];
// Blinking insertion-point marker, positioned each draw at the end of
// text - a separate label rather than appending to text/textSprites,
// since neither buffer has spare room for it (see
// UI_KEYBOARD_TEXT_MAX/UI_KEYBOARD_TEXT_SPRITES_MAX).
uilabel_t cursorLabel;
spritebatchsprite_t cursorSprites[UI_KEYBOARD_CURSOR_SPRITES_MAX];
uimenu_t menu;
uimenuitem_t items[UI_KEYBOARD_ITEMS_MAX];
char_t confirmQuestion[UI_KEYBOARD_CONFIRM_QUESTION_MAX];
char_t cancelConfirmQuestion[UI_KEYBOARD_CANCEL_QUESTION_MAX];
uikeyboardinputcallback_t onInput;
uikeyboardkeypresscallback_t onKeyPress;
void *user;
size_t maxLength;
uint8_t lineCount;
bool_t cancel;
bool_t confirm;
bool_t cancelConfirm;
bool_t trimmed;
bool_t allowBlank;
bool_t open;
bool_t confirmed;
// Persistent caps-lock toggle, and a one-shot shift consumed by the
// next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels
// are redrawn to match (see uiKeyboardUpdateDisplay) rather than
// staying static.
bool_t caps;
bool_t shift;
} uikeyboard_t;
extern uikeyboard_t UI_KEYBOARD;
/**
* Initializes the on-screen keyboard.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardInit(void);
/**
* Draws the on-screen keyboard: a semi-transparent black backdrop
* covering the whole screen, then its own centered frame with the title,
* currently entered text, and the key grid. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardDraw(void);
/**
* Returns true when the keyboard dialog is currently open.
*
* @returns True if open.
*/
bool_t uiKeyboardIsOpen(void);
/**
* Opens the on-screen keyboard with an empty text buffer.
*
* @param open Parameters for this dialog; copied internally, safe to be
* transient. Cannot be NULL.
*/
void uiKeyboardOpen(const uikeyboardopen_t *open);
/**
* Closes the keyboard dialog, invoking the result callback set by
* uiKeyboardOpen as cancelled (confirmed=false) unless the confirm
* button was what triggered this close. No-op when already closed.
*/
void uiKeyboardClose(void);
/**
* Disposes of the on-screen keyboard.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardDispose(void);
/**
* Appends a character to the currently entered text, silently ignoring
* it once UI_KEYBOARD.maxLength is reached, or (for '\n') once the text
* already has UI_KEYBOARD.lineCount - 1 newlines. Does not consult
* onKeyPress - that happens in uiKeyboardMenuSelected before this is
* called. Moves focus to the confirm button once this append fills the
* text to maxLength - see uiKeyboardFocusConfirm.
*
* @param c The character to append.
*/
void uiKeyboardAppendChar(const char_t c);
/**
* Moves the menu's focus to the confirm button.
*/
void uiKeyboardFocusConfirm(void);
/**
* Returns whether the currently entered text counts as blank for
* UI_KEYBOARD.allowBlank purposes - an empty string, or (when
* UI_KEYBOARD.trimmed is false, so whitespace-only text was never
* reduced to empty) a string of only whitespace.
*
* @returns True if blank.
*/
bool_t uiKeyboardTextIsBlank(void);
/**
* Removes the last character of the currently entered text. No-op when
* already empty.
*/
void uiKeyboardBackspace(void);
/**
* Internal menu callback - routes a picked key/confirm/cancel item to
* uiKeyboardAppendChar/uiKeyboardBackspace/uiKeyboardClose, consulting
* onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
* is set, picking confirm opens a second uiConfirm dialog (see
* uiKeyboardConfirmSecondaryResult) instead of closing immediately.
* SHIFT/CAPS toggle UI_KEYBOARD.shift/caps instead of entering anything.
* A blank ("") label is a filler/reserved grid cell and is a no-op.
*
* @param menu The keyboard's menu.
* @param index Index of the picked item.
* @param item The picked item.
*/
void uiKeyboardMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
);
/**
* Result callback for the second "are you sure" uiConfirm dialog opened
* when UI_KEYBOARD.confirm is set - see uiKeyboardMenuSelected.
*
* @param result True if Confirm was picked there, false if Cancel.
* @param user Unused.
*/
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user);
/**
* Actually cancels the keyboard - sets confirmed to false and closes the
* dialog. Shared by uiKeyboardMenuCancel (back with nothing to delete)
* and uiKeyboardMenuSelected (the CANCEL button); called directly when
* UI_KEYBOARD.cancelConfirm is false, or as the result callback (see
* uiKeyboardCancelConfirmSecondaryResult) once the "are you sure" dialog
* confirms it when cancelConfirm is true.
*/
void uiKeyboardDoCancel(void);
/**
* Result callback for the "are you sure" uiConfirm dialog opened when
* UI_KEYBOARD.cancelConfirm is set - see uiKeyboardDoCancel.
*
* @param result True if Confirm was picked there, false if Cancel.
* @param user Unused.
*/
void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user);
/**
* Internal menu callback - fires the result callback set by
* uiKeyboardOpen once the menu finishes closing.
*
* @param menu Unused; matches uimenuclosedcallback_t.
*/
void uiKeyboardMenuClosed(const uimenu_t *menu);
/**
* Internal menu cancel callback (see uiMenuSetCancelCallback): back
* deletes the last entered character; with nothing left to delete, it
* closes the dialog as cancelled via uiKeyboardClose, but only when
* UI_KEYBOARD.cancel allows it - otherwise back is swallowed and does
* nothing.
*
* @param menu The keyboard's menu.
* @returns Always true - this always fully handles back itself
* (backspace, explicit close, or swallow), never falling through to the
* default pop.
*/
bool_t uiKeyboardMenuCancel(const uimenu_t *menu);
/**
* Collapses a UI_KEYBOARD_KEYS_* table entry's NULL ("no key here") down
* to "" (blank filler), the only form of "nothing" callers need to
* handle.
*
* @param key A table entry, possibly NULL.
* @returns key, or "" if key was NULL.
*/
const char_t * uiKeyboardNormalizeKey(const char_t *key);
/**
* Picks which of UI_KEYBOARD_KEYS_NONE/_CAPS/_SHIFT is active for the
* given state - shift takes priority over caps when both are set.
*
* @param shift Current shift state.
* @param caps Current caps state.
* @returns The selected table.
*/
const char_t *const * uiKeyboardSelectTable(
const bool_t shift,
const bool_t caps
);
/**
* Builds the key grid's buttons into items, starting at index 0, one
* UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_KEY_COUNT of
* them, in row-major order across UI_KEYBOARD_COLUMNS columns). Each
* button's label is a single character, one of the UI_KEYBOARD_KEY_*
* literals, or "" for a blank filler/reserved slot, read straight from
* whichever of the three UI_KEYBOARD_KEYS_* tables shift/caps selects
* (see uiKeyboardGetChar) - can be refreshed later with
* uiKeyboardUpdateDisplay. See uiKeyboardGetCanonicalKey for identifying
* a picked key independent of its displayed character.
*
* @param items Destination array; must have room for at least
* UI_KEYBOARD_KEY_COUNT entries.
* @param shift Initial shift state - see uiKeyboardGetChar.
* @param caps Initial caps state - see uiKeyboardGetChar.
* @returns The number of items written (UI_KEYBOARD_KEY_COUNT).
*/
uint8_t uiKeyboardBuildItems(
uimenuitem_t *items,
const bool_t shift,
const bool_t caps
);
/**
* Repoints every key already built into items by uiKeyboardBuildItems at
* whichever UI_KEYBOARD_KEYS_* table the new shift/caps state selects,
* marking all of them dirty so they redraw. Unconditional (not just the
* letter/digit/-/= keys that actually differ between tables) - simpler
* than tracking which changed, and cheap since this only runs on an
* infrequent SHIFT/CAPS press.
*
* @param items The same array passed to uiKeyboardBuildItems.
* @param shift New shift state - see uiKeyboardGetChar.
* @param caps New caps state - see uiKeyboardGetChar.
*/
void uiKeyboardUpdateDisplay(
uimenuitem_t *items,
const bool_t shift,
const bool_t caps
);
/**
* Returns a grid slot's canonical (unshifted, no-caps) key label,
* independent of whatever character it's currently drawn as - use this
* to identify which key was picked instead of reading a selected item's
* own (possibly shifted) label.text.
*
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
* @returns The slot's canonical label.
*/
const char_t * uiKeyboardGetCanonicalKey(const uint8_t index);
/**
* True if a grid slot's character actually differs between the
* unshifted and shifted tables - i.e. a letter, digit, '-', or '='. False
* for a slot whose UI_KEYBOARD_KEYS_* entry is the same in every table
* (SPACE, DEL, SHIFT, CAPS, or blank filler).
*
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
* @returns True if shift affects this slot's character.
*/
bool_t uiKeyboardIsShiftSensitive(const uint8_t index);
/**
* Looks up the character a grid slot represents under the given shift/
* caps state, straight from whichever UI_KEYBOARD_KEYS_* table they
* select - shift takes priority over caps when both are set (so, unlike
* a physical keyboard, shift while caps is on still gives uppercase, not
* lowercase - there is no fourth table for that combination). For a slot
* whose label isn't a single character (a blank filler cell, or a
* multi-character special key like "SPACE"), returns that label's first
* byte ('\0' for blank).
*
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
* @param shift Current shift state (one-shot, see UI_KEYBOARD.shift).
* @param caps Current caps state (persistent toggle, see UI_KEYBOARD.caps).
* @returns The character to display/type.
*/
char_t uiKeyboardGetChar(
const uint8_t index,
const bool_t shift,
const bool_t caps
);
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uisaveslot.c
uiselectsave.c
)
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uisaveslot.h"
#include "ui/widget/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/color.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
void uiSaveSlotInit(
uisaveslot_t *saveSlot,
const uint8_t slotIndex,
const char_t *fileName
) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
assertNotNull(fileName, "File name cannot be NULL");
memoryZero(saveSlot, sizeof(uisaveslot_t));
saveSlot->slotIndex = slotIndex;
uiLabelInit(
&saveSlot->numberLabel, saveSlot->numberText,
saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX
);
errorret_t result = assetLocaleGetStringWithVA(
&LOCALE.entry->data.locale, "ui.save_slot.number_format", 0,
saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX,
slotIndex + 1
);
if(errorIsNotOk(result)) errorCatch(result);
saveSlot->numberLabel.dirty = true;
uiLabelInit(
&saveSlot->nameLabel, saveSlot->nameText,
saveSlot->nameSprites, UI_SAVE_SLOT_NAME_SPRITES_MAX
);
stringCopy(saveSlot->nameText, fileName, UI_SAVE_SLOT_NAME_TEXT_MAX);
saveSlot->nameLabel.dirty = true;
}
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
return saveSlot->highlighted;
}
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
saveSlot->highlighted = highlighted;
}
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
return saveSlot->displayType;
}
void uiSaveSlotSetDisplayType(
uisaveslot_t *saveSlot,
const uisaveslotdisplaytype_t displayType
) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
saveSlot->displayType = displayType;
}
errorret_t uiSaveSlotDraw(
uisaveslot_t *saveSlot,
const float_t x,
const float_t y,
const float_t width,
const float_t height
) {
assertNotNull(saveSlot, "Save slot cannot be NULL");
errorChain(uiFrameDraw(x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
color_t color;
if(saveSlot->highlighted) {
color = COLOR_RED;
} else if(saveSlot->displayType == UI_SAVE_SLOT_DISPLAY_TYPE_DELETE) {
color = COLOR_ORANGE;
} else {
color = COLOR_WHITE;
}
uiLabelSetX(&saveSlot->numberLabel, contentX);
uiLabelSetY(&saveSlot->numberLabel, contentY);
errorChain(uiLabelRender(&saveSlot->numberLabel, color));
const float_t nameY = contentY + (float_t)saveSlot->numberLabel.height +
UI_FRAME_PADDING_Y;
uiLabelSetX(&saveSlot->nameLabel, contentX);
uiLabelSetY(&saveSlot->nameLabel, nameY);
errorChain(uiLabelRender(&saveSlot->nameLabel, color));
errorOk();
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#define UI_SAVE_SLOT_NUMBER_TEXT_MAX 16
#define UI_SAVE_SLOT_NUMBER_SPRITES_MAX UI_SAVE_SLOT_NUMBER_TEXT_MAX
#define UI_SAVE_SLOT_NAME_TEXT_MAX 64
#define UI_SAVE_SLOT_NAME_SPRITES_MAX UI_SAVE_SLOT_NAME_TEXT_MAX
/**
* How a save slot entry should be styled: NORMAL for everyday
* save/load browsing, DELETE to visually flag the row as part of a
* destructive delete flow.
*/
typedef enum {
UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL,
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE
} uisaveslotdisplaytype_t;
/**
* A single framed save slot entry: a slot number and a file name. Later
* this will grow to show more per-slot save data (level, playtime,
* etc).
*/
typedef struct {
uint8_t slotIndex;
uisaveslotdisplaytype_t displayType;
uilabel_t numberLabel;
char_t numberText[UI_SAVE_SLOT_NUMBER_TEXT_MAX];
spritebatchsprite_t numberSprites[UI_SAVE_SLOT_NUMBER_SPRITES_MAX];
uilabel_t nameLabel;
char_t nameText[UI_SAVE_SLOT_NAME_TEXT_MAX];
spritebatchsprite_t nameSprites[UI_SAVE_SLOT_NAME_SPRITES_MAX];
bool_t highlighted;
} uisaveslot_t;
/**
* Initializes a save slot widget with the given slot index and display
* file name.
*
* @param saveSlot The save slot widget to initialize.
* @param slotIndex The save slot index this widget represents.
* @param fileName Display file name; copied internally, safe to be
* transient.
*/
void uiSaveSlotInit(
uisaveslot_t *saveSlot,
const uint8_t slotIndex,
const char_t *fileName
);
/**
* Returns whether the save slot widget is highlighted.
*
* @param saveSlot The save slot widget to query.
* @returns True if highlighted.
*/
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot);
/**
* Sets the highlighted state of the save slot widget.
*
* @param saveSlot The save slot widget to update.
* @param highlighted The new highlighted state.
*/
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted);
/**
* Returns the save slot widget's display type.
*
* @param saveSlot The save slot widget to query.
* @returns The display type.
*/
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot);
/**
* Sets the save slot widget's display type.
*
* @param saveSlot The save slot widget to update.
* @param displayType The new display type.
*/
void uiSaveSlotSetDisplayType(
uisaveslot_t *saveSlot,
const uisaveslotdisplaytype_t displayType
);
/**
* Draws the save slot widget as a framed box at the given position and
* size, showing its slot number and file name.
*
* @param saveSlot The save slot widget to draw.
* @param x Screen x position.
* @param y Screen y position.
* @param width Frame width.
* @param height Frame height.
* @return Any error that occurs.
*/
errorret_t uiSaveSlotDraw(
uisaveslot_t *saveSlot,
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
+357
View File
@@ -0,0 +1,357 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiselectsave.h"
#include "ui/widget/uiframe.h"
#include "ui/dialog/uiconfirm.h"
#include "ui/dialog/keyboard/uikeyboard.h"
#include "ui/overlay/uifatalerror.h"
#include "save/save.h"
#include "time/timeepoch.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
uiselectsave_t UI_SELECT_SAVE;
void uiSelectSaveFormatSlot(
const uint8_t index,
char_t *buffer,
const size_t bufferSize
) {
saveslotcache_t *cache = &SAVE.caches[index];
if(!saveSlotInUse(cache)) {
errorret_t result = assetLocaleGetString(
&LOCALE.entry->data.locale, "ui.select_save.empty", 0,
buffer, bufferSize
);
if(errorIsNotOk(result)) errorCatch(result);
return;
}
char_t dateText[32];
timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText));
errorret_t result = assetLocaleGetStringWithVA(
&LOCALE.entry->data.locale, "ui.select_save.slot_format", 0,
buffer, bufferSize,
cache->name, cache->playerLevel, dateText
);
if(errorIsNotOk(result)) errorCatch(result);
}
void uiSelectSaveRefreshSlot(const uint8_t index) {
char_t fileName[UI_SAVE_SLOT_NAME_TEXT_MAX];
uiSelectSaveFormatSlot(index, fileName, sizeof(fileName));
uiSaveSlotInit(&UI_SELECT_SAVE.slots[index], index, fileName);
uisaveslotdisplaytype_t displayType =
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE ?
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[index], displayType);
}
void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
if(!result) return;
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex;
saveslot_t slot;
saveSlotInit(&slot);
errorret_t writeResult = saveDeviceSlotWrite(
&SAVE.devices[SAVE.deviceCurrent], &slot, index
);
if(errorIsNotOk(writeResult)) {
errorCatch(errorPrint(writeResult));
return;
}
SAVE.caches[index] = slot.cachedData;
uiSelectSaveRefreshSlot(index);
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[index], true);
}
void uiSelectSaveConfirmDelete(const uint8_t index) {
UI_SELECT_SAVE.pendingDeleteIndex = index;
uiConfirmOpen(
"ui.select_save.delete_confirm", uiSelectSaveDeleteConfirmed, NULL
);
}
void uiSelectSaveNameEntered(
const char_t *text,
const bool_t confirmed,
void *user
) {
if(!confirmed) return;
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
saveslot_t slot;
saveSlotInit(&slot);
stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
errorret_t writeResult = saveDeviceSlotWrite(
&SAVE.devices[SAVE.deviceCurrent], &slot, index
);
if(errorIsNotOk(writeResult)) {
errorCatch(errorPrint(writeResult));
uiFatalErrorOpen(writeResult.state->message);
return;
}
SAVE.caches[index] = slot.cachedData;
uiSelectSaveRefreshSlot(index);
UI_SELECT_SAVE.result = index;
uiSelectSaveClose();
}
void uiSelectSaveNameEmptySlot(const uint8_t index) {
UI_SELECT_SAVE.pendingNameIndex = index;
char_t title[UI_KEYBOARD_TITLE_TEXT_MAX];
errorret_t result = assetLocaleGetString(
&LOCALE.entry->data.locale, "ui.select_save.name_title", 0,
title, sizeof(title)
);
if(errorIsNotOk(result)) errorCatch(result);
uikeyboardopen_t open = {
.title = title,
.onInput = uiSelectSaveNameEntered,
.cancel = true,
.maxLength = SAVE_SLOT_NAME_LENGTH,
.trimmed = true,
.allowBlank = false
};
uiKeyboardOpen(&open);
}
void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) {
const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ?
"ui.select_save.delete_mode" : "ui.confirm.cancel";
errorret_t result = assetLocaleGetString(
&LOCALE.entry->data.locale, labelKey, 0,
UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX
);
if(errorIsNotOk(result)) errorCatch(result);
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button.label.dirty = true;
}
void uiSelectSaveSwitchType(const uiselectsavetype_t type) {
UI_SELECT_SAVE.type = type;
uisaveslotdisplaytype_t displayType = type == UI_SELECT_SAVE_TYPE_DELETE ?
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[i], displayType);
}
uiSelectSaveSetActionLabel(type);
}
void uiSelectSaveSlotSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
if(UI_SELECT_SAVE.hasAction && index == UI_SELECT_SAVE_ACTION_INDEX) {
uiSelectSaveSwitchType(
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_LOAD ?
UI_SELECT_SAVE_TYPE_DELETE : UI_SELECT_SAVE_TYPE_LOAD
);
return;
}
if(UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE) {
uiSelectSaveConfirmDelete(index);
return;
}
if(!saveSlotInUse(&SAVE.caches[index])) {
uiSelectSaveNameEmptySlot(index);
return;
}
UI_SELECT_SAVE.result = index;
uiSelectSaveClose();
}
void uiSelectSaveSlotChanged(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], i == index);
}
// The action row sits fixed below the scrollable list, so it never
// needs to be scrolled into view.
if(index >= SAVE_SLOT_COUNT) return;
uiScrollingEnsureVisible(
&UI_SELECT_SAVE.scroll, index, SAVE_SLOT_COUNT, UI_SELECT_SAVE_VISIBLE_ROWS
);
}
void uiSelectSaveSlotClosed(const uimenu_t *menu) {
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], false);
}
if(UI_SELECT_SAVE.callback != NULL) {
UI_SELECT_SAVE.callback(UI_SELECT_SAVE.result, UI_SELECT_SAVE.user);
}
}
errorret_t uiSelectSaveInit(void) {
memoryZero(&UI_SELECT_SAVE, sizeof(uiselectsave_t));
uiLabelInit(
&UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText,
UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX
);
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale, "ui.select_save.title", 0,
UI_SELECT_SAVE.titleText, UI_SELECT_SAVE_TITLE_TEXT_MAX
));
UI_SELECT_SAVE.titleLabel.dirty = true;
uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel);
uiMenuInit(
&UI_SELECT_SAVE.slotMenu, uiSelectSaveSlotSelected, uiSelectSaveSlotClosed,
uiSelectSaveSlotChanged
);
uiMenuSetItems(
&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, SAVE_SLOT_COUNT, 1
);
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].type =
UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
UI_SELECT_SAVE.actionText
);
uiScrollingInit(&UI_SELECT_SAVE.scroll);
errorOk();
}
errorret_t uiSelectSaveDraw(void) {
if(!uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) errorOk();
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t rowFrameHeight =
(UI_FRAME_START_Y * 2) + (rowHeight * 2) + UI_FRAME_PADDING_Y;
const float_t listHeight =
(rowFrameHeight * (float_t)UI_SELECT_SAVE_VISIBLE_ROWS) +
(UI_SELECT_SAVE_ROW_GAP * (float_t)(UI_SELECT_SAVE_VISIBLE_ROWS - 1));
const float_t width = UI_SELECT_SAVE_WIDTH;
const float_t height = (UI_FRAME_START_Y * 2)
+ (float_t)UI_SELECT_SAVE.titleLabel.height + UI_FRAME_PADDING_Y
+ listHeight
+ (UI_SELECT_SAVE.hasAction ? UI_FRAME_PADDING_Y + rowHeight : 0.0f);
const float_t x = (float_t)SCREEN.scanX +
((float_t)SCREEN.scanWidth - width) * 0.5f;
const float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
uiLabelSetX(&UI_SELECT_SAVE.titleLabel, contentX);
uiLabelSetY(&UI_SELECT_SAVE.titleLabel, contentY);
errorChain(uiLabelRender(&UI_SELECT_SAVE.titleLabel, COLOR_WHITE));
const float_t listY = contentY + (float_t)UI_SELECT_SAVE.titleLabel.height +
UI_FRAME_PADDING_Y;
const uint8_t offset = UI_SELECT_SAVE.scroll.offset;
uint8_t visibleEnd = (uint8_t)(offset + UI_SELECT_SAVE_VISIBLE_ROWS);
if(visibleEnd > SAVE_SLOT_COUNT) visibleEnd = SAVE_SLOT_COUNT;
for(uint8_t i = offset; i < visibleEnd; i++) {
const float_t iy =
listY + (float_t)(i - offset) * (rowFrameHeight + UI_SELECT_SAVE_ROW_GAP);
errorChain(uiSaveSlotDraw(
&UI_SELECT_SAVE.slots[i], contentX, iy, contentWidth, rowFrameHeight
));
}
if(UI_SELECT_SAVE.hasAction) {
const float_t actionY = listY + listHeight + UI_FRAME_PADDING_Y;
errorChain(uiButtonDraw(
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
contentX, actionY
));
}
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiSelectSaveIsOpen(void) {
return uiMenuIsActive(&UI_SELECT_SAVE.slotMenu);
}
void uiSelectSaveOpen(
const uiselectsavetype_t type,
uiselectsaveresultcallback_t callback,
void *user
) {
assertNotNull(callback, "Callback cannot be NULL");
if(uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) return;
UI_SELECT_SAVE.type = type;
UI_SELECT_SAVE.callback = callback;
UI_SELECT_SAVE.user = user;
UI_SELECT_SAVE.result = UI_SELECT_SAVE_RESULT_NONE;
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
uiSelectSaveRefreshSlot(i);
}
UI_SELECT_SAVE.hasAction =
type == UI_SELECT_SAVE_TYPE_LOAD || type == UI_SELECT_SAVE_TYPE_DELETE;
uint8_t itemCount = SAVE_SLOT_COUNT;
if(UI_SELECT_SAVE.hasAction) {
uiSelectSaveSetActionLabel(type);
itemCount = UI_SELECT_SAVE_ITEM_CAPACITY;
}
uiMenuSetItems(&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, itemCount, 1);
uiScrollingInit(&UI_SELECT_SAVE.scroll);
uiMenuOpen(&UI_SELECT_SAVE.slotMenu);
}
void uiSelectSaveClose(void) {
uiMenuClose(&UI_SELECT_SAVE.slotMenu);
}
errorret_t uiSelectSaveDispose(void) {
errorOk();
}
+134
View File
@@ -0,0 +1,134 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uimenu.h"
#include "ui/widget/uiscrolling.h"
#include "ui/dialog/save/uisaveslot.h"
#include "save/saveslot.h"
#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
#define UI_SELECT_SAVE_ACTION_LABEL_MAX 32
#define UI_SELECT_SAVE_VISIBLE_ROWS (SAVE_SLOT_COUNT < 3 ? SAVE_SLOT_COUNT : 3)
#define UI_SELECT_SAVE_WIDTH 260.0f
#define UI_SELECT_SAVE_ROW_GAP 4.0f
#define UI_SELECT_SAVE_RESULT_NONE 0xFF
// Slot rows plus one trailing action row (switch-to-delete/cancel), only
// present for the LOAD and DELETE types - see uiSelectSaveOpen.
#define UI_SELECT_SAVE_ITEM_CAPACITY (SAVE_SLOT_COUNT + 1)
#define UI_SELECT_SAVE_ACTION_INDEX SAVE_SLOT_COUNT
/**
* The action the select-save panel is being used for. The caller
* chooses this when calling uiSelectSaveOpen.
*/
typedef enum {
UI_SELECT_SAVE_TYPE_SAVE,
UI_SELECT_SAVE_TYPE_LOAD,
UI_SELECT_SAVE_TYPE_DELETE
} uiselectsavetype_t;
/**
* Callback invoked once the select-save panel closes: slotIndex is the
* chosen slot, or UI_SELECT_SAVE_RESULT_NONE if backed out of without
* choosing.
*
* @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE.
* @param user Arbitrary pointer passed to uiSelectSaveOpen.
*/
typedef void (*uiselectsaveresultcallback_t)(
const uint8_t slotIndex, void *user
);
typedef struct {
uilabel_t titleLabel;
char_t titleText[UI_SELECT_SAVE_TITLE_TEXT_MAX];
spritebatchsprite_t titleSprites[UI_SELECT_SAVE_TITLE_SPRITES_MAX];
uiselectsavetype_t type;
bool_t hasAction;
char_t actionText[UI_SELECT_SAVE_ACTION_LABEL_MAX];
uimenu_t slotMenu;
uimenuitem_t slotItems[UI_SELECT_SAVE_ITEM_CAPACITY];
uisaveslot_t slots[SAVE_SLOT_COUNT];
uiscrolling_t scroll;
uint8_t pendingDeleteIndex;
uint8_t pendingNameIndex;
uiselectsaveresultcallback_t callback;
void *user;
uint8_t result;
} uiselectsave_t;
extern uiselectsave_t UI_SELECT_SAVE;
/**
* Initializes the select-save panel.
*
* @return Any error that occurs.
*/
errorret_t uiSelectSaveInit(void);
/**
* Draws the select-save panel: a centered frame with a title, a
* scrolling list of save/save.h's slots (each shown as a framed
* uisaveslot_t, showing at most UI_SELECT_SAVE_VISIBLE_ROWS at a time),
* and - when UI_SELECT_SAVE.hasAction is set - the switch-to-delete/
* cancel action row fixed below the scrollable list, outside it. No-op
* when not open.
*
* @return Any error that occurs.
*/
errorret_t uiSelectSaveDraw(void);
/**
* Returns true when the select-save panel is currently open.
*
* @returns True if open.
*/
bool_t uiSelectSaveIsOpen(void);
/**
* Opens the select-save panel for the given action, refreshing each row
* from the current SAVE.caches state. callback is invoked exactly once
* with the result, whether a slot was chosen or the panel was backed
* out of without choosing.
*
* UI_SELECT_SAVE_TYPE_LOAD adds a trailing row to switch to
* UI_SELECT_SAVE_TYPE_DELETE in place, and UI_SELECT_SAVE_TYPE_DELETE
* adds a trailing "Cancel" row to switch back to
* UI_SELECT_SAVE_TYPE_LOAD - neither closes the panel or invokes
* callback, they just toggle UI_SELECT_SAVE.type and the slot rows'
* styling. UI_SELECT_SAVE_TYPE_SAVE has no trailing row.
*
* @param type The action the caller is opening the panel for.
* @param callback Called with the result once the panel closes. Cannot
* be NULL.
* @param user Arbitrary pointer passed through to callback.
*/
void uiSelectSaveOpen(
const uiselectsavetype_t type,
uiselectsaveresultcallback_t callback,
void *user
);
/**
* Closes the select-save panel. No-op when already closed.
*/
void uiSelectSaveClose(void);
/**
* Disposes of the select-save panel.
*
* @return Any error that occurs.
*/
errorret_t uiSelectSaveDispose(void);
+64
View File
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiconfirm.h"
#include "ui/widget/uimodal.h"
#include "assert/assert.h"
#include "util/memory.h"
#define UI_CONFIRM_INDEX_CONFIRM 0
#define UI_CONFIRM_INDEX_CANCEL 1
uiconfirm_t UI_CONFIRM;
void uiConfirmModalCallback(const uint8_t optionIndex, void *user) {
UI_CONFIRM.result = optionIndex == UI_CONFIRM_INDEX_CONFIRM;
if(UI_CONFIRM.callback != NULL) {
UI_CONFIRM.callback(UI_CONFIRM.result, user);
}
}
errorret_t uiConfirmInit(void) {
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
errorOk();
}
bool_t uiConfirmIsOpen(void) {
return uiModalIsOpen();
}
bool_t uiConfirmGetResult(void) {
return UI_CONFIRM.result;
}
void uiConfirmOpen(
const char_t *question,
uiconfirmcallback_t callback,
void *user
) {
assertNotNull(question, "Question cannot be NULL");
UI_CONFIRM.callback = callback;
UI_CONFIRM.result = false;
const char_t *options[] = {
"ui.confirm.confirm",
"ui.confirm.cancel"
};
uiModalOpen(
NULL, question, options, sizeof(options) / sizeof(options[0]),
uiConfirmModalCallback, NULL, user
);
}
void uiConfirmClose(void) {
uiModalClose(NULL);
}
errorret_t uiConfirmDispose(void) {
errorOk();
}
@@ -7,33 +7,21 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_CONFIRM_TEXT_MAX 256
#define UI_CONFIRM_MIN_WIDTH 160.0f
#define UI_CONFIRM_INDEX_CONFIRM 0
#define UI_CONFIRM_INDEX_CANCEL 1
#define UI_CONFIRM_ITEM_COUNT 2
#define UI_CONFIRM_LABEL_MAX 32
/** /**
* Callback invoked once a confirm dialog is dismissed. * Callback invoked once a confirm dialog is dismissed. Back/cancel
* input is disabled while it's open, so the dialog can only be
* dismissed by picking Confirm or Cancel.
* *
* @param result True if Confirm was selected, false if Cancel was * @param result True if Confirm was selected, false if Cancel was
* selected or the dialog was backed out of. * selected.
* @param user Arbitrary pointer passed to uiConfirmOpen. * @param user Arbitrary pointer passed to uiConfirmOpen.
*/ */
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user); typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
typedef struct { typedef struct {
char_t text[UI_CONFIRM_TEXT_MAX];
uimenu_t menu;
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
uiconfirmcallback_t callback; uiconfirmcallback_t callback;
void *user;
bool_t result; bool_t result;
char_t confirmLabel[UI_CONFIRM_LABEL_MAX];
char_t cancelLabel[UI_CONFIRM_LABEL_MAX];
} uiconfirm_t; } uiconfirm_t;
extern uiconfirm_t UI_CONFIRM; extern uiconfirm_t UI_CONFIRM;
@@ -45,15 +33,6 @@ extern uiconfirm_t UI_CONFIRM;
*/ */
errorret_t uiConfirmInit(void); errorret_t uiConfirmInit(void);
/**
* Draws the confirm dialog: a semi-transparent black backdrop covering
* the whole screen, then its own centered frame with the question text
* and Confirm/Cancel buttons. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiConfirmDraw(void);
/** /**
* Returns true when the confirm dialog is currently open. * Returns true when the confirm dialog is currently open.
* *
@@ -69,11 +48,12 @@ bool_t uiConfirmIsOpen(void);
bool_t uiConfirmGetResult(void); bool_t uiConfirmGetResult(void);
/** /**
* Opens the confirm dialog with the given question text. callback is * Opens a confirm dialog, backed by the shared uimodal_t, with the given
* invoked exactly once with the result, whether the dialog was * question text and Confirm/Cancel buttons. callback is invoked exactly
* dismissed by selecting a button or by pressing cancel/back. * once with the result once a button is selected.
* *
* @param question Display text; copied internally, safe to be transient. * @param question Display text, or a locale message ID; copied
* internally by uimodal, safe to be transient.
* @param callback Called with the result once the dialog closes. May be * @param callback Called with the result once the dialog closes. May be
* NULL. * NULL.
* @param user Arbitrary pointer passed through to callback. * @param user Arbitrary pointer passed through to callback.
+20 -6
View File
@@ -59,9 +59,14 @@ uifocusitem_t * uiFocusPush(
void uiFocusPop(void) { void uiFocusPop(void) {
assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow"); assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; // Count is decremented before the closed callback fires so that a
if(item->closed != NULL) item->closed(item); // push triggered from within it (e.g. reopening a menu once another
// one reports it closed with no result) lands in the now-free slot
// instead of one past it, which would silently strand the new item
// above the tracked stack top.
UI_FOCUS.count--; UI_FOCUS.count--;
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count];
if(item->closed != NULL) item->closed(item);
} }
void uiFocusPopItem(uifocusitem_t *item) { void uiFocusPopItem(uifocusitem_t *item) {
@@ -113,9 +118,17 @@ void uiFocusMoveDirection(
) { ) {
if(m->direction != dir) continue; if(m->direction != dir) continue;
uint8_t x = (uint8_t)(item->x + m->dx); // Widened to int16_t so a move off the left/top edge (e.g. x=0,
uint8_t y = (uint8_t)(item->y + m->dy); // dx=-1) stays negative here instead of wrapping straight to 255 via
uiFocusSetPosition(item, x, y); // a uint8_t truncation - that would then land on `255 % cols`
// (column 3 of a 14-wide grid, not the intended last column) rather
// than wrapping to the row/column's actual last cell.
int16_t x = (int16_t)item->x + m->dx;
int16_t y = (int16_t)item->y + m->dy;
if(x < 0) x += item->cols;
if(y < 0) y += item->rows;
uiFocusSetPosition(item, (uint8_t)x, (uint8_t)y);
break; break;
} }
} }
@@ -148,7 +161,8 @@ void uiFocusUpdate(void) {
} }
if(inputPressed(INPUT_ACTION_CANCEL)) { if(inputPressed(INPUT_ACTION_CANCEL)) {
uiFocusPop(); if(item->cancel != NULL && item->cancel(item)) return;
if(!item->disableBack) uiFocusPop();
return; return;
} }
+14
View File
@@ -53,5 +53,19 @@ struct uifocusitem_s {
uifocusitemcallback_t changed; uifocusitemcallback_t changed;
uifocusitemcallback_t closed; uifocusitemcallback_t closed;
uifocusitemdirectioncallback_t direction; uifocusitemdirectioncallback_t direction;
// Called when INPUT_ACTION_CANCEL is pressed, before disableBack/the
// default pop is applied; may be NULL. Returns true if fully handled
// (e.g. it did something else instead, or popped itself), which skips
// the default pop; false to fall through to the default disableBack/
// pop behavior below.
uifocusitemcallback_t cancel;
void *user; void *user;
// When true, INPUT_ACTION_CANCEL is ignored while this item is the
// topmost focus item - it does not pop. Does not affect a
// programmatic uiFocusPop()/uiFocusPopItem() call. Only consulted
// when cancel is unset or returns false.
bool_t disableBack;
}; };
-145
View File
@@ -1,145 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiconfirm.h"
#include "ui/widget/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "display/shader/shaderunlit.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#define UI_CONFIRM_BACKDROP_COLOR color4b(0, 0, 0, 160)
uiconfirm_t UI_CONFIRM;
void uiConfirmSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
UI_CONFIRM.result = index == UI_CONFIRM_INDEX_CONFIRM;
uiConfirmClose();
}
void uiConfirmClosed(const uimenu_t *menu) {
if(UI_CONFIRM.callback != NULL) {
UI_CONFIRM.callback(UI_CONFIRM.result, UI_CONFIRM.user);
}
}
errorret_t uiConfirmInit(void) {
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.confirm.confirm",
0,
UI_CONFIRM.confirmLabel,
UI_CONFIRM_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.confirm.cancel",
0,
UI_CONFIRM.cancelLabel,
UI_CONFIRM_LABEL_MAX
));
MENU_BEGIN(
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
);
MENU_BUTTON(UI_CONFIRM.confirmLabel);
MENU_BUTTON(UI_CONFIRM.cancelLabel);
MENU_END(UI_CONFIRM.items, menuIndex);
errorOk();
}
errorret_t uiConfirmDraw(void) {
if(!uiMenuIsActive(&UI_CONFIRM.menu)) errorOk();
spritebatchsprite_t backdropSprite = {
.min = { 0.0f, 0.0f, 0.0f },
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t backdropMaterial = {
.unlit = {
.color = UI_CONFIRM_BACKDROP_COLOR,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
int32_t textW, textH;
textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH);
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t width = mathMax(
(float_t)textW + (UI_FRAME_START_X * 2), UI_CONFIRM_MIN_WIDTH
);
float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y +
rowHeight;
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
float_t contentWidth = width - (UI_FRAME_START_X * 2);
errorChain(textDraw(contentX, contentY, UI_CONFIRM.text, COLOR_WHITE, &FONT_DEFAULT));
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiConfirmIsOpen(void) {
return uiMenuIsActive(&UI_CONFIRM.menu);
}
bool_t uiConfirmGetResult(void) {
return UI_CONFIRM.result;
}
void uiConfirmOpen(
const char_t *question,
uiconfirmcallback_t callback,
void *user
) {
assertNotNull(question, "Question cannot be NULL");
stringCopy(UI_CONFIRM.text, question, UI_CONFIRM_TEXT_MAX);
UI_CONFIRM.callback = callback;
UI_CONFIRM.user = user;
UI_CONFIRM.result = false;
uiMenuOpen(&UI_CONFIRM.menu);
}
void uiConfirmClose(void) {
uiMenuClose(&UI_CONFIRM.menu);
}
errorret_t uiConfirmDispose(void) {
errorOk();
}
-116
View File
@@ -1,116 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uimenu.h"
#define UI_MODAL_TITLE_TEXT_MAX 64
#define UI_MODAL_TITLE_SPRITES_MAX UI_MODAL_TITLE_TEXT_MAX
#define UI_MODAL_MESSAGE_TEXT_MAX 256
#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
#define UI_MODAL_OPTIONS_MAX 4
#define UI_MODAL_MIN_WIDTH 160.0f
#define UI_MODAL_RESULT_NONE 0xFF
/**
* Callback invoked once a modal is dismissed.
*
* @param optionIndex Index into the options array passed to uiModalOpen
* that was selected, or UI_MODAL_RESULT_NONE if backed out of without
* selecting an option.
* @param user Arbitrary pointer passed to uiModalOpen.
*/
typedef void (*uimodalcallback_t)(const uint8_t optionIndex, void *user);
typedef struct {
uilabel_t titleLabel;
char_t titleText[UI_MODAL_TITLE_TEXT_MAX];
spritebatchsprite_t titleSprites[UI_MODAL_TITLE_SPRITES_MAX];
uilabel_t messageLabel;
char_t messageText[UI_MODAL_MESSAGE_TEXT_MAX];
spritebatchsprite_t messageSprites[UI_MODAL_MESSAGE_SPRITES_MAX];
uimenu_t menu;
uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
uimodalcallback_t callback;
void *user;
uint8_t result;
} uimodal_t;
extern uimodal_t UI_MODAL;
/**
* Initializes the modal dialog.
*
* @return Any error that occurs.
*/
errorret_t uiModalInit(void);
/**
* Draws the modal dialog: a semi-transparent black backdrop covering the
* whole screen, then its own centered frame with the title, message, and
* option buttons. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiModalDraw(void);
/**
* Returns true when the modal dialog is currently open.
*
* @returns True if open.
*/
bool_t uiModalIsOpen(void);
/**
* Returns the result of the most recently dismissed modal.
*
* @returns The selected option index, or UI_MODAL_RESULT_NONE.
*/
uint8_t uiModalGetResult(void);
/**
* Opens the modal dialog with the given title, message, and options.
* callback is invoked exactly once with the result, whether the dialog
* was dismissed by selecting an option or by pressing cancel/back.
*
* @param title Display title; copied internally, safe to be transient.
* @param message Display message; copied internally, safe to be
* transient.
* @param options Array of option label strings; NOT copied internally,
* the pointers are stored directly by the underlying buttons, so they
* must remain valid for as long as the modal is open (e.g. string
* literals or locale-owned strings).
* @param optionCount Number of options, from 1 to UI_MODAL_OPTIONS_MAX.
* @param callback Called with the result once the dialog closes. May be
* NULL.
* @param user Arbitrary pointer passed through to callback.
*/
void uiModalOpen(
const char_t *title,
const char_t *message,
const char_t **options,
const uint8_t optionCount,
uimodalcallback_t callback,
void *user
);
/**
* Closes the modal dialog. No-op when already closed.
*/
void uiModalClose(void);
/**
* Disposes of the modal dialog.
*
* @return Any error that occurs.
*/
errorret_t uiModalDispose(void);
+1
View File
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
uicrop.c uicrop.c
uifullbox.c uifullbox.c
uiloading.c uiloading.c
uifatalerror.c
) )
+108
View File
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uifatalerror.h"
#include "ui/widget/uiframe.h"
#include "ui/widget/uibutton.h"
#include "engine/engine.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/color.h"
#include "display/text/font.h"
#include "display/spritebatch/spritebatch.h"
uifatalerror_t UI_FATAL_ERROR;
void uiFatalErrorMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
ENGINE.running = false;
}
errorret_t uiFatalErrorInit(void) {
memoryZero(&UI_FATAL_ERROR, sizeof(uifatalerror_t));
uiLabelInit(
&UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageText,
UI_FATAL_ERROR.messageSprites, UI_FATAL_ERROR_MESSAGE_SPRITES_MAX
);
errorOk();
}
void uiFatalErrorOpen(const char_t *message) {
assertNotNull(message, "Message cannot be NULL");
// This is the last line of defense - it must never itself fail, so a
// second fatal error while one is already showing is silently dropped
// rather than asserting.
if(UI_FATAL_ERROR.open) return;
stringCopy(
UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1
);
UI_FATAL_ERROR.messageLabel.dirty = true;
uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel);
UI_FATAL_ERROR.open = true;
uiMenuInit(&UI_FATAL_ERROR.menu, uiFatalErrorMenuSelected, NULL, NULL);
UI_FATAL_ERROR.items[0].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_FATAL_ERROR.items[0].button, UI_FATAL_ERROR_QUIT_LABEL);
uiMenuSetItems(
&UI_FATAL_ERROR.menu, UI_FATAL_ERROR.items, UI_FATAL_ERROR_ITEM_COUNT, 1
);
// No way out but the QUIT button - this is a fatal, unrecoverable
// condition.
uiMenuSetDisableBack(&UI_FATAL_ERROR.menu, true);
uiMenuOpen(&UI_FATAL_ERROR.menu);
}
bool_t uiFatalErrorIsOpen(void) {
return UI_FATAL_ERROR.open;
}
errorret_t uiFatalErrorDraw(void) {
if(!UI_FATAL_ERROR.open) errorOk();
const float_t x = (float_t)SCREEN.scanX;
const float_t y = (float_t)SCREEN.scanY;
const float_t width = (float_t)SCREEN.scanWidth;
const float_t height = (float_t)SCREEN.scanHeight;
errorChain(uiFrameDraw(x, y, width, height));
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t contentWidth = mathMax(
(float_t)UI_FATAL_ERROR.messageLabel.width,
(float_t)UI_FATAL_ERROR.items[0].button.label.width
);
const float_t contentHeight = (float_t)UI_FATAL_ERROR.messageLabel.height +
UI_FRAME_PADDING_Y + rowHeight;
const float_t contentX = x + (width - contentWidth) * 0.5f;
const float_t contentY = y + (height - contentHeight) * 0.5f;
uiLabelSetX(&UI_FATAL_ERROR.messageLabel, contentX);
uiLabelSetY(&UI_FATAL_ERROR.messageLabel, contentY);
errorChain(uiLabelRender(&UI_FATAL_ERROR.messageLabel, COLOR_WHITE));
const float_t menuY = contentY +
(float_t)UI_FATAL_ERROR.messageLabel.height + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(&UI_FATAL_ERROR.menu, contentX, menuY, contentWidth, rowHeight));
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiFatalErrorDispose(void) {
errorOk();
}
+71
View File
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uimenu.h"
#define UI_FATAL_ERROR_MESSAGE_MAX 256
#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX
#define UI_FATAL_ERROR_ITEM_COUNT 1
#define UI_FATAL_ERROR_QUIT_LABEL "QUIT"
typedef struct {
bool_t open;
uilabel_t messageLabel;
char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX];
spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX];
uimenu_t menu;
uimenuitem_t items[UI_FATAL_ERROR_ITEM_COUNT];
} uifatalerror_t;
extern uifatalerror_t UI_FATAL_ERROR;
/**
* Initializes the fatal error overlay.
*
* @return Any error that occurs.
*/
errorret_t uiFatalErrorInit(void);
/**
* Opens the fatal error overlay, a fullscreen, non-dismissible display
* for an unrecoverable error - the only way out is its QUIT button,
* which sets ENGINE.running to false. Intended as a last-resort, nicer-
* looking alternative to a raw assert crash for a genuinely fatal
* condition, so it must never itself fail: a second call while already
* open is silently ignored rather than asserting, and it draws above
* every other UI element.
*
* @param message The error text to display; copied internally, safe to
* be transient. Truncated to UI_FATAL_ERROR_MESSAGE_MAX - 1 characters.
*/
void uiFatalErrorOpen(const char_t *message);
/**
* Returns whether the fatal error overlay is currently open.
*
* @returns True if open.
*/
bool_t uiFatalErrorIsOpen(void);
/**
* Draws the fatal error overlay. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiFatalErrorDraw(void);
/**
* Disposes of the fatal error overlay.
*
* @return Any error that occurs.
*/
errorret_t uiFatalErrorDispose(void);
+3 -7
View File
@@ -20,12 +20,6 @@ uifullbox_t UI_FULLBOX_OVER;
void uiFullboxInit(uifullbox_t *fullbox) { void uiFullboxInit(uifullbox_t *fullbox) {
assertNotNull(fullbox, "fullbox must not be NULL"); assertNotNull(fullbox, "fullbox must not be NULL");
memoryZero(fullbox, sizeof(uifullbox_t)); memoryZero(fullbox, sizeof(uifullbox_t));
eventInit(
&fullbox->onTransitionEnd,
fullbox->onTransitionEndCallbacks,
fullbox->onTransitionEndUsers,
4
);
} }
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) { void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
@@ -35,7 +29,9 @@ void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
fullbox->time += delta; fullbox->time += delta;
if(fullbox->time >= fullbox->duration) { if(fullbox->time >= fullbox->duration) {
fullbox->time = fullbox->duration; fullbox->time = fullbox->duration;
eventInvoke(&fullbox->onTransitionEnd, fullbox); if(fullbox->onTransitionEnd) {
fullbox->onTransitionEnd(fullbox, fullbox->onTransitionEndUser);
}
} }
} }
+14 -6
View File
@@ -9,18 +9,26 @@
#include "error/error.h" #include "error/error.h"
#include "display/color.h" #include "display/color.h"
#include "animation/easing.h" #include "animation/easing.h"
#include "event/event.h"
typedef struct { typedef struct uifullbox_s uifullbox_t;
/**
* Callback fired once when a fullbox transition completes.
*
* @param fullbox The uifullbox_t the transition ran on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*uifullboxcallback_t)(uifullbox_t *fullbox, void *user);
struct uifullbox_s {
color_t fromColor; color_t fromColor;
color_t toColor; color_t toColor;
float_t duration; float_t duration;
float_t time; float_t time;
easingtype_t easing; easingtype_t easing;
eventcallback_t onTransitionEndCallbacks[4]; uifullboxcallback_t onTransitionEnd;
void *onTransitionEndUsers[4]; void *onTransitionEndUser;
event_t onTransitionEnd; };
} uifullbox_t;
extern uifullbox_t UI_FULLBOX_UNDER; extern uifullbox_t UI_FULLBOX_UNDER;
extern uifullbox_t UI_FULLBOX_OVER; extern uifullbox_t UI_FULLBOX_OVER;
+9 -18
View File
@@ -20,12 +20,6 @@ uiloading_t UI_LOADING;
errorret_t uiLoadingInit(void) { errorret_t uiLoadingInit(void) {
memoryZero(&UI_LOADING, sizeof(uiloading_t)); memoryZero(&UI_LOADING, sizeof(uiloading_t));
eventInit(
&UI_LOADING.onTransitionEnd,
UI_LOADING.onTransitionEndCallbacks,
UI_LOADING.onTransitionEndUsers,
4
);
errorChain(assetLocaleGetString( errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale, &LOCALE.entry->data.locale,
@@ -43,7 +37,9 @@ errorret_t uiLoadingUpdate(void) {
UI_LOADING.time += TIME.delta; UI_LOADING.time += TIME.delta;
if(UI_LOADING.time >= UI_LOADING.duration) { if(UI_LOADING.time >= UI_LOADING.duration) {
UI_LOADING.time = UI_LOADING.duration; UI_LOADING.time = UI_LOADING.duration;
eventInvoke(&UI_LOADING.onTransitionEnd, &UI_LOADING); if(UI_LOADING.onTransitionEnd) {
UI_LOADING.onTransitionEnd(&UI_LOADING, UI_LOADING.onTransitionEndUser);
}
} }
} }
errorOk(); errorOk();
@@ -76,26 +72,21 @@ errorret_t uiLoadingDraw(void) {
return spriteBatchFlush(); return spriteBatchFlush();
} }
static void uiLoadingTransition( void uiLoadingTransition(
const float_t from, const float_t from,
const float_t to, const float_t to,
const eventcallback_t callback, const uiloadingcallback_t callback,
void *user void *user
) { ) {
UI_LOADING.fromAlpha = from; UI_LOADING.fromAlpha = from;
UI_LOADING.toAlpha = to; UI_LOADING.toAlpha = to;
UI_LOADING.duration = UI_LOADING_FADE_DURATION; UI_LOADING.duration = UI_LOADING_FADE_DURATION;
UI_LOADING.time = 0.0f; UI_LOADING.time = 0.0f;
eventInit( UI_LOADING.onTransitionEnd = callback;
&UI_LOADING.onTransitionEnd, UI_LOADING.onTransitionEndUser = user;
UI_LOADING.onTransitionEndCallbacks,
UI_LOADING.onTransitionEndUsers,
4
);
if(callback) eventSubscribe(&UI_LOADING.onTransitionEnd, callback, user);
} }
void uiLoadingShow(eventcallback_t callback, void *user) { void uiLoadingShow(uiloadingcallback_t callback, void *user) {
uiLoadingTransition(0.0f, 1.0f, callback, user); uiLoadingTransition(0.0f, 1.0f, callback, user);
uiFullboxTransition( uiFullboxTransition(
&UI_FULLBOX_OVER, &UI_FULLBOX_OVER,
@@ -106,7 +97,7 @@ void uiLoadingShow(eventcallback_t callback, void *user) {
); );
} }
void uiLoadingHide(eventcallback_t callback, void *user) { void uiLoadingHide(uiloadingcallback_t callback, void *user) {
uiLoadingTransition(1.0f, 0.0f, callback, user); uiLoadingTransition(1.0f, 0.0f, callback, user);
uiFullboxTransition( uiFullboxTransition(
&UI_FULLBOX_OVER, &UI_FULLBOX_OVER,
+32 -8
View File
@@ -7,22 +7,30 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "event/event.h"
#define UI_LOADING_FADE_DURATION 0.5f #define UI_LOADING_FADE_DURATION 0.5f
#define UI_LOADING_MARGIN 8.0f #define UI_LOADING_MARGIN 8.0f
#define UI_LOADING_TEXT_MAX 32 #define UI_LOADING_TEXT_MAX 32
typedef struct { typedef struct uiloading_s uiloading_t;
/**
* Callback fired once when a loading indicator fade transition completes.
*
* @param loading The uiloading_t the transition ran on.
* @param user The user pointer passed alongside the callback.
*/
typedef void (*uiloadingcallback_t)(uiloading_t *loading, void *user);
struct uiloading_s {
float_t fromAlpha; float_t fromAlpha;
float_t toAlpha; float_t toAlpha;
float_t duration; float_t duration;
float_t time; float_t time;
eventcallback_t onTransitionEndCallbacks[4]; uiloadingcallback_t onTransitionEnd;
void *onTransitionEndUsers[4]; void *onTransitionEndUser;
event_t onTransitionEnd;
char_t text[UI_LOADING_TEXT_MAX]; char_t text[UI_LOADING_TEXT_MAX];
} uiloading_t; };
extern uiloading_t UI_LOADING; extern uiloading_t UI_LOADING;
@@ -48,13 +56,29 @@ errorret_t uiLoadingUpdate(void);
*/ */
errorret_t uiLoadingDraw(void); errorret_t uiLoadingDraw(void);
/**
* Begins a loading indicator fade transition, replacing any transition
* already in progress along with its pending callback.
*
* @param from Starting alpha.
* @param to Ending alpha.
* @param callback Called when the transition completes. May be NULL.
* @param user Forwarded to the callback unchanged.
*/
void uiLoadingTransition(
const float_t from,
const float_t to,
const uiloadingcallback_t callback,
void *user
);
/** /**
* Fades the loading indicator in. Invokes callback when fully visible. * Fades the loading indicator in. Invokes callback when fully visible.
* *
* @param callback Called when the fade-in completes. May be NULL. * @param callback Called when the fade-in completes. May be NULL.
* @param user Forwarded to the callback unchanged. * @param user Forwarded to the callback unchanged.
*/ */
void uiLoadingShow(eventcallback_t callback, void *user); void uiLoadingShow(uiloadingcallback_t callback, void *user);
/** /**
* Fades the loading indicator out. Invokes callback when fully hidden. * Fades the loading indicator out. Invokes callback when fully hidden.
@@ -62,4 +86,4 @@ void uiLoadingShow(eventcallback_t callback, void *user);
* @param callback Called when the fade-out completes. May be NULL. * @param callback Called when the fade-out completes. May be NULL.
* @param user Forwarded to the callback unchanged. * @param user Forwarded to the callback unchanged.
*/ */
void uiLoadingHide(eventcallback_t callback, void *user); void uiLoadingHide(uiloadingcallback_t callback, void *user);
@@ -3,12 +3,6 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uiconfirm.c
uimodal.c
)
add_subdirectory(game) add_subdirectory(game)
add_subdirectory(mainmenu) add_subdirectory(mainmenu)
add_subdirectory(battle) add_subdirectory(battle)

Some files were not shown because too many files have changed in this diff Show More