summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Cox <garrettjcox@gmail.com>2026-07-27 16:13:57 -0500
committerGitHub <noreply@github.com>2026-07-27 16:13:57 -0500
commit2c83cc890d70f4f68e1ca1e7bee753905efca467 (patch)
tree6807f4b539320cac588e6523c874588deee01b58
parente3310fe1b327c51a39e40a0f09d12b43bf7a0cb5 (diff)
Option to auto advance text to credits (#1821)
-rw-r--r--mm/2s2h/BenGui/BenMenu.cpp5
-rw-r--r--mm/2s2h/Enhancements/Cutscenes/AutoAdvanceEndingText.cpp42
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h8
-rw-r--r--mm/2s2h/PresetManager/PresetManager.cpp1
-rw-r--r--mm/src/code/z_message.c32
5 files changed, 76 insertions, 12 deletions
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 <libultraship/bridge/consolevariablebridge.h>
+#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
@@ -1422,6 +1422,14 @@ typedef enum {
// #### `result`
// ```c
+ // CHECK_BTN_ALL(controller->press.button, BTN_A) || ...
+ // ```
+ // #### `args`
+ // - None
+ VB_MSG_ADVANCE,
+
+ // #### `result`
+ // ```c
// false
// ```
// #### `args`
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) {