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>
This commit is contained in:
@@ -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, \
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutsceneitem.c
|
||||
cutscenecallback.c
|
||||
cutsceneprint.c
|
||||
)
|
||||
|
||||
add_subdirectory(control)
|
||||
|
||||
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenewait.c
|
||||
cutscenesetpause.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
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -11,4 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenefade.c
|
||||
cutsceneemoji.c
|
||||
cutsceneshake.c
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
+50
-22
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user