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:
2026-08-20 14:35:41 -05:00
parent 774c8ad0f8
commit e225a076f0
22 changed files with 705 additions and 58 deletions
+60
View File
@@ -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, \
+33
View File
@@ -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;
+19
View File
@@ -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
);
+30
View File
@@ -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
}
};
+15
View File
@@ -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
);
+48 -25
View File
@@ -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();
}
+2 -4
View File
@@ -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;
/**
+37 -9
View File
@@ -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));
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,9 +155,12 @@ 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;
if(options != NULL) {
MENU_BEGIN(
&UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL
);
@@ -160,10 +170,28 @@ void uiModalOpen(
MENU_END(UI_MODAL.options, menuIndex);
uiMenuOpen(&UI_MODAL.menu);
} else {
uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
}
if(UI_MODAL.onOpen != NULL) {
UI_MODAL.onOpen(UI_MODAL.user);
}
}
void uiModalClose(void) {
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) {
+32 -7
View File
@@ -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.