From e225a076f02d83247be43223521eccc2ce23f3b8 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 20 Aug 2026 14:35:41 -0500 Subject: [PATCH] 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 --- src/dusk/rpg/cutscene/cutscene.h | 60 ++++++++++ src/dusk/rpg/cutscene/cutscenesystem.c | 33 ++++++ src/dusk/rpg/cutscene/cutscenesystem.h | 19 ++++ src/dusk/rpg/cutscene/item/CMakeLists.txt | 1 + .../rpg/cutscene/item/control/CMakeLists.txt | 3 + .../cutscene/item/control/cutscenemarker.c | 21 ++++ .../cutscene/item/control/cutscenemarker.h | 40 +++++++ .../cutscene/item/control/cutscenerestart.c | 23 ++++ .../cutscene/item/control/cutscenerestart.h | 38 +++++++ .../rpg/cutscene/item/control/cutscenescene.c | 23 ++++ .../rpg/cutscene/item/control/cutscenescene.h | 44 ++++++++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 30 +++++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 15 +++ src/dusk/rpg/cutscene/item/cutsceneprint.c | 23 ++++ src/dusk/rpg/cutscene/item/cutsceneprint.h | 41 +++++++ src/dusk/rpg/cutscene/item/ui/CMakeLists.txt | 1 + src/dusk/rpg/cutscene/item/ui/cutscenemodal.c | 53 +++++++++ src/dusk/rpg/cutscene/item/ui/cutscenemodal.h | 105 ++++++++++++++++++ src/dusk/scene/initial/sceneinitial.c | 73 +++++++----- src/dusk/scene/initial/sceneinitial.h | 6 +- src/dusk/ui/frame/uimodal.c | 72 ++++++++---- src/dusk/ui/frame/uimodal.h | 39 +++++-- 22 files changed, 705 insertions(+), 58 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenemarker.c create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenemarker.h create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenerestart.c create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenerestart.h create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenescene.c create mode 100644 src/dusk/rpg/cutscene/item/control/cutscenescene.h create mode 100644 src/dusk/rpg/cutscene/item/cutsceneprint.c create mode 100644 src/dusk/rpg/cutscene/item/cutsceneprint.h create mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodal.c create mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodal.h diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index 187eed25..dfe02354 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -53,6 +53,25 @@ typedef struct cutscene_s { #define CUTSCENE_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) \ { \ .type = CUTSCENE_ITEM_TYPE_CUTSCENE, \ @@ -62,6 +81,47 @@ typedef struct cutscene_s { #define CUTSCENE_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. +#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. +// CALLBACK fires with the selected option index (or UI_MODAL_RESULT_NONE +// if backed out of) 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, so they +// must outlive the modal (string literals are fine). +#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 \ + } \ + } + +// 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 } + #define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \ { \ .type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \ diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 933dbc0b..35438df9 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -8,6 +8,7 @@ #include "cutscenesystem.h" #include "rpg/entity/entity.h" #include "util/memory.h" +#include "util/string.h" #include "assert/assert.h" cutscenesystem_t CUTSCENE_SYSTEM; @@ -42,6 +43,17 @@ void cutsceneSystemStartCutsceneWith( cutsceneSystemNext(); } +void cutsceneRestart(void) { + assertNotNull( + CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running" + ); + cutsceneSystemStartCutsceneWith( + CUTSCENE_SYSTEM.scene, + CUTSCENE_SYSTEM.entityInteract, + CUTSCENE_SYSTEM.entityInteracted + ); +} + void cutsceneSystemUpdate() { if(CUTSCENE_SYSTEM.scene == NULL) return; @@ -76,6 +88,27 @@ void cutsceneSystemNext() { cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); } +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() { if(CUTSCENE_SYSTEM.scene == NULL) return NULL; diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 6f95a2a7..75d75d07 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -67,6 +67,13 @@ void cutsceneSystemStartCutsceneWith( entity_t *interacted ); +/** + * 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); + /** * Resolves a raw entity index (or sentinel) to an entity pointer. * Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED, @@ -102,6 +109,18 @@ uint8_t cutsceneSystemGetTextMiniId(const uint8_t index); */ 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. */ diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt index 5a0f884f..d00c8e53 100755 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cutsceneitem.c cutscenecallback.c + cutsceneprint.c ) add_subdirectory(control) diff --git a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt index d0e492d6..1c4e73d9 100644 --- a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt @@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenewait.c cutscenesetpause.c cutsceneconcurrent.c + cutscenemarker.c + cutscenerestart.c + cutscenescene.c ) diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.c b/src/dusk/rpg/cutscene/item/control/cutscenemarker.c new file mode 100644 index 00000000..79114d70 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenemarker.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; +} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.h b/src/dusk/rpg/cutscene/item/control/cutscenemarker.h new file mode 100644 index 00000000..b8b033b4 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenemarker.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.c b/src/dusk/rpg/cutscene/item/control/cutscenerestart.c new file mode 100644 index 00000000..160a35ba --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenerestart.c @@ -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; +} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.h b/src/dusk/rpg/cutscene/item/control/cutscenerestart.h new file mode 100644 index 00000000..34957a8d --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenerestart.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.c b/src/dusk/rpg/cutscene/item/control/cutscenescene.c new file mode 100644 index 00000000..2d66b772 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenescene.c @@ -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; +} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.h b/src/dusk/rpg/cutscene/item/control/cutscenescene.h new file mode 100644 index 00000000..2f659e42 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenescene.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 85435fa1..1a1ff3f2 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -127,6 +127,36 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { [CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = { .init = cutsceneBattleForceActionStart, .update = cutsceneBattleForceActionUpdate + }, + + [CUTSCENE_ITEM_TYPE_MODAL] = { + .init = cutsceneModalStart, + .update = cutsceneModalUpdate + }, + + [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 } }; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 0d39d6c7..532a9f16 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -7,9 +7,13 @@ #pragma once #include "cutscenecallback.h" +#include "cutsceneprint.h" #include "control/cutscenewait.h" #include "control/cutscenesetpause.h" #include "control/cutsceneconcurrent.h" +#include "control/cutscenemarker.h" +#include "control/cutscenerestart.h" +#include "control/cutscenescene.h" #include "entity/cutsceneentityteleport.h" #include "entity/cutsceneentitywalkto.h" #include "entity/cutsceneentityremove.h" @@ -22,6 +26,7 @@ #include "ui/cutscenefade.h" #include "ui/cutsceneemoji.h" #include "ui/cutsceneshake.h" +#include "ui/cutscenemodal.h" #include "item/cutsceneitemgive.h" #include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemaparearemove.h" @@ -59,6 +64,12 @@ typedef enum { CUTSCENE_ITEM_TYPE_SHAKE, CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, + CUTSCENE_ITEM_TYPE_MODAL, + CUTSCENE_ITEM_TYPE_MODAL_CLOSE, + CUTSCENE_ITEM_TYPE_PRINT, + CUTSCENE_ITEM_TYPE_MARKER, + CUTSCENE_ITEM_TYPE_RESTART, + CUTSCENE_ITEM_TYPE_SCENE, CUTSCENE_ITEM_TYPE_COUNT } cutsceneitemtype_t; @@ -91,6 +102,10 @@ struct cutsceneitem_s { cutsceneshake_t shake; cutscenebattlewaitstate_t battleWaitState; cutscenebattleforceaction_t battleForceAction; + cutscenemodal_t modal; + cutsceneprint_t print; + cutscenemarker_t marker; + cutscenescene_t sceneChange; }; }; diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.c b/src/dusk/rpg/cutscene/item/cutsceneprint.c new file mode 100644 index 00000000..9ccff491 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/cutsceneprint.c @@ -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; +} diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.h b/src/dusk/rpg/cutscene/item/cutsceneprint.h new file mode 100644 index 00000000..01abf3fd --- /dev/null +++ b/src/dusk/rpg/cutscene/item/cutsceneprint.h @@ -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 +); diff --git a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt index 8f2e44fc..1d966039 100644 --- a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt @@ -11,4 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenefade.c cutsceneemoji.c cutsceneshake.c + cutscenemodal.c ) diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c new file mode 100644 index 00000000..5c4c5822 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c @@ -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/frame/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; +} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h new file mode 100644 index 00000000..c1ff265b --- /dev/null +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h @@ -0,0 +1,105 @@ +/** + * 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. + * + * @param optionIndex Index into the options array that was selected, + * or UI_MODAL_RESULT_NONE if backed out of without selecting one. + * @param userData CUTSCENE_SYSTEM.userData for the running cutscene. + */ +typedef void (*cutscenemodaloptioncallback_t)( + const uint8_t optionIndex, void *userData +); + +typedef struct { + char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS]; + char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS]; + + // NOT copied - these pointers are stored directly by the underlying + // buttons, so they must stay valid for as long as the modal is open + // (e.g. string literals, as CUTSCENE_MODAL_OPTIONS produces). + 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 +); diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c index b669e65c..b3482d72 100644 --- a/src/dusk/scene/initial/sceneinitial.c +++ b/src/dusk/scene/initial/sceneinitial.c @@ -9,42 +9,65 @@ #include "assert/assert.h" #include "util/memory.h" #include "error/error.h" - +#include "display/screen/screen.h" #include "console/console.h" #include "save/save.h" +#include "ui/frame/uimodal.h" +#include "rpg/cutscene/cutscene.h" +#include "rpg/cutscene/cutscenesystem.h" -int32_t testData = 69; +void sceneInitialAvailableDeviceCallback(savedevice_t *device, void *user); +void sceneInitialFindDevices(void *userData); +void sceneInitialSaveDeviceRetryCallback(const uint8_t opt, void *u); -void testCallback(savedevice_t *device, void *user) { - if(device == 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 - ); +CUTSCENE(INITIAL, 0, DEFAULT, + CUTSCENE_MODAL( + "Checking for save data...", + "Checking for save data... Please wait." + ), + CUTSCENE_WAIT(1.0f), + CUTSCENE_CALLBACK(sceneInitialFindDevices), - 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); - } - } - } + CUTSCENE_MARKER("NO_DEVICE"), + CUTSCENE_MODAL_CLOSE(), + CUTSCENE_MODAL_OPTIONS( + "No save device found", + "Please ensure a save device is connected and try again.", + sceneInitialSaveDeviceRetryCallback, + "Retry", "Continue without saving" + ), + + CUTSCENE_MARKER("RETRY"), + CUTSCENE_MODAL_CLOSE(), + CUTSCENE_RESTART(), + + CUTSCENE_MARKER("CONTINUE"), + CUTSCENE_MODAL_CLOSE(), + CUTSCENE_SCENE(SCENE_TYPE_MAIN_MENU) +); + +void sceneInitialAvailableDeviceCallback(savedevice_t *device, void *user) { + cutsceneGoTo(device == NULL ? "NO_DEVICE" : "CONTINUE"); } +void sceneInitialFindDevices(void *userData) { + saveFindAvailableDevice(sceneInitialAvailableDeviceCallback, NULL); +} + +void sceneInitialSaveDeviceRetryCallback(const uint8_t opt, void *u) { + cutsceneGoTo(opt == 0 ? "RETRY" : "CONTINUE"); +} + + + errorret_t sceneInitialInit(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); memoryZero(&sceneData->initial, sizeof(sceneinitial_t)); - consolePrint("Going to find a save device."); - saveFindAvailableDevice( - testCallback, - &testData - ); + // Set background color to black for the initial scene + SCREEN.background = COLOR_BLACK; + + cutsceneSystemStartCutscene(&CUTSCENE_INITIAL); errorOk(); } diff --git a/src/dusk/scene/initial/sceneinitial.h b/src/dusk/scene/initial/sceneinitial.h index d184b62d..321f2da2 100644 --- a/src/dusk/scene/initial/sceneinitial.h +++ b/src/dusk/scene/initial/sceneinitial.h @@ -8,11 +8,9 @@ #pragma once #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 { - uint8_t reserved; + // uint32_t callbackState; + void *nothing; } sceneinitial_t; /** diff --git a/src/dusk/ui/frame/uimodal.c b/src/dusk/ui/frame/uimodal.c index e907a9b3..981b944d 100644 --- a/src/dusk/ui/frame/uimodal.c +++ b/src/dusk/ui/frame/uimodal.c @@ -28,7 +28,7 @@ void uiModalSelected( const uimenuitem_t *item ) { UI_MODAL.result = index; - uiModalClose(); + uiModalClose(NULL); } void uiModalClosed(const uimenu_t *menu) { @@ -53,7 +53,9 @@ errorret_t uiModalInit(void) { } errorret_t uiModalDraw(void) { - if(!uiMenuIsActive(&UI_MODAL.menu)) errorOk(); + if(!UI_MODAL.open) errorOk(); + + bool_t hasOptions = UI_MODAL.menu.itemCount > 0; spritebatchsprite_t backdropSprite = { .min = { 0.0f, 0.0f, 0.0f }, @@ -82,8 +84,8 @@ errorret_t uiModalDraw(void) { float_t width = contentWidth + (UI_FRAME_START_X * 2); float_t height = (UI_FRAME_START_Y * 2) + (float_t)UI_MODAL.titleLabel.height + UI_FRAME_PADDING_Y - + (float_t)UI_MODAL.messageLabel.height + UI_FRAME_PADDING_Y - + rowHeight; + + (float_t)UI_MODAL.messageLabel.height + + (hasOptions ? UI_FRAME_PADDING_Y + rowHeight : 0.0f); float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f; @@ -105,18 +107,20 @@ errorret_t uiModalDraw(void) { uiLabelSetY(&UI_MODAL.messageLabel, messageY); errorChain(uiLabelRender(&UI_MODAL.messageLabel, COLOR_WHITE)); - float_t buttonsY = messageY + (float_t)UI_MODAL.messageLabel.height + - UI_FRAME_PADDING_Y; - errorChain( - uiMenuDraw(&UI_MODAL.menu, contentX, buttonsY, contentWidth, rowHeight) - ); + if(hasOptions) { + float_t buttonsY = messageY + (float_t)UI_MODAL.messageLabel.height + + UI_FRAME_PADDING_Y; + errorChain( + uiMenuDraw(&UI_MODAL.menu, contentX, buttonsY, contentWidth, rowHeight) + ); + } errorChain(spriteBatchFlush()); errorOk(); } bool_t uiModalIsOpen(void) { - return uiMenuIsActive(&UI_MODAL.menu); + return UI_MODAL.open; } uint8_t uiModalGetResult(void) { @@ -128,13 +132,16 @@ void uiModalOpen( const char_t *message, const char_t **options, const uint8_t optionCount, - uimodalcallback_t callback, + uimodaloptioncallback_t callback, + uimodalopenedcallback_t onOpen, void *user ) { assertNotNull(title, "Title cannot be NULL"); assertNotNull(message, "Message cannot be NULL"); - assertNotNull(options, "Options cannot be NULL"); - assertTrue(optionCount > 0, "Must have at least one option"); + assertTrue( + options != NULL || optionCount == 0, + "Options cannot be NULL when optionCount > 0" + ); assertTrue( optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal" ); @@ -148,22 +155,43 @@ void uiModalOpen( uiLabelRebuffer(&UI_MODAL.messageLabel); UI_MODAL.callback = callback; + UI_MODAL.onOpen = onOpen; UI_MODAL.user = user; UI_MODAL.result = UI_MODAL_RESULT_NONE; + UI_MODAL.open = true; - MENU_BEGIN( - &UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL - ); - for(uint8_t i = 0; i < optionCount; i++) { - MENU_BUTTON(options[i]); + if(options != NULL) { + MENU_BEGIN( + &UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL + ); + for(uint8_t i = 0; i < optionCount; i++) { + MENU_BUTTON(options[i]); + } + MENU_END(UI_MODAL.options, menuIndex); + + uiMenuOpen(&UI_MODAL.menu); + } else { + uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL); } - MENU_END(UI_MODAL.options, menuIndex); - uiMenuOpen(&UI_MODAL.menu); + if(UI_MODAL.onOpen != NULL) { + UI_MODAL.onOpen(UI_MODAL.user); + } } -void uiModalClose(void) { - uiMenuClose(&UI_MODAL.menu); +void uiModalClose(uimodalclosedcallback_t callback) { + if(!UI_MODAL.open) return; + UI_MODAL.open = false; + + if(uiMenuIsActive(&UI_MODAL.menu)) { + uiMenuClose(&UI_MODAL.menu); + } else { + uiModalClosed(NULL); + } + + if(callback != NULL) { + callback(UI_MODAL.user); + } } errorret_t uiModalDispose(void) { diff --git a/src/dusk/ui/frame/uimodal.h b/src/dusk/ui/frame/uimodal.h index 41d77155..b496425e 100644 --- a/src/dusk/ui/frame/uimodal.h +++ b/src/dusk/ui/frame/uimodal.h @@ -26,7 +26,22 @@ * selecting an option. * @param user Arbitrary pointer passed to uiModalOpen. */ -typedef void (*uimodalcallback_t)(const uint8_t optionIndex, void *user); +typedef void (*uimodaloptioncallback_t)(const uint8_t optionIndex, void *user); + +/** + * Callback invoked when a modal opens. Fired immediately for now, but + * gives a hook point for a future open transition to key off of. + * + * @param user Arbitrary pointer passed to uiModalOpen. + */ +typedef void (*uimodalopenedcallback_t)(void *user); + +/** + * Callback invoked when a modal closes, passed to uiModalClose. + * + * @param user Arbitrary pointer passed to uiModalOpen. + */ +typedef void (*uimodalclosedcallback_t)(void *user); typedef struct { uilabel_t titleLabel; @@ -40,9 +55,11 @@ typedef struct { uimenu_t menu; uimenuitem_t options[UI_MODAL_OPTIONS_MAX]; - uimodalcallback_t callback; + uimodaloptioncallback_t callback; + uimodalopenedcallback_t onOpen; void *user; uint8_t result; + bool_t open; } uimodal_t; extern uimodal_t UI_MODAL; @@ -88,25 +105,33 @@ uint8_t uiModalGetResult(void); * @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. + * literals or locale-owned strings). May be NULL for a message-only + * dialog with no option buttons, in which case optionCount must be 0. + * @param optionCount Number of options, from 0 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. + * @param onOpen Called once the modal has opened. May be NULL. + * @param user Arbitrary pointer passed through to callback and onOpen, + * and to whatever callback is later passed to uiModalClose. */ void uiModalOpen( const char_t *title, const char_t *message, const char_t **options, const uint8_t optionCount, - uimodalcallback_t callback, + uimodaloptioncallback_t callback, + uimodalopenedcallback_t onOpen, void *user ); /** * Closes the modal dialog. No-op when already closed. + * + * @param callback Called with the modal's user pointer once the dialog + * has closed, after the option-result callback set by uiModalOpen. May + * be NULL. Not called if the modal was already closed. */ -void uiModalClose(void); +void uiModalClose(uimodalclosedcallback_t callback); /** * Disposes of the modal dialog.