From 2c83cc890d70f4f68e1ca1e7bee753905efca467 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Mon, 27 Jul 2026 16:13:57 -0500 Subject: Option to auto advance text to credits (#1821) --- mm/2s2h/BenGui/BenMenu.cpp | 5 +++ .../Cutscenes/AutoAdvanceEndingText.cpp | 42 ++++++++++++++++++++++ .../GameInteractor_VanillaBehavior.h | 8 +++++ mm/2s2h/PresetManager/PresetManager.cpp | 1 + mm/src/code/z_message.c | 32 ++++++++++------- 5 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 mm/2s2h/Enhancements/Cutscenes/AutoAdvanceEndingText.cpp diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index 81537b383..56dad0d58 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -1572,6 +1572,11 @@ void BenMenu::AddEnhancements() { AddWidget(path, "Skip Bottle Pickup Messages", WIDGET_CVAR_CHECKBOX) .CVar("gEnhancements.Dialogue.SkipBottlePickupMessages") .Options(CheckboxOptions().Tooltip("Skip pickup messages for bottle swipes.")); + AddWidget(path, "Auto Advance Ending Text", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Cutscenes.AutoAdvanceEndingText") + .Options(CheckboxOptions().Tooltip( + "After completing the game, textboxes shown on the way to the credits advance on their own " + "once they've been up long enough to read, so the ending plays out without controller input.")); path.column = SECTION_COLUMN_3; AddWidget(path, "Other", WIDGET_SEPARATOR_TEXT); diff --git a/mm/2s2h/Enhancements/Cutscenes/AutoAdvanceEndingText.cpp b/mm/2s2h/Enhancements/Cutscenes/AutoAdvanceEndingText.cpp new file mode 100644 index 000000000..54ae72173 --- /dev/null +++ b/mm/2s2h/Enhancements/Cutscenes/AutoAdvanceEndingText.cpp @@ -0,0 +1,42 @@ +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" + +extern "C" { +#include "variables.h" +} + +#define CVAR_NAME "gEnhancements.Cutscenes.AutoAdvanceEndingText" +#define CVAR CVarGetInteger(CVAR_NAME, 0) + +static bool sAdvanceMessages = false; +static u32 sTextboxTimer = 0; + +static bool IsWaitingOnPlayer(MessageContext* msgCtx) { + return (msgCtx->msgLength != 0) && + ((msgCtx->msgMode == MSGMODE_TEXT_AWAIT_INPUT) || (msgCtx->msgMode == MSGMODE_TEXT_AWAIT_NEXT) || + (msgCtx->msgMode == MSGMODE_TEXT_DONE)); +} + +static RegisterShipInitFunc initFunc( + []() { + // Unconditionally registering these in case the user forgets to turn this on prior to finishing + COND_HOOK(OnGameCompletion, true, []() { sAdvanceMessages = true; }); + COND_HOOK(OnSaveLoad, true, [](s16 fileNum) { sAdvanceMessages = false; }); + + COND_HOOK(OnGameStateUpdate, CVAR, []() { + if (sAdvanceMessages && (gPlayState != NULL) && IsWaitingOnPlayer(&gPlayState->msgCtx)) { + sTextboxTimer++; + } else { + sTextboxTimer = 0; + } + }); + + COND_VB_SHOULD(VB_MSG_ADVANCE, CVAR, { + if (sAdvanceMessages && (gPlayState != NULL) && (sTextboxTimer >= 20)) { + sTextboxTimer = 0; + *should = true; + } + }); + }, + { CVAR_NAME }); diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h index 143c1d1a2..8103fe6b1 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h +++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h @@ -1420,6 +1420,14 @@ typedef enum { // - `*EnMnk` VB_MONKEY_WAIT_TO_TALK_AFTER_APPROACH, + // #### `result` + // ```c + // CHECK_BTN_ALL(controller->press.button, BTN_A) || ... + // ``` + // #### `args` + // - None + VB_MSG_ADVANCE, + // #### `result` // ```c // false diff --git a/mm/2s2h/PresetManager/PresetManager.cpp b/mm/2s2h/PresetManager/PresetManager.cpp index f61c8a94e..44dea18be 100644 --- a/mm/2s2h/PresetManager/PresetManager.cpp +++ b/mm/2s2h/PresetManager/PresetManager.cpp @@ -69,6 +69,7 @@ nlohmann::json curatedPresetJ = R"( "FixTargettingCameraSnap": 1 }, "Cutscenes": { + "AutoAdvanceEndingText": 1, "HideTitleCards": 1, "SkipEnemyCutscenes": 1, "SkipEntranceCutscenes": 1, diff --git a/mm/src/code/z_message.c b/mm/src/code/z_message.c index 1e0a6eed6..c1450cdf3 100644 --- a/mm/src/code/z_message.c +++ b/mm/src/code/z_message.c @@ -291,40 +291,48 @@ void Message_ResetOcarinaButtonState(PlayState* play) { bool Message_ShouldAdvance(PlayState* play) { MessageContext* msgCtx = &play->msgCtx; Input* controller = CONTROLLER1(&play->state); + bool shouldAdvance; if ((msgCtx->textboxEndType == TEXTBOX_ENDTYPE_TWO_CHOICE) || (msgCtx->textboxEndType == TEXTBOX_ENDTYPE_THREE_CHOICE)) { if (CHECK_BTN_ALL(controller->press.button, BTN_A)) { Audio_PlaySfx(NA_SE_SY_MESSAGE_PASS); } - return CHECK_BTN_ALL(controller->press.button, BTN_A); + shouldAdvance = CHECK_BTN_ALL(controller->press.button, BTN_A); } else { if (CHECK_BTN_ALL(controller->press.button, BTN_A) || CHECK_BTN_ALL(controller->press.button, BTN_B) || CHECK_BTN_ALL(controller->press.button, BTN_CUP)) { Audio_PlaySfx(NA_SE_SY_MESSAGE_PASS); } - return CHECK_BTN_ALL(controller->press.button, BTN_A) || CHECK_BTN_ALL(controller->press.button, BTN_B) || - // 2S2H [Enhancement] When fast text is on, we want to check if B is held instead of only if it was just - // pressed - (CVarGetInteger("gEnhancements.Dialogue.FastText", 0) && CHECK_BTN_ALL(controller->cur.button, BTN_B)) || - CHECK_BTN_ALL(controller->press.button, BTN_CUP); + shouldAdvance = + CHECK_BTN_ALL(controller->press.button, BTN_A) || CHECK_BTN_ALL(controller->press.button, BTN_B) || + // 2S2H [Enhancement] When fast text is on, we want to check if B is held instead of only if it was just + // pressed + (CVarGetInteger("gEnhancements.Dialogue.FastText", 0) && CHECK_BTN_ALL(controller->cur.button, BTN_B)) || + CHECK_BTN_ALL(controller->press.button, BTN_CUP); } + + return GameInteractor_Should(VB_MSG_ADVANCE, shouldAdvance); } bool Message_ShouldAdvanceSilent(PlayState* play) { MessageContext* msgCtx = &play->msgCtx; Input* controller = CONTROLLER1(&play->state); + bool shouldAdvance; if ((msgCtx->textboxEndType == TEXTBOX_ENDTYPE_TWO_CHOICE) || (msgCtx->textboxEndType == TEXTBOX_ENDTYPE_THREE_CHOICE)) { - return CHECK_BTN_ALL(controller->press.button, BTN_A); + shouldAdvance = CHECK_BTN_ALL(controller->press.button, BTN_A); } else { - return CHECK_BTN_ALL(controller->press.button, BTN_A) || CHECK_BTN_ALL(controller->press.button, BTN_B) || - // 2S2H [Enhancement] When fast text is on, we want to check if B is held instead of only if it was just - // pressed - (CVarGetInteger("gEnhancements.Dialogue.FastText", 0) && CHECK_BTN_ALL(controller->cur.button, BTN_B)) || - CHECK_BTN_ALL(controller->press.button, BTN_CUP); + shouldAdvance = + CHECK_BTN_ALL(controller->press.button, BTN_A) || CHECK_BTN_ALL(controller->press.button, BTN_B) || + // 2S2H [Enhancement] When fast text is on, we want to check if B is held instead of only if it was just + // pressed + (CVarGetInteger("gEnhancements.Dialogue.FastText", 0) && CHECK_BTN_ALL(controller->cur.button, BTN_B)) || + CHECK_BTN_ALL(controller->press.button, BTN_CUP); } + + return GameInteractor_Should(VB_MSG_ADVANCE, shouldAdvance); } void Message_CloseTextbox(PlayState* play) { -- cgit v1.2.3