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
// 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 by this
// item, so they must stay valid until the modal opens (string literals
// are fine). Like TITLE and MESSAGE, each option is translated if it
// matches a locale message ID.
// Back/cancel input is disabled while it's open (see
// uiMenuSetDisableBack), so it must be dismissed by picking one; CALLBACK
// then fires with the selected option index once the dialog closes.
// Option labels are passed as trailing arguments, e.g.
// CUTSCENE_MODAL_OPTIONS(title, message, callback, "Retry", "Cancel") -
// their strings are not copied by this item, so they must stay valid
// until the modal opens (string literals are fine). Like TITLE and
// MESSAGE, each option is translated if it matches a locale message ID.
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL, \
@@ -121,10 +122,9 @@ typedef struct cutscene_s {
}
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and
// MARKER1 to jump straight to via cutsceneGoTo once it's selected (or
// the dialog is backed out of) - no callback function to write. Does
// not fall through to whatever follows this item, so MARKER1 must be
// scripted elsewhere in the same cutscene.
// MARKER1 to jump straight to via cutsceneGoTo once it's selected - no
// callback function to write. Does not fall through to whatever follows
// this item, so MARKER1 must be scripted elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \
{ \
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
@@ -139,10 +139,8 @@ typedef struct cutscene_s {
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options,
// jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo
// once the corresponding option is selected - no callback function to
// write. Backing out without selecting (UI_MODAL_RESULT_NONE) is
// treated the same as selecting option 2. Does not fall through to
// whatever follows this item, so both markers must be scripted
// elsewhere in the same cutscene.
// write. Does not fall through to whatever follows this item, so both
// markers must be scripted elsewhere in the same cutscene.
#define CUTSCENE_MODAL_OPTIONS_TWO( \
TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \
) \
+21 -5
View File
@@ -17,11 +17,7 @@ void cutsceneSystemInit() {
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
}
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
}
void cutsceneSystemStartCutsceneWith(
void cutsceneSystemPrepare(
const cutscene_t *cutscene,
entity_t *interact,
entity_t *interacted
@@ -40,9 +36,29 @@ void cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
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();
}
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
) {
cutsceneSystemPrepare(cutscene, NULL, NULL);
cutsceneGoTo(marker);
}
void cutsceneRestart(void) {
assertNotNull(
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
+15
View File
@@ -67,6 +67,21 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interacted
);
/**
* Starts a cutscene with no bound entities, jumping straight to the
* CUTSCENE_MARKER item with the given name instead of running from the
* first item - as if cutsceneGoTo(marker) had been called immediately
* after cutsceneSystemStartCutscene. Asserts if no marker with that
* name exists in the cutscene.
*
* @param cutscene Pointer to the cutscene to start.
* @param marker Marker name to jump to, matched with stringEquals.
*/
void cutsceneSystemStartCutsceneAndGoToMarker(
const cutscene_t *cutscene,
const char_t *marker
);
/**
* Restarts the currently running cutscene from its first item,
* preserving whatever interact/interacted entities triggered it.
@@ -17,10 +17,10 @@ typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* 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,
* or UI_MODAL_RESULT_NONE if backed out of without selecting one.
* @param optionIndex Index into the options array that was selected.
* @param userData CUTSCENE_SYSTEM.userData for the running cutscene.
*/
typedef void (*cutscenemodaloptioncallback_t)(
@@ -16,10 +16,10 @@ void cutsceneModalOptionsMarkersCallback(
const cutscenemodaloptionsmarkers_t *options =
&cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
// A backed-out-of dialog (UI_MODAL_RESULT_NONE) falls back to the
// last option's marker, same as a hand-written two-option callback
// (e.g. sceneInitialSaveDeviceRetryCallback) treating "backed out" as
// the safe/continue choice rather than the destructive one.
// uiModalOpen disables back/cancel for a dialog with options, so
// UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
// the last option's marker only matters if something ever force-closes
// the modal directly (uiModalClose) before an option is picked.
const uint8_t index =
optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
cutsceneGoTo(options->markers[index]);
@@ -42,9 +42,10 @@ void cutsceneModalOptionsMarkersStart(
/**
* Updates a modal-options-markers item. Never completes on its own -
* the cutscene blocks here indefinitely. Once an option is selected (or
* the dialog is backed out of, which is treated as selecting the last
* option), it jumps straight to that option's marker via cutsceneGoTo.
* the cutscene blocks here indefinitely. Back/cancel input is disabled
* while it's open (see uiMenuSetDisableBack), so it must be dismissed
* by picking an option, which jumps straight to that option's marker
* via cutsceneGoTo.
*
* @param item The cutscene item.
* @param data Runtime data storage.
+1 -1
View File
@@ -19,7 +19,7 @@ CUTSCENE(INITIAL, 0, DEFAULT,
CUTSCENE_MODAL(
"initial.checking_save.title", "initial.checking_save.message"
),
CUTSCENE_WAIT(1.0f),
CUTSCENE_WAIT(0.2f),
CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "NO_DEVICE"),
CUTSCENE_MARKER("NO_DEVICE"),
+5 -4
View File
@@ -9,10 +9,12 @@
#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
* selected or the dialog was backed out of.
* selected.
* @param user Arbitrary pointer passed to uiConfirmOpen.
*/
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
* question text and Confirm/Cancel buttons. callback is invoked exactly
* once with the result, whether the dialog was dismissed by selecting a
* button or by pressing cancel/back.
* once with the result once a button is selected.
*
* @param question Display text, or a locale message ID; copied
* internally by uimodal, safe to be transient.
+1 -1
View File
@@ -153,7 +153,7 @@ void uiFocusUpdate(void) {
}
if(inputPressed(INPUT_ACTION_CANCEL)) {
uiFocusPop();
if(!item->disableBack) uiFocusPop();
return;
}
+5
View File
@@ -54,4 +54,9 @@ struct uifocusitem_s {
uifocusitemcallback_t closed;
uifocusitemdirectioncallback_t direction;
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.message"
),
CUTSCENE_WAIT(1.0f),
CUTSCENE_WAIT(0.2f),
CUTSCENE_SAVE_DEVICE_CHECK("CONTINUE", "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(
const uimenu_t *menu,
const uint8_t index,
@@ -107,7 +98,6 @@ void uiMainMenuSelected(
switch(index) {
case UI_MAIN_MENU_INDEX_START_GAME:
UI_MAIN_MENU.suppressClosedSceneChange = true;
uiMenuClose(&UI_MAIN_MENU.menu);
cutsceneSystemStartCutscene(&CUTSCENE_MAIN_MENU_START_GAME);
break;
@@ -151,14 +141,17 @@ errorret_t uiMainMenuInit(void) {
));
MENU_BEGIN(
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected,
uiMainMenuClosed, NULL
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
);
MENU_BUTTON(UI_MAIN_MENU.startGameLabel);
MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
MENU_BUTTON(UI_MAIN_MENU.quitLabel);
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();
}
-6
View File
@@ -20,12 +20,6 @@ typedef struct {
char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX];
char_t optionsLabel[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;
extern uimainmenu_t UI_MAIN_MENU;
+7
View File
@@ -67,6 +67,7 @@ void uiMenuOpen(uimenu_t *menu) {
uiMenuFocusDirection,
menu
);
menu->focusItem->disableBack = menu->disableBack;
}
void uiMenuClose(uimenu_t *menu) {
@@ -76,6 +77,12 @@ void uiMenuClose(uimenu_t *menu) {
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) {
assertNotNull(menu, "Menu cannot be NULL");
return menu->focusItem != NULL;
+14
View File
@@ -66,6 +66,9 @@ typedef struct uimenu_s {
uimenuchangedcallback_t changed;
void *user;
// See uiMenuSetDisableBack.
bool_t disableBack;
} uimenu_t;
/**
@@ -124,6 +127,17 @@ void uiMenuOpen(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).
*
+3
View File
@@ -202,6 +202,9 @@ void uiModalOpen(
}
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);
} else {
uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL);
+7 -4
View File
@@ -27,8 +27,10 @@
* Callback invoked once a modal is dismissed.
*
* @param optionIndex Index into the options array passed to uiModalOpen
* that was selected, or UI_MODAL_RESULT_NONE if backed out of without
* selecting an option.
* that was selected. A dialog with options ignores back/cancel input
* (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.
*/
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.
* callback is invoked exactly once with the result, whether the dialog
* was dismissed by selecting an option or by pressing cancel/back.
* callback is invoked exactly once with the selected option. A dialog
* 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
* first: if one matches an entry in the active locale file its