Add uimenu disableBack flag, apply to main menu and all modal options

uifocusitem_t/uimenu_t gain a disableBack flag that makes back/cancel a
no-op while that item is focused, set via uiMenuSetDisableBack. Applied
centrally in uimodal.c so every modal with option buttons (confirm
dialogs, cutscene modals) must be dismissed by picking an option rather
than backing out, and to the main menu so it no longer bounces back to
the initial scene on cancel - replacing the suppressClosedSceneChange
hack entirely. Updated docs that described the now-unreachable
back/cancel dismissal path. Also adds
cutsceneSystemStartCutsceneAndGoToMarker for starting a cutscene
straight at a given marker instead of its first item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 22:11:27 -05:00
parent 1a199f6ce3
commit 4a26f79945
16 changed files with 110 additions and 60 deletions
+13 -15
View File
@@ -100,13 +100,14 @@ typedef struct cutscene_s {
// Shows a modal with option buttons and immediately continues on, same // Shows a modal with option buttons and immediately continues on, same
// as CUTSCENE_MODAL - it does not block waiting for a selection. // as CUTSCENE_MODAL - it does not block waiting for a selection.
// CALLBACK fires with the selected option index (or UI_MODAL_RESULT_NONE // Back/cancel input is disabled while it's open (see
// if backed out of) once the dialog closes. Option labels are passed as // uiMenuSetDisableBack), so it must be dismissed by picking one; CALLBACK
// trailing arguments, e.g. CUTSCENE_MODAL_OPTIONS(title, message, // then fires with the selected option index once the dialog closes.
// callback, "Retry", "Cancel") - their strings are not copied by this // Option labels are passed as trailing arguments, e.g.
// item, so they must stay valid until the modal opens (string literals // CUTSCENE_MODAL_OPTIONS(title, message, callback, "Retry", "Cancel") -
// are fine). Like TITLE and MESSAGE, each option is translated if it // their strings are not copied by this item, so they must stay valid
// matches a locale message ID. // until the modal opens (string literals are fine). Like TITLE and
// MESSAGE, each option is translated if it matches a locale message ID.
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \ #define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_MODAL, \ .type = CUTSCENE_ITEM_TYPE_MODAL, \
@@ -121,10 +122,9 @@ typedef struct cutscene_s {
} }
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and // Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and
// MARKER1 to jump straight to via cutsceneGoTo once it's selected (or // MARKER1 to jump straight to via cutsceneGoTo once it's selected - no
// the dialog is backed out of) - no callback function to write. Does // callback function to write. Does not fall through to whatever follows
// not fall through to whatever follows this item, so MARKER1 must be // this item, so MARKER1 must be scripted elsewhere in the same cutscene.
// scripted elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \ #define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \ .type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
@@ -139,10 +139,8 @@ typedef struct cutscene_s {
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options, // Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options,
// jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo // jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo
// once the corresponding option is selected - no callback function to // once the corresponding option is selected - no callback function to
// write. Backing out without selecting (UI_MODAL_RESULT_NONE) is // write. Does not fall through to whatever follows this item, so both
// treated the same as selecting option 2. Does not fall through to // markers must be scripted elsewhere in the same cutscene.
// whatever follows this item, so both markers must be scripted
// elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_TWO( \ #define CUTSCENE_MODAL_OPTIONS_TWO( \
TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \ TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \
) \ ) \
+21 -5
View File
@@ -17,11 +17,7 @@ void cutsceneSystemInit() {
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
} }
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { void cutsceneSystemPrepare(
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
}
void cutsceneSystemStartCutsceneWith(
const cutscene_t *cutscene, const cutscene_t *cutscene,
entity_t *interact, entity_t *interact,
entity_t *interacted entity_t *interacted
@@ -40,9 +36,29 @@ void cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
}
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
}
void cutsceneSystemStartCutsceneWith(
const cutscene_t *cutscene,
entity_t *interact,
entity_t *interacted
) {
cutsceneSystemPrepare(cutscene, interact, interacted);
cutsceneSystemNext(); cutsceneSystemNext();
} }
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
) {
cutsceneSystemPrepare(cutscene, NULL, NULL);
cutsceneGoTo(marker);
}
void cutsceneRestart(void) { void cutsceneRestart(void) {
assertNotNull( assertNotNull(
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running" CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
+15
View File
@@ -67,6 +67,21 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interacted entity_t *interacted
); );
/**
* Starts a cutscene with no bound entities, jumping straight to the
* CUTSCENE_MARKER item with the given name instead of running from the
* first item - as if cutsceneGoTo(marker) had been called immediately
* after cutsceneSystemStartCutscene. Asserts if no marker with that
* name exists in the cutscene.
*
* @param cutscene Pointer to the cutscene to start.
* @param marker Marker name to jump to, matched with stringEquals.
*/
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
);
/** /**
* Restarts the currently running cutscene from its first item, * Restarts the currently running cutscene from its first item,
* preserving whatever interact/interacted entities triggered it. * preserving whatever interact/interacted entities triggered it.
@@ -17,10 +17,10 @@ typedef union cutsceneitemdata_u cutsceneitemdata_t;
/** /**
* Callback invoked with the selected option once a cutscene modal * Callback invoked with the selected option once a cutscene modal
* item's dialog closes. * item's dialog closes. Back/cancel input is disabled while it's open
* (see uiMenuSetDisableBack), so it must be dismissed by picking one.
* *
* @param optionIndex Index into the options array that was selected, * @param 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. * @param userData CUTSCENE_SYSTEM.userData for the running cutscene.
*/ */
typedef void (*cutscenemodaloptioncallback_t)( typedef void (*cutscenemodaloptioncallback_t)(
@@ -16,10 +16,10 @@ void cutsceneModalOptionsMarkersCallback(
const cutscenemodaloptionsmarkers_t *options = const cutscenemodaloptionsmarkers_t *options =
&cutsceneSystemGetCurrentItem()->modalOptionsMarkers; &cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
// A backed-out-of dialog (UI_MODAL_RESULT_NONE) falls back to the // uiModalOpen disables back/cancel for a dialog with options, so
// last option's marker, same as a hand-written two-option callback // UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
// (e.g. sceneInitialSaveDeviceRetryCallback) treating "backed out" as // the last option's marker only matters if something ever force-closes
// the safe/continue choice rather than the destructive one. // the modal directly (uiModalClose) before an option is picked.
const uint8_t index = const uint8_t index =
optionIndex < options->optionCount ? optionIndex : options->optionCount - 1; optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
cutsceneGoTo(options->markers[index]); cutsceneGoTo(options->markers[index]);
@@ -42,9 +42,10 @@ void cutsceneModalOptionsMarkersStart(
/** /**
* Updates a modal-options-markers item. Never completes on its own - * Updates a modal-options-markers item. Never completes on its own -
* the cutscene blocks here indefinitely. Once an option is selected (or * the cutscene blocks here indefinitely. Back/cancel input is disabled
* the dialog is backed out of, which is treated as selecting the last * while it's open (see uiMenuSetDisableBack), so it must be dismissed
* option), it jumps straight to that option's marker via cutsceneGoTo. * by picking an option, which jumps straight to that option's marker
* via cutsceneGoTo.
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
+1 -1
View File
@@ -19,7 +19,7 @@ CUTSCENE(INITIAL, 0, DEFAULT,
CUTSCENE_MODAL( CUTSCENE_MODAL(
"initial.checking_save.title", "initial.checking_save.message" "initial.checking_save.title", "initial.checking_save.message"
), ),
CUTSCENE_WAIT(1.0f), CUTSCENE_WAIT(0.2f),
CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "NO_DEVICE"), CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "NO_DEVICE"),
CUTSCENE_MARKER("NO_DEVICE"), CUTSCENE_MARKER("NO_DEVICE"),
+5 -4
View File
@@ -9,10 +9,12 @@
#include "error/error.h" #include "error/error.h"
/** /**
* Callback invoked once a confirm dialog is dismissed. * Callback invoked once a confirm dialog is dismissed. Back/cancel
* input is disabled while it's open, so the dialog can only be
* dismissed by picking Confirm or Cancel.
* *
* @param result True if Confirm was selected, false if Cancel was * @param result True if Confirm was selected, false if Cancel was
* selected or the dialog was backed out of. * selected.
* @param user Arbitrary pointer passed to uiConfirmOpen. * @param user Arbitrary pointer passed to uiConfirmOpen.
*/ */
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user); typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
@@ -48,8 +50,7 @@ bool_t uiConfirmGetResult(void);
/** /**
* Opens a confirm dialog, backed by the shared uimodal_t, with the given * Opens a confirm dialog, backed by the shared uimodal_t, with the given
* question text and Confirm/Cancel buttons. callback is invoked exactly * question text and Confirm/Cancel buttons. callback is invoked exactly
* once with the result, whether the dialog was dismissed by selecting a * once with the result once a button is selected.
* button or by pressing cancel/back.
* *
* @param question Display text, or a locale message ID; copied * @param question Display text, or a locale message ID; copied
* internally by uimodal, safe to be transient. * internally by uimodal, safe to be transient.
+1 -1
View File
@@ -153,7 +153,7 @@ void uiFocusUpdate(void) {
} }
if(inputPressed(INPUT_ACTION_CANCEL)) { if(inputPressed(INPUT_ACTION_CANCEL)) {
uiFocusPop(); if(!item->disableBack) uiFocusPop();
return; return;
} }
+5
View File
@@ -54,4 +54,9 @@ struct uifocusitem_s {
uifocusitemcallback_t closed; uifocusitemcallback_t closed;
uifocusitemdirectioncallback_t direction; uifocusitemdirectioncallback_t direction;
void *user; void *user;
// When true, INPUT_ACTION_CANCEL is ignored while this item is the
// topmost focus item - it does not pop. Does not affect a
// programmatic uiFocusPop()/uiFocusPopItem() call.
bool_t disableBack;
}; };
+6 -13
View File
@@ -36,7 +36,7 @@ CUTSCENE(MAIN_MENU_START_GAME, 0, DEFAULT,
"main_menu.checking_save.title", "main_menu.checking_save.title",
"main_menu.checking_save.message" "main_menu.checking_save.message"
), ),
CUTSCENE_WAIT(1.0f), CUTSCENE_WAIT(0.2f),
CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "NO_DEVICE"), CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "NO_DEVICE"),
CUTSCENE_MARKER("NO_DEVICE"), CUTSCENE_MARKER("NO_DEVICE"),
@@ -90,15 +90,6 @@ void uiMainMenuOpenSelectSave(void *userData) {
); );
} }
void uiMainMenuClosed(const uimenu_t *menu) {
if(UI_MAIN_MENU.suppressClosedSceneChange) {
UI_MAIN_MENU.suppressClosedSceneChange = false;
return;
}
sceneSet(SCENE_TYPE_INITIAL);
}
void uiMainMenuSelected( void uiMainMenuSelected(
const uimenu_t *menu, const uimenu_t *menu,
const uint8_t index, const uint8_t index,
@@ -107,7 +98,6 @@ void uiMainMenuSelected(
switch(index) { switch(index) {
case UI_MAIN_MENU_INDEX_START_GAME: case UI_MAIN_MENU_INDEX_START_GAME:
UI_MAIN_MENU.suppressClosedSceneChange = true;
uiMenuClose(&UI_MAIN_MENU.menu); uiMenuClose(&UI_MAIN_MENU.menu);
cutsceneSystemStartCutscene(&CUTSCENE_MAIN_MENU_START_GAME); cutsceneSystemStartCutscene(&CUTSCENE_MAIN_MENU_START_GAME);
break; break;
@@ -151,14 +141,17 @@ errorret_t uiMainMenuInit(void) {
)); ));
MENU_BEGIN( MENU_BEGIN(
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, &UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
uiMainMenuClosed, NULL
); );
MENU_BUTTON(UI_MAIN_MENU.startGameLabel); MENU_BUTTON(UI_MAIN_MENU.startGameLabel);
MENU_BUTTON(UI_MAIN_MENU.optionsLabel); MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
MENU_BUTTON(UI_MAIN_MENU.quitLabel); MENU_BUTTON(UI_MAIN_MENU.quitLabel);
MENU_END(UI_MAIN_MENU.items, 1); MENU_END(UI_MAIN_MENU.items, 1);
// The main menu is the root screen once past the initial boot check -
// there's nothing to back out to, so back/cancel does nothing here.
uiMenuSetDisableBack(&UI_MAIN_MENU.menu, true);
errorOk(); errorOk();
} }
-6
View File
@@ -20,12 +20,6 @@ typedef struct {
char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX]; char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX];
char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX]; char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX];
char_t quitLabel[UI_MAIN_MENU_LABEL_MAX]; char_t quitLabel[UI_MAIN_MENU_LABEL_MAX];
// Set right before a uiMenuClose() call that hands off to something
// else (e.g. starting the start-game cutscene) rather than backing
// all the way out, so the menu's closed handler knows to skip
// returning to the initial scene for that one close.
bool_t suppressClosedSceneChange;
} uimainmenu_t; } uimainmenu_t;
extern uimainmenu_t UI_MAIN_MENU; extern uimainmenu_t UI_MAIN_MENU;
+7
View File
@@ -67,6 +67,7 @@ void uiMenuOpen(uimenu_t *menu) {
uiMenuFocusDirection, uiMenuFocusDirection,
menu menu
); );
menu->focusItem->disableBack = menu->disableBack;
} }
void uiMenuClose(uimenu_t *menu) { void uiMenuClose(uimenu_t *menu) {
@@ -76,6 +77,12 @@ void uiMenuClose(uimenu_t *menu) {
menu->focusItem = NULL; menu->focusItem = NULL;
} }
void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack) {
assertNotNull(menu, "Menu cannot be NULL");
menu->disableBack = disableBack;
if(menu->focusItem != NULL) menu->focusItem->disableBack = disableBack;
}
bool_t uiMenuIsActive(const uimenu_t *menu) { bool_t uiMenuIsActive(const uimenu_t *menu) {
assertNotNull(menu, "Menu cannot be NULL"); assertNotNull(menu, "Menu cannot be NULL");
return menu->focusItem != NULL; return menu->focusItem != NULL;
+14
View File
@@ -66,6 +66,9 @@ typedef struct uimenu_s {
uimenuchangedcallback_t changed; uimenuchangedcallback_t changed;
void *user; void *user;
// See uiMenuSetDisableBack.
bool_t disableBack;
} uimenu_t; } uimenu_t;
/** /**
@@ -124,6 +127,17 @@ void uiMenuOpen(uimenu_t *menu);
*/ */
void uiMenuClose(uimenu_t *menu); void uiMenuClose(uimenu_t *menu);
/**
* Sets whether pressing back/cancel is ignored while this menu is the
* topmost focus item - it simply does nothing, rather than closing the
* menu. Does not affect a deliberate uiMenuClose() call. Safe to call
* before or after uiMenuOpen().
*
* @param menu The menu to update.
* @param disableBack True to ignore back/cancel input for this menu.
*/
void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack);
/** /**
* Returns whether the menu is currently active (on the UI focus stack). * Returns whether the menu is currently active (on the UI focus stack).
* *
+3
View File
@@ -202,6 +202,9 @@ void uiModalOpen(
} }
MENU_END(UI_MODAL.options, menuIndex); MENU_END(UI_MODAL.options, menuIndex);
// A dialog with options must be dismissed by picking one, not by
// backing out of it - see uiMenuSetDisableBack.
uiMenuSetDisableBack(&UI_MODAL.menu, true);
uiMenuOpen(&UI_MODAL.menu); uiMenuOpen(&UI_MODAL.menu);
} else { } else {
uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL); uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
+7 -4
View File
@@ -27,8 +27,10 @@
* Callback invoked once a modal is dismissed. * Callback invoked once a modal is dismissed.
* *
* @param optionIndex Index into the options array passed to uiModalOpen * @param optionIndex Index into the options array passed to uiModalOpen
* that was selected, or UI_MODAL_RESULT_NONE if backed out of without * that was selected. A dialog with options ignores back/cancel input
* selecting an option. * (see uiMenuSetDisableBack) and must be dismissed by picking one, so
* UI_MODAL_RESULT_NONE only reaches here via a direct uiModalClose()
* call made before any option was picked.
* @param user Arbitrary pointer passed to uiModalOpen. * @param user Arbitrary pointer passed to uiModalOpen.
*/ */
typedef void (*uimodaloptioncallback_t)(const uint8_t optionIndex, void *user); typedef void (*uimodaloptioncallback_t)(const uint8_t optionIndex, void *user);
@@ -103,8 +105,9 @@ uint8_t uiModalGetResult(void);
/** /**
* Opens the modal dialog with the given title, message, and options. * Opens the modal dialog with the given title, message, and options.
* callback is invoked exactly once with the result, whether the dialog * callback is invoked exactly once with the selected option. A dialog
* was dismissed by selecting an option or by pressing cancel/back. * with options > 0 disables back/cancel (see uiMenuSetDisableBack), so
* it must be dismissed by picking one.
* *
* title, message, and each option are treated as locale message IDs * title, message, and each option are treated as locale message IDs
* first: if one matches an entry in the active locale file its * first: if one matches an entry in the active locale file its