From 640539f649ba4abf3b0855ea510e0f26cf729a72 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 20:38:27 -0500 Subject: [PATCH 01/90] port more ppcutil macros added ori, cmpwi, subi, rlwinm, subfic --- src/utils/ppcutil.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/ppcutil.h b/src/utils/ppcutil.h index 32d7f896..3f21be6b 100644 --- a/src/utils/ppcutil.h +++ b/src/utils/ppcutil.h @@ -15,10 +15,24 @@ (0x38000000 + (((u32)(dest_register)) << 21) + ((u16)value)) #define PPC_INSTR_LIS(dest_register, value) \ (0x3C000000 + (((u32)(dest_register)) << 21) + ((u16)value)) +#define PPC_INSTR_ORI(dest_register, value) \ + ((0b011000 << 26) + (((u32)(dest_register)) << 21) + (((u32)(dest_register)) << 16) + \ + ((u16)value)) #define PPC_INSTR_NOP() (0x60000000) -// TODO: PPC_INSR_CMPWI +#define PPC_INSTR_CMPWI(rA, imm) (0x2C000000u + ((u32)(rA) << 16) + (u16)(imm)) + +#define PPC_INSTR_SUBI(dest_register, src_register, value) \ + (0x38000000u + ((u32)(dest_register) << 21) + ((u32)(src_register) << 16) + \ + (u16)(-(s16)(value))) + +#define PPC_INSTR_RLWINM(dest_register, src_register, shift_amt, mask_begin, mask_end, rc) \ + ((0b010101 << 26) + ((u32)src_register << 21) + ((u32)dest_register << 16) + \ + ((u32)shift_amt << 11) + ((u32)mask_begin << 6) + ((u32)mask_end << 1) + ((u32)rc)) + +#define PPC_INSTR_SUBFIC(dest_register, src_register, simm) \ + ((0b001000 << 26) + ((u32)dest_register << 21) + ((u32)src_register << 16) + (int16_t)simm) #define PPC_R0 0 #define PPC_R1 1 From 851b6b59e2df9f841f983fcc113aeabd0d102999 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 22:51:15 -0500 Subject: [PATCH 02/90] import timerdisp updated cmseg, iw, and timer to use draw_timer_right_side, which is a special case of the more general draw_timer function --- src/mods/cmseg.cpp | 2 +- src/mods/iw.cpp | 2 +- src/mods/timer.cpp | 4 +- src/utils/timerdisp.cpp | 108 ++++++++++++++++++++++++++++++++++++---- src/utils/timerdisp.h | 36 +++++++++++++- 5 files changed, 136 insertions(+), 16 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index 9b1e88fc..95697458 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -365,7 +365,7 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - timerdisp::draw_timer(static_cast(s_seg_time), "SEG:", 0, color, false); + timerdisp::draw_timer_right_side(static_cast(s_seg_time), "SEG:", 0, color, false); } } diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index feaf2589..123f10ab 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -124,7 +124,7 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - timerdisp::draw_timer(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); + timerdisp::draw_timer_right_side(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); } } // namespace iw diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index a7c475a5..ece362c4 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -59,11 +59,11 @@ void disp() { u32 row = 1; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - timerdisp::draw_timer(s_rta_timer, "RTA:", row++, draw::WHITE, true); + timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - timerdisp::draw_timer(s_pause_timer, "PAU:", row++, draw::WHITE, true); + timerdisp::draw_timer_right_side(s_pause_timer, "PAU:", row++, draw::WHITE, true); } switch (mkb::sub_mode) { diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 16afc4f1..b07750f3 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -1,6 +1,6 @@ #include "timerdisp.h" -#include "utils/draw.h" +#include "../utils/draw.h" namespace timerdisp { @@ -11,7 +11,17 @@ static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; static constexpr s32 X = 378; static constexpr s32 Y = 24; -void draw_timer(s32 frames, const char *prefix, u32 row, mkb::GXColor color, bool show_seconds) { +s32 row_number_to_vertical_pos(u32 row_num) { + return Y + 16 * row_num; +} + +void draw_timer(u32 pos_x, + u32 row, + u32 text_offset, + const char *prefix, + s32 frames, + bool show_seconds, + mkb::GXColor color) { bool positive = frames >= 0; if (!positive) frames = -frames; const char *sign = positive ? "" : "-"; @@ -21,24 +31,35 @@ void draw_timer(s32 frames, const char *prefix, u32 row, mkb::GXColor color, boo u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; u32 centiseconds = (frames % SECOND_FRAMES) * 100 / 60; - s32 y = Y + row * 16; + s32 A = pos_x; + s32 a = A + text_offset; + s32 b = row_number_to_vertical_pos(row); if (hours > 0 && !show_seconds) { - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, centiseconds); } else if (minutes > 0 && !show_seconds) { - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%02d:%02d.%02d", sign, minutes, seconds, - centiseconds); + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%02d:%02d.%02d", sign, minutes, seconds, centiseconds); } else { u32 total_seconds = seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%02d.%02d", sign, total_seconds, centiseconds); + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%02d.%02d", sign, total_seconds, centiseconds); } } +// Special case of the above function with a hardcoded x-pos and text offset that covers a lot of +// use cases, used for the RTA/pause timer, challenge mode segment timer, and IW timer +void draw_timer_right_side(s32 frames, + const char *prefix, + u32 row, + mkb::GXColor color, + bool show_seconds) { + draw_timer(X, row, 4 * draw::DEBUG_CHAR_WIDTH, prefix, frames, show_seconds, color); +} + void draw_subtick_timer(s32 frames, const char *prefix, u32 row, @@ -73,4 +94,71 @@ void draw_percentage(s32 fsave, const char *prefix, u32 row, mkb::GXColor color) draw::debug_text(X + 48, y, color, "%2d%", fsave); } +TimeComp format_time(u32 time) { // input time in frames + u32 time_hours = time / HOUR_FRAMES; + u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; + u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; + u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; + return {time_hours, time_minutes, time_seconds, time_centiseconds}; +} + +void format_time_to_buffer(char *buffer, + u32 time, + TimeFormatType format_type) { // time is in frames + TimeComp formatted_time = format_time(time); + + switch (format_type) { + case TimeFormatType::SECONDS_ONLY: { + u32 seconds = time / SECOND_FRAMES; + u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; + mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); + break; + } + case TimeFormatType::HOURS_ALWAYS: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + break; + } + case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } + break; + } + case TimeFormatType::MINIMAL_LEADING: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + } +} + } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 236f0491..e7e63873 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -1,10 +1,39 @@ #pragma once -#include "mkb/mkb.h" +#include "../mkb/mkb.h" namespace timerdisp { -void draw_timer(s32 frames, const char *prefix, u32 row, mkb::GXColor color, bool show_minutes); +// TimeComp = "Time Components" +struct TimeComp { + u32 hours; + u32 minutes; + u32 seconds; + u32 centiseconds; +}; + +enum class TimeFormatType { + SECONDS_ONLY, // ss.cc + HOURS_ALWAYS, // h:mm:ss.cc or 0:mm:ss.cc + MINUTES_ALWAYS_LEADING_ZERO, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise + MINIMAL_LEADING, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, + // h:mm:ss.cc otherwise + ALWAYS_LEAD_NON_HOURS // mm:ss.cc if < 1 hour, ss.cc if < 1 min +}; + +s32 row_number_to_vertical_pos(u32 row_num); +void draw_timer(u32 pos_x, + u32 row, + u32 text_offset, + const char *prefix, + s32 frames, + bool show_seconds, + mkb::GXColor color); +void draw_timer_right_side(s32 frames, + const char *prefix, + u32 row, + mkb::GXColor color, + bool show_seconds); void draw_subtick_timer(s32 frames, const char *prefix, u32 row, @@ -14,4 +43,7 @@ void draw_subtick_timer(s32 frames, bool extra_precision); void draw_percentage(s32 fsave, const char *prefix, u32 row, mkb::GXColor color); +TimeComp format_time(u32 time); +void format_time_to_buffer(char *buffer, u32 time, TimeFormatType format_type); + } // namespace timerdisp From c8ffb389cc8fa5d9d79290192a8a12b4d7859ffd Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 22:53:29 -0500 Subject: [PATCH 03/90] add getter to gotostory --- src/mods/gotostory.cpp | 8 ++------ src/mods/gotostory.h | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/mods/gotostory.cpp b/src/mods/gotostory.cpp index e00b6cc6..d6d42d36 100644 --- a/src/mods/gotostory.cpp +++ b/src/mods/gotostory.cpp @@ -4,14 +4,10 @@ namespace gotostory { -enum class State { - Default, - LoadMenuReq, - LoadStoryReq, -}; - static State s_state = State::Default; +State get_gotostory_state() { return s_state; } + void load_storymode() { if (mkb::main_mode == mkb::MD_SEL) { s_state = State::LoadStoryReq; diff --git a/src/mods/gotostory.h b/src/mods/gotostory.h index 40e8f579..c3ffea98 100644 --- a/src/mods/gotostory.h +++ b/src/mods/gotostory.h @@ -2,7 +2,14 @@ namespace gotostory { +enum class State { + Default, + LoadMenuReq, + LoadStoryReq, +}; + void tick(); void load_storymode(); +State get_gotostory_state(); } // namespace gotostory \ No newline at end of file From 272978f719af902b66e63d3e54a3fcf078d4df40 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:15:58 -0500 Subject: [PATCH 04/90] import new prefs --- src/systems/pref.cpp | 9 +++++++++ src/systems/pref.h | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/systems/pref.cpp b/src/systems/pref.cpp index 2890a34f..a79d109e 100644 --- a/src/systems/pref.cpp +++ b/src/systems/pref.cpp @@ -99,6 +99,13 @@ static const Pref PREF_IDS[] = { Pref::InputDispGradientStart, Pref::InputDispGradientEnd, Pref::RgbFormat, + Pref::FullgameTimerOptions, + Pref::SegmentTimerOptions, + Pref::ShowRunBreakdown, + Pref::HideRunResetMessage, + Pref::DeathCounterDisplayOptions, + Pref::CountFirstStageDeaths, + Pref::MenuAcceleration, }; struct DefaultPref { @@ -131,6 +138,8 @@ static const DefaultPref DEFAULT_PREFS[] = { {Pref::CustomPhysicsDisp, 1}, {Pref::SavestateClearAllBind, 255}, {Pref::InputDispGradientEnd, 100}, + {Pref::HideRunResetMessage, 1}, + {Pref::CountFirstStageDeaths, 1}, }; // diff --git a/src/systems/pref.h b/src/systems/pref.h index 9566ad28..326b017d 100644 --- a/src/systems/pref.h +++ b/src/systems/pref.h @@ -103,6 +103,13 @@ enum class Pref : u8 { InputDispGradientStart = 90, InputDispGradientEnd = 91, RgbFormat = 92, + FullgameTimerOptions = 93, + SegmentTimerOptions = 94, + ShowRunBreakdown = 95, + HideRunResetMessage = 96, + DeathCounterDisplayOptions = 97, + CountFirstStageDeaths = 98, + MenuAcceleration = 99, }; void init(); From e3d3588f42df9192248f42c5e00950e29da2c319 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:16:11 -0500 Subject: [PATCH 05/90] import new menus --- src/systems/menu_defn.cpp | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index 7e5d5fe1..71fd76f8 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1003,6 +1003,88 @@ static Widget s_cm_seg_widgets[] = { }, }; +// put my stuff here + +static const char *FULLGAME_TIMER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", + "End of run", +}; + +static const char *SEGMENT_TIMER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", +}; + +static Widget s_loadless_timers_widgets[] = { + { + .type = WidgetType::Choose, + .choose = + { + .label = "Fullgame Timer", + .choices = FULLGAME_TIMER_OPTIONS, + .num_choices = LEN(FULLGAME_TIMER_OPTIONS), + .pref = pref::Pref::FullgameTimerOptions, + }, + }, + { + .type = WidgetType::Choose, + .choose = + { + .label = "Segment Timer", + .choices = SEGMENT_TIMER_OPTIONS, + .num_choices = LEN(SEGMENT_TIMER_OPTIONS), + .pref = pref::Pref::SegmentTimerOptions, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Show Run Breakdown", + .pref = pref::Pref::ShowRunBreakdown, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Hide Reset Message", + .pref = pref::Pref::HideRunResetMessage, + }, + }, +}; + +static const char *DEATH_COUNTER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", + "End of run", +}; + +static Widget s_deathcounter_widgets[] = { + { + .type = WidgetType::Choose, + .choose = + { + .label = "Death Counter", + .choices = DEATH_COUNTER_OPTIONS, + .num_choices = LEN(DEATH_COUNTER_OPTIONS), + .pref = pref::Pref::DeathCounterDisplayOptions, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Count Stage 1 Deaths", + .pref = pref::Pref::CountFirstStageDeaths, + }, + }, +}; + static Widget s_timers_widgets[] = { {.type = WidgetType::Header, .header = {"Realtime Timers"}}, { @@ -1040,7 +1122,7 @@ static Widget s_timers_widgets[] = { }, }, {.type = WidgetType::Separator}, - {.type = WidgetType::Header, .header = {"Segment Timers"}}, + {.type = WidgetType::Header, .header = {"Segment & Loadless Timers"}}, { .type = WidgetType::Checkbox, .checkbox = @@ -1057,6 +1139,10 @@ static Widget s_timers_widgets[] = { .pref = pref::Pref::CmTimer, }, }, + { + .type = WidgetType::Menu, + .menu = {"Loadless Timers", s_loadless_timers_widgets, LEN(s_loadless_timers_widgets)}, + }, }; static Widget s_sound_widgets[] = { @@ -1368,6 +1454,8 @@ static Widget s_savestate_widgets[] = { }, }; +static const char *MENU_ACCEL_OPTIONS[] = {"Disabled", "Always enabled", "Only in story"}; + static Widget s_tools_widgets[] = { { .type = WidgetType::Button, @@ -1426,6 +1514,16 @@ static Widget s_tools_widgets[] = { .pref = pref::Pref::DebugMode, }, }, + { + .type = WidgetType::Choose, + .choose = + { + .label = "Menu Acceleration", + .choices = MENU_ACCEL_OPTIONS, + .num_choices = LEN(MENU_ACCEL_OPTIONS), + .pref = pref::Pref::MenuAcceleration, + }, + }, }; static Widget s_reset_ilmark_widgets[] = { @@ -1453,6 +1551,8 @@ static Widget s_reset_ilmark_widgets[] = { }, }; +const char *IL_MARK_STORY_OPTIONS[] = {"Off", "Always", "Run End"}; + static Widget s_il_mark_widgets[] = { { .type = WidgetType::Header, @@ -1476,10 +1576,12 @@ static Widget s_il_mark_widgets[] = { }, }, { - .type = WidgetType::Checkbox, - .checkbox = + .type = WidgetType::Choose, + .choose = { .label = "Story Mode", + .choices = IL_MARK_STORY_OPTIONS, + .num_choices = LEN(IL_MARK_STORY_OPTIONS), .pref = pref::Pref::IlMarkStory, }, }, @@ -1519,6 +1621,10 @@ static Widget s_displays_widgets[] = { .type = WidgetType::Menu, .menu = {"Timers", s_timers_widgets, LEN(s_timers_widgets)}, }, + { + .type = WidgetType::Menu, + .menu = {"Death Counter", s_deathcounter_widgets, LEN(s_deathcounter_widgets)}, + }, { .type = WidgetType::Menu, .menu = {"IL Battle", s_il_battle_widgets, LEN(s_il_battle_widgets)}, From c1d22ec9652851d601006ad967a3bb80d3c97f91 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:19:55 -0500 Subject: [PATCH 06/90] import mode.cpp/h --- src/utils/mode.cpp | 303 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/mode.h | 78 ++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 src/utils/mode.cpp create mode 100644 src/utils/mode.h diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp new file mode 100644 index 00000000..7e442a66 --- /dev/null +++ b/src/utils/mode.cpp @@ -0,0 +1,303 @@ +#include "mode.h" + +// #include "../internal/relutil.h" +#include "../mkb/mkb.h" + +namespace mode { + +// This file collects several submode, main game mode, scenario mode, and storymode stage select +// state checks all in one place. Since these kinds of checks are common, this is done with +// the hope of trying to make things more concise/convenient while retaining clarity + +// --- game scenario submodes --- + +bool is_game_scenario_return(mkb::SubMode submode) { + // this submode doesn't seem to be entered on first 10 ball screen spin in + return submode == mkb::SMD_GAME_SCENARIO_RETURN; +} + +bool is_game_scenario_main(mkb::SubMode submode) { + // submode that includes the 10 ball screen, the story mode file select, and name creation + // screens + return (submode == mkb::SMD_GAME_SCENARIO_MAIN); +} + +bool is_game_scenario(mkb::SubMode submode) { + // includes the 1 frame of game scenario return; it's useful to include this in some checks + return (submode == mkb::SMD_GAME_SCENARIO_MAIN || submode == mkb::SMD_GAME_SCENARIO_RETURN); +} + +// --- spin in --- + +bool is_spin_in_first_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_FIRST_INIT; +} + +bool is_spin_in_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_READY_INIT; +} + +bool is_spin_in(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_READY_INIT || + submode == mkb::SMD_GAME_READY_MAIN); // does not include SMD_GAME_FIRST_INIT +} + +bool is_spin_in_with_first_init(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_spin_in(submode)); +} + +// --- gameplay --- + +bool is_gameplay_init(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_INIT); +} + +bool is_gameplay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_INIT || submode == mkb::SMD_GAME_PLAY_MAIN); +} + +bool is_gameplay_main(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_MAIN); +} + +// --- goal submodes --- + +bool is_postgoal_pre_replay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_GOAL_INIT || submode == mkb::SMD_GAME_GOAL_MAIN); +} + +bool is_postgoal_replay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_GOAL_REPLAY_INIT || + submode == mkb::SMD_GAME_GOAL_REPLAY_MAIN); // does not include + // SMD_GAME_SCENARIO_RETURN +} + +bool is_postgoal(mkb::SubMode submode) { + return is_postgoal_pre_replay(submode) || is_postgoal_replay(submode); +} + +bool is_postgoal_with_game_return(mkb::SubMode submode) { + return is_postgoal(submode) || submode == mkb::SMD_GAME_SCENARIO_RETURN; +} + +bool is_goal_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_GOAL_INIT; +} + +// --- death submodes --- + +bool is_fallout(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RINGOUT_INIT || submode == mkb::SMD_GAME_RINGOUT_MAIN); +} + +bool is_timeover(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_TIMEOVER_INIT || submode == mkb::SMD_GAME_TIMEOVER_MAIN); +} + +bool is_story_retry_screen(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RETRY_INIT || submode == mkb::SMD_GAME_RETRY_MAIN); +} + +bool is_death_submode(mkb::SubMode submode) { + return is_fallout(submode) || is_timeover(submode) || is_story_retry_screen(submode); +} + +bool is_death_init(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RINGOUT_INIT || submode == mkb::SMD_GAME_TIMEOVER_INIT || + submode == mkb::SMD_GAME_RETRY_INIT); +} + +bool is_timed_death_submode(mkb::SubMode submode) { + return is_fallout(submode) || is_timeover(submode); +} + +// --- actual gameplay --- + +bool is_on_stage(mkb::SubMode submode) { + return (is_spin_in(submode) || is_gameplay(submode) || is_postgoal(submode) || + is_death_submode(submode)); +} + +bool is_on_stage_with_first_init(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_on_stage(submode)); +} + +bool is_on_stage_with_endpoints(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_on_stage(submode) || + is_game_scenario_return(submode)); +} + +// --- menu submodes --- + +bool is_sel_ngc_init(mkb::SubMode submode) { + return (submode == mkb::SMD_SEL_NGC_INIT); +} + +bool is_sel_ngc_main(mkb::SubMode submode) { + return submode == mkb::SMD_SEL_NGC_MAIN; +} + +bool is_sel_ngc(mkb::SubMode submode) { + return (submode == mkb::SMD_SEL_NGC_INIT || submode == mkb::SMD_SEL_NGC_MAIN); +} + +// --- story exit game submodes --- + +bool is_story_exit_game_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_INTR_SEL_INIT; +} + +bool is_story_exit_game_message(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_INTR_SEL_INIT || submode == mkb::SMD_GAME_INTR_SEL_MAIN); +} + +bool is_story_exit_game_save_prompt(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_SUGG_SAVE_INIT || submode == mkb::SMD_GAME_SUGG_SAVE_MAIN); +} + +bool is_story_exit_game(mkb::SubMode submode) { + return (is_story_exit_game_message(submode) || is_story_exit_game_save_prompt(submode)); +} + +// --- combinations of non-gameplay submodes --- + +bool is_stage_exit_submode(mkb::SubMode submode) { + return (is_game_scenario_main(submode) || is_sel_ngc(submode) || is_story_exit_game(submode)); +} + +bool is_stage_exit_init(mkb::SubMode submode) { + return is_game_scenario_return(submode) || is_sel_ngc_init(submode) || + is_story_exit_game_init(submode); +} + +// --- some story mode stuff --- + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode) { + return main_game_mode == mkb::STORY_MODE; +} + +bool is_story_cutscene(mkb::SubMode submode) { + return (submode == mkb::SMD_AUTHOR_PLAY_INIT || submode == mkb::SMD_AUTHOR_PLAY_MAIN || + submode == mkb::SMD_AUTHOR_PLAY_STORY_INIT || + submode == mkb::SMD_GAME_SCENSCNPLAY_RETURN); +} + +// --- 10 ball screen things using StoryModeStageSelectState --- + +bool is_on_10_ball_spin_in(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_INTRO_SEQUENCE; +} + +bool is_idle_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + return (state == mkb::STAGE_SELECT_UNK3 || // idle init + state == mkb::STAGE_SELECT_IDLE); +} + +bool has_selected_stage_on_10_ball_screen_init(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_UNK5; // selected init +} + +bool has_selected_stage_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_SELECTED; +} + +// --- 10 ball screen related things using ScenInfo --- + +// The point of the next 3 functions is to provide safety checks for being able to access +// mkb::g_storymode_stageselect_state safely at all. We need to be careful not to access +// mkb::g_storymode_stageselect_state when not in a story mode run (we're not allowed to access it +// even on the file and name entry screens, so a main_game_mode check isn't enough) + +bool is_scen_mode_10_ball_screen(mkb::ScenInfo scen_info) { + // Some notes + // (1) Note that this will return false a little after we select + // a stage on the 10 ball screen but before entering a stage (I think when the next stage is + // loaded) + // (2) Also note that this can return true while on a stage if we do any% glitch related + // things, so be careful with that too + // (3) Note that this also returns true after exiting game on + // the 10 ball screen (during the exit game menu) + return (scen_info.mode == mkb::DMD_SCEN_RETURN_INIT || + scen_info.mode == mkb::DMD_SCEN_SEL_FLOOR_INIT || + scen_info.mode == mkb::DMD_SCEN_SEL_FLOOR_MAIN); +} + +bool is_scen_mode_on_stage(mkb::ScenInfo scen_info) { + // Returns true on a stage and the tail end of the 10 ball screen (usually, see exceptions + // explained above) + return (scen_info.mode == mkb::DMD_SCEN_GAME_INIT || scen_info.mode == mkb::DMD_SCEN_GAME_MAIN); +} + +// The next function gets rid of many of the exceptions described above by adding an additional +// submode check +bool is_on_10_ball_screen(mkb::SubMode sub_mode, mkb::ScenInfo scen_info) { + return (is_game_scenario(sub_mode) && + (is_scen_mode_10_ball_screen(scen_info) || is_scen_mode_on_stage(scen_info))); +} + +int get_story_stage_id_from_scen_info(mkb::ScenInfo scen_info) { + // useful if we want to get the stage id for the stage we're currently hovering over + // on the 10 ball screen + return mkb::get_story_mode_stage_id(scen_info.world, scen_info.world_stage); +} + +// --- story file select/name entry screens --- + +bool is_storymode_file_screen_init(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_INIT); +} + +bool is_storymode_file_screen_main(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_MAIN); +} + +bool is_storymode_name_entry_init(mkb::ScenInfo scen_info) { + // For some reason causes crashes when you check this during the file screen init + return scen_info.mode == mkb::DMD_SCEN_ENTRY_INIT; +} + +bool is_storymode_name_entry_screen_main(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_ENTRY_MAIN); +} + +// --- titlescreen related things --- + +bool is_titlescreen_main(mkb::SubMode submode) { + return (submode == mkb::SMD_ADV_TITLE_MAIN); +} + +// --- misc utility functions --- + +bool is_paused() { + // pre relocate_addr version: *reinterpret_cast(0x805BC474) & 8 + // note that checking mkb::g_some_other_flags == mkb::OF_GAME_PAUSED won't work, I think + // g_some_other_flags is a bitfield? + bool paused_now = (mkb::g_some_other_flags & mkb::OF_GAME_PAUSED); + return paused_now; +} + +// general utility function to get the theme id of a stage from its stage id +u16 theme_id_from_stage_id(int stage_id) { + return static_cast(mkb::get_stage_world_theme(stage_id)); +} + +// --- Story Mode Status Functions --- + +// Get the clear count of the current world we're on. Some notes: +// (1) mkb::get_world_unbeaten_stage_count() only increments after the game scenario return +// submode finishes +// (2) After completing a world, this function returns a value of 10 until the 10 ball screen +// of the next world (at which point it reverts to 0) +u16 get_clear_count_for_world() { + return mkb::get_world_unbeaten_stage_count(mkb::scen_info.world); +} + +u16 get_storymode_total_clear_count() { + u16 total = 0; + for (u16 k = 0; k < WORLD_COUNT; k++) { + total += mkb::get_world_unbeaten_stage_count(k); + } + return total; +} + +} // namespace mode diff --git a/src/utils/mode.h b/src/utils/mode.h new file mode 100644 index 00000000..c6ac5a60 --- /dev/null +++ b/src/utils/mode.h @@ -0,0 +1,78 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace mode { + +constexpr u16 WORLD_COUNT = 10; +constexpr u16 STAGES_PER_WORLD = 10; + +// submodes that are on the stage + +bool is_spin_in_first_init(mkb::SubMode submode); +bool is_spin_in_init(mkb::SubMode submode); +bool is_spin_in(mkb::SubMode submode); + +bool is_gameplay_init(mkb::SubMode submode); +bool is_gameplay_main(mkb::SubMode submode); +bool is_gameplay(mkb::SubMode submode); + +bool is_goal_init(mkb::SubMode submode); +bool is_postgoal(mkb::SubMode submode); +bool is_postgoal_replay(mkb::SubMode submode); +bool is_postgoal_with_game_return(mkb::SubMode submode); + +bool is_story_retry_screen(mkb::SubMode submode); +bool is_death_init(mkb::SubMode submode); +bool is_death_submode(mkb::SubMode submode); +bool is_timed_death_submode(mkb::SubMode submode); + +bool is_on_stage(mkb::SubMode submode); +bool is_on_stage_with_endpoints(mkb::SubMode submode); + +// other submodes + +bool is_sel_ngc_init(mkb::SubMode submode); +bool is_sel_ngc_main(mkb::SubMode submode); +bool is_sel_ngc(mkb::SubMode submode); +bool is_game_scenario_main(mkb::SubMode submode); +bool is_game_scenario_return(mkb::SubMode submode); +bool is_titlescreen_main(mkb::SubMode submode); +bool is_story_exit_game_init(mkb::SubMode submode); +bool is_story_exit_game(mkb::SubMode submode); +bool is_stage_exit_init(mkb::SubMode submode); +bool is_stage_exit_submode(mkb::SubMode submode); + +// 10 ball screen stuff + +bool is_on_10_ball_spin_in(mkb::StoryModeStageSelectState state); +bool is_idle_on_10_ball_screen(mkb::StoryModeStageSelectState state); +bool has_selected_stage_on_10_ball_screen_init(mkb::StoryModeStageSelectState state); +bool has_selected_stage_on_10_ball_screen(mkb::StoryModeStageSelectState state); + +bool is_on_10_ball_screen(mkb::SubMode sub_mode, mkb::ScenInfo scen_info); + +// Other storymode stuff + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode); + +bool is_storymode_file_screen_init(mkb::ScenInfo scen_info); +bool is_storymode_file_screen_main(mkb::ScenInfo scen_info); +bool is_storymode_name_entry_screen_main(mkb::ScenInfo scen_info); + +bool is_story_cutscene(mkb::SubMode submode); + +// story mode status + +u16 get_clear_count_for_world(); +u16 get_storymode_total_clear_count(); +// bool is_between_worlds(bool has_entered_goal); +// bool is_run_complete(bool has_entered_goal); + +// misc utility + +u16 theme_id_from_stage_id(int stage_id); +int get_story_stage_id_from_scen_info(mkb::ScenInfo scen_info); +bool is_paused(); + +} // namespace mode From 36976d8c75e659278b6e6dcbfa7055433de0dbc7 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:42:43 -0500 Subject: [PATCH 07/90] added last_action_was_blank() functions to savest --- src/systems/savest.cpp | 8 ++++++++ src/systems/savest.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index df382d60..2f3daf56 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -396,4 +396,12 @@ Action get_last_action() { return s_last_action; } +bool last_action_was_load() { + return s_last_action == Action::Load; +} + +bool last_action_was_save() { + return s_last_action == Action::Save; +} + } // namespace savest diff --git a/src/systems/savest.h b/src/systems/savest.h index f8516335..1e2b50ee 100644 --- a/src/systems/savest.h +++ b/src/systems/savest.h @@ -47,5 +47,7 @@ u32 get_timestamp(u32 slot); bool is_enabled(); Action get_last_action(); +bool last_action_was_load(); +bool last_action_was_save(); } // namespace savest From 5d21d4312fd2a4bd0d623e95e1bd4d7a4fa630b4 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:43:57 -0500 Subject: [PATCH 08/90] imported and updated goal.cpp/h updated hooks to new system, updated the libesavest call haven't tested yet, will do after importing other things --- src/systems/goal.cpp | 214 +++++++++++++++++++++++++++++++++++++++++++ src/systems/goal.h | 19 ++++ src/systems/main.cpp | 4 +- 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/systems/goal.cpp create mode 100644 src/systems/goal.h diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp new file mode 100644 index 00000000..6f9ceab7 --- /dev/null +++ b/src/systems/goal.cpp @@ -0,0 +1,214 @@ +#include "goal.h" + +#include "../mkb/mkb.h" +#include "../systems/savest.h" +#include "../utils/mode.h" +#include "../utils/patch.h" + +namespace goal { + +// Utility file that provides some functions that deal with and are based off of giving precise goal +// checks (ie the moment we break the tape and not delayed like the goal submodes are) +// This grew out of validate.cpp and was separated into its own file to avoid conflicting interests +// Namely, this file should be very low in the include chain to allow as many other files to use it + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; +constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; +constexpr u8 TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE = 3; +static u8 s_frames_until_goal_submode = 0; + +// Various goal flags that get set to true on goal tape break (and stay true during postgoal), but +// get unset at different times. Having different flags actually does have utility for us (see the +// is_between_worlds() function below and the associated notes) +static bool s_goal_flag_game_return; // Unset after game scenario return +static bool s_goal_flag_exit_game; // Unset outside of exit game and game scenario return +static bool s_goal_flag_last_stage; // Unset on spin in, gameplay pre tape break, and game scenario + // main + +// static patch::Tramp s_goal_tramp; +// static patch::Tramp s_mode_tick_tramp; + +u8 get_frames_until_goal_submode() { + return s_frames_until_goal_submode; +} +bool get_goal_flag_game_return() { + return s_goal_flag_game_return; +} +bool get_goal_flag_exit_game() { + return s_goal_flag_exit_game; +} +bool get_goal_flag_last_stage() { + return s_goal_flag_last_stage; +} + +// Important note on run order +// The run order of various relevant functions for us here is as follows: +// (1) mkb::process_inputs() (what prac mod tick() functions hook) +// (2) mkb::mode_tick() (changes the submode) +// (3) mkb::did_ball_enter_goal() +// (4) mkb::get_world_unbeaten_stage_count() (used to determine clear count, and if we're between +// worlds/at the end of a story mode run. Increments after game scenario return) +// (5) mkb::draw_debugtext (what prac mod disp() functions hook) + +void set_goal_flags() { + s_goal_flag_game_return = true; + s_goal_flag_exit_game = true; + s_goal_flag_last_stage = true; +} + +// Remedy for goal submode checks being delayed from tape break +bool is_postgoal_exact() { + return (s_frames_until_goal_submode != 0 && mode::is_gameplay_main(mkb::sub_mode)) || + mode::is_postgoal(mkb::sub_mode); +} + +bool is_gameplay_exact() { + return mode::is_gameplay(mkb::sub_mode) && !is_postgoal_exact(); +} + +// When entering the goal, set our flags to true and start the frames until goal submode timer +static patch::Tramp s_goal_tramp( + [](mkb::Ball *ball, int *out_stage_goal_idx, int *out_itemgroup_id, mkb::byte *out_goal_flags) { + // mkb::BOOL32 orig_result = s_did_ball_fallout_tramp.chain(ball); + bool result = + s_goal_tramp.chain(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); + + if (result) { + set_goal_flags(); + s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; + } + // We don't need to check if we're paused because this function doesn't + // get run while paused + if (s_frames_until_goal_submode != 0) { + s_frames_until_goal_submode -= 1; + } + + return result; + }); + +// When entering the goal, set our flags to true and start the frames until goal submode timer +/* bool did_ball_enter_goal_hook(mkb::Ball *ball, + int *out_stage_goal_idx, + int *out_itemgroup_id, + mkb::byte *out_goal_flags) { + bool result = s_goal_tramp.dest(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); + + if (result) { + set_goal_flags(); + s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; + } + // We don't need to check if we're paused because this function doesn't + // get run while paused + if (s_frames_until_goal_submode != 0) { + s_frames_until_goal_submode -= 1; + } + return result; +} */ + +// We also need to handle properly zeroing the timer and unsetting our flags in the appropriate +// submodes + +void reset_tape_break_counter() { + // Handle cases where we load state or pause (and either leave the stage or retry) immediately + // after breaking the tape + // This gets run after mkb::mode_tick ie after the game uppdates the submode + // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, + // which means that at the end of the frame that we load the state, s_frames_until_goal_submode + // will be 0 + // libsavest::state_loaded_this_frame() + // TODO: test that updating this didn't break anything! + if (mode::is_stage_exit_init(mkb::sub_mode) || savest::last_action_was_load() || + mode::is_spin_in_init(mkb::sub_mode)) { + s_frames_until_goal_submode = 0; + } +} + +void unset_goal_flags() { + if (!is_postgoal_exact()) { + if (!mode::is_game_scenario_return(mkb::sub_mode)) { + s_goal_flag_game_return = false; + } + if (!(mode::is_game_scenario_return(mkb::sub_mode) || + mode::is_story_exit_game(mkb::sub_mode))) { + s_goal_flag_exit_game = false; + } + if (mode::is_spin_in_init(mkb::sub_mode) || is_gameplay_exact() || + mode::is_game_scenario_main(mkb::sub_mode)) { + s_goal_flag_last_stage = false; + } + } +} + +static patch::Tramp s_mode_tick_tramp([]() { + s_mode_tick_tramp.chain(); + // Run this after the game updates the submode + // This ensures that our flags that should get unset during a submode switch don't get delayed + // by a frame + reset_tape_break_counter(); + unset_goal_flags(); +}); + +/* void mode_tick_hook() { + s_mode_tick_tramp.dest(); + // Run this after the game updates the submode + // This ensures that our flags that should get unset during a submode switch don't get delayed + // by a frame + reset_tape_break_counter(); + unset_goal_flags(); +} */ + +void init() { + // patch::hook_function(s_goal_tramp, &mkb::did_ball_enter_goal, did_ball_enter_goal_hook); + // patch::hook_function(s_mode_tick_tramp, mkb::mode_tick, mode_tick_hook); + s_goal_tramp.hook(); + s_mode_tick_tramp.hook(); +} + +// --- story mode status functions based on goal checks (is_between_worlds(), is_run_complete()) --- + +// Important note: mode::get_clear_count_for_world() is based off +// mkb::get_world_unbeaten_stage_count(), which only increments after game scenario return ends +// This is why it's important to make sure our flags aren't being unset 1 frame late when the +// submode switches to game scenario main (this ensures our timer/deathcounter won't flash/disappear +// for a frame when the "Between Worlds" pref is enabled). Due to the run order notes earlier, when +// the submode switches to game scenario main, first our flag gets updated, then the clear count +// increments, and then disp() functions run + +// To allow for the possibility of passing in different goal flags, we phrase the next function +// using a generic bool argument +bool is_between_worlds_main(bool goal_flag) { + u16 world_clear_count = mode::get_clear_count_for_world(); + if ((world_clear_count == STAGES_PER_WORLD - 1) && goal_flag) { + return true; + } else if (world_clear_count == STAGES_PER_WORLD) { + return true; + } + return false; +} + +// The reason we use different flags depending on if we're on the last world or not is for the +// following behavior: +// (1) If we exit game after completing the last stage in W10 and then return to the menu, we want +// our run to still be flagged as complete (so that we know we should reset the run instead of +// running the timer, due to how we handle resetting/not resetting the run if we fully exit game) +// (2) We can't use s_goal_flag_last_stage for every world, however, because of the possibility of +// doing an accidental (full) exit game all the way back out to the menus on the last stage of a +// (non world 10) world. If this happens, the game doesn't count the stage as cleared and you would +// need to reclear it. In this case, the correct behavior would be to continue running the segment +// timer on the menus and not consider us to be between worlds. Using different goal flags solves +// this edge case behavior + +bool is_between_worlds() { + u8 curr_world = mkb::scen_info.world; + if (curr_world == WORLD_COUNT - 1) { + return is_between_worlds_main(s_goal_flag_last_stage); + } else { + return is_between_worlds_main(s_goal_flag_exit_game); + } +} + +bool is_run_complete() { + return mkb::scen_info.world == WORLD_COUNT - 1 && is_between_worlds(); +} + +} // namespace goal \ No newline at end of file diff --git a/src/systems/goal.h b/src/systems/goal.h new file mode 100644 index 00000000..223cedce --- /dev/null +++ b/src/systems/goal.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace goal { + +u8 get_frames_until_goal_submode(); +bool get_goal_flag_game_return(); +bool get_goal_flag_exit_game(); +bool get_goal_flag_last_stage(); + +bool is_postgoal_exact(); +bool is_gameplay_exact(); +bool is_between_worlds(); +bool is_run_complete(); + +void init(); + +} // namespace goal diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 444756cb..502ea6ef 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -4,17 +4,18 @@ #include "systems/assembly.h" #include "systems/binds.h" #include "systems/cardio.h" +#include "systems/goal.h" #include "systems/heap.h" #include "systems/menu_defn.h" #include "systems/menu_impl.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/savest.h" #include "systems/version.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" #include "utils/relutil.h" -#include "systems/savest.h" #include "mods/ballcolor.h" #include "mods/banans.h" @@ -185,6 +186,7 @@ void init() { physics::init(); iw::init(); savest::init(); + goal::init(); timer::init(); inputdisp::init(); cmseg::init(); From a65bec6b03831103fc663516a19a9c5f6ec2ae1d Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:49:28 -0500 Subject: [PATCH 09/90] import storyreset.cpp/h --- src/mods/storyreset.cpp | 186 ++++++++++++++++++++++++++++++++++++++++ src/mods/storyreset.h | 33 +++++++ 2 files changed, 219 insertions(+) create mode 100644 src/mods/storyreset.cpp create mode 100644 src/mods/storyreset.h diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp new file mode 100644 index 00000000..94e4520a --- /dev/null +++ b/src/mods/storyreset.cpp @@ -0,0 +1,186 @@ +#include "storyreset.h" + +#include "../systems/goal.h" +#include "../systems/pref.h" +#include "../utils/draw.h" +#include "../utils/mode.h" +#include "gotostory.h" + +namespace storyreset { + +// This file is used by both storytimer.cpp and deathcounter.cpp to determine when to reset the run +// since we want to allow the possibility for accidental exit games taking us back to the menus but +// still have the run be considered "active" + +static bool s_is_run_active = false; +static u8 s_active_save_file_idx = 0; // for handling resetting +static u8 s_last_active_world = 0; // for handling resetting + +bool is_run_active() { + return s_is_run_active; +} +u8 get_active_file_index() { + return s_active_save_file_idx; +} +u8 get_active_world() { + return s_last_active_world; +} + +void set_run_active_status(bool is_active) { + // setter that is called in storytimer.cpp so that deathcounter.cpp can know whether the run is + // active or not without having access to the stuff in storytimer.cpp (checking for loadless + // time being nonzero works to see if a run is active, but checking for deaths being nonzero + // doesn't work) + s_is_run_active = is_active; +} + +void reset_active_run_info() { + s_active_save_file_idx = 0; + s_last_active_world = 0; +} + +// This only gets called during the exit game init +// We store the world we were on + what file we were on so that we're able to properly handle +// resetting on the file screen +void record_run_status() { + s_last_active_world = mkb::scen_info.world; + s_active_save_file_idx = mkb::scen_info.save_file_idx; +} + +// --- checks for if we should reset --- + +bool detect_selecting_wrong_file() { + return mkb::data_select_menu_state == mkb::DSMS_OPEN_DATA && + mkb::selected_story_file_idx != s_active_save_file_idx; +} + +// Catches using the IW picker on our current active file +bool detect_wrong_world_on_active_file() { + mkb::StoryModeSaveFile active_file = mkb::storymode_save_files[s_active_save_file_idx]; + bool wrong_world = active_file.current_world != s_last_active_world; + bool active_file_empty = active_file.playtime_in_frames == 0; + return wrong_world || active_file_empty; +} + +bool should_reset_on_file_screen() { + if (mode::is_main_game_mode_story(mkb::main_game_mode) && + mode::is_storymode_file_screen_main(mkb::scen_info)) { + return detect_wrong_world_on_active_file() || detect_selecting_wrong_file(); + } + return false; +} + +// If we select challenge mode, practice mode, or go back to the main menu +bool is_on_wrong_menu() { + if (mode::is_sel_ngc_main(mkb::sub_mode)) { + mkb::MenuScreenID menu = mkb::g_currently_visible_menu_screen; + return (menu == mkb::MENUSCREEN_NUMBER_OF_PLAYERS || + menu == mkb::MENUSCREEN_CHARACTER_SELECT_1 || menu == mkb::MENUSCREEN_MODE_SELECT); + } + return false; +} + +bool used_go_to_story() { + return gotostory::get_gotostory_state() != gotostory::State::Default; +} + +bool should_reset_completed_run() { + if (mode::is_sel_ngc(mkb::sub_mode) || mode::is_titlescreen_main(mkb::sub_mode)) { + return goal::is_run_complete(); + } + return false; +} + +bool should_reset_run() { + return should_reset_on_file_screen() || is_on_wrong_menu() || used_go_to_story() || + should_reset_completed_run(); +} + +// TODO: reset run if selecting active file and active file has 0 stages cleared? + +void tick() { + if (mode::is_main_game_mode_story(mkb::main_game_mode) && + mode::is_story_exit_game_init(mkb::sub_mode)) { + record_run_status(); + } +} + +bool all_loadless_timer_prefs_off() { + StoryDisplayOptions fullgame_pref = + StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); + StoryDisplayOptions segment_pref = + StoryDisplayOptions(pref::get(pref::Pref::SegmentTimerOptions)); + bool breakdown_pref = pref::get(pref::Pref::ShowRunBreakdown); + + return fullgame_pref == StoryDisplayOptions::DontShow && + segment_pref == StoryDisplayOptions::DontShow && !breakdown_pref; +} + +bool death_counter_pref_off() { + StoryDisplayOptions death_counter_pref = + StoryDisplayOptions(pref::get(pref::Pref::DeathCounterDisplayOptions)); + return death_counter_pref == StoryDisplayOptions::DontShow; +} + +// During an accidental full exit game to the menu mid-run, the only time a timer/death counter is +// visible is if "always show" is turned on for that display +bool displaying_on_menus_during_accidental_exit_game() { + StoryDisplayOptions fullgame_pref = + StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); + StoryDisplayOptions segment_pref = + StoryDisplayOptions(pref::get(pref::Pref::SegmentTimerOptions)); + StoryDisplayOptions death_counter_pref = + StoryDisplayOptions(pref::get(pref::Pref::DeathCounterDisplayOptions)); + return fullgame_pref == StoryDisplayOptions::AlwaysShow || + segment_pref == StoryDisplayOptions::AlwaysShow || + death_counter_pref == StoryDisplayOptions::AlwaysShow; +} + +bool run_active_but_not_displaying_on_menus() { + return !displaying_on_menus_during_accidental_exit_game() && + !(all_loadless_timer_prefs_off() && death_counter_pref_off()); +} + +bool is_silent_reset_type() { + return should_reset_on_file_screen() || is_on_wrong_menu(); +} + +void display_reset_run_message() { + if (pref::get(pref::Pref::HideRunResetMessage)) { + return; + } + + // If the hide run reset message pref is off, we still want to be "minimal" with displaying it + // So, we only display a reset message if: + // (1) it's a less obvious reset trigger (eg not go to story) + // (2) at least one timer/death counter pref is on (don't bother displaying a reset message if + // everything is turned off) + // (3) the player has "always show" turned on for at least one of the timers/death counter. The + // idea here is if always show is turned on and if the timer gets zero-ed, it's obvious that the + // run was reset and no message needs to be displayed + + if (!is_silent_reset_type()) { + return; + } + + if (all_loadless_timer_prefs_off() && death_counter_pref_off()) { + return; + } + + if (displaying_on_menus_during_accidental_exit_game()) { + return; + } + + draw::notify(draw::WHITE, "Run Was Reset"); +} + +/* if ((!is_silent_reset_type() || !run_active_but_not_displaying_on_menus())) { + return; + } */ + +// && death_counter_pref_off() +/* if (all_loadless_timer_prefs_off()) { + return; +} */ + +} // namespace storyreset \ No newline at end of file diff --git a/src/mods/storyreset.h b/src/mods/storyreset.h new file mode 100644 index 00000000..a73ea431 --- /dev/null +++ b/src/mods/storyreset.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace storyreset { + +// Does it make more sense to move this enum class to pref.h? +// We need this enum class in storyreset.cpp, but we also need it in both deathcounter.cpp and +// storytimer.cpp +enum class StoryDisplayOptions { + DontShow, + AlwaysShow, + BetweenWorlds, + EndOfRun, +}; +// The fullgame loadless timer and death counter share the same display options. The loadless +// segment timer options are a subset of this enum class (everything except an EndOfRun setting), +// and the segment timer options show up in the same order in the menu as the corresponding fullgame +// one, so we don't need to make a separate enum class for it + +bool is_run_active(); +void set_run_active_status(bool is_active); + +u8 get_active_file_index(); +u8 get_active_world(); +void reset_active_run_info(); +bool should_reset_run(); +void display_reset_run_message(); +bool all_loadless_timer_prefs_off(); + +void tick(); + +} // namespace storyreset From 1f46cba0de360ad562cedc7c2812c374626a5433 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 02:59:36 -0500 Subject: [PATCH 10/90] fix unresolved external symbol error with gotostory very fun and wonderful bug --- src/mods/gotostory.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mods/gotostory.cpp b/src/mods/gotostory.cpp index d6d42d36..f1038e26 100644 --- a/src/mods/gotostory.cpp +++ b/src/mods/gotostory.cpp @@ -6,7 +6,9 @@ namespace gotostory { static State s_state = State::Default; -State get_gotostory_state() { return s_state; } +State get_gotostory_state() { + return s_state; +} void load_storymode() { if (mkb::main_mode == mkb::MD_SEL) { From ab6a94b4615d0f200c94d6a757839edac33cbea4 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:09:02 -0500 Subject: [PATCH 11/90] Revert to using History struct in savest i like it --- src/systems/savest.cpp | 40 +++++++++++++++++++++++++++++----------- src/systems/savest.h | 13 ++++++++++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index 2f3daf56..07b27909 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -32,7 +32,7 @@ struct SaveState { u32 s_timestamp; SaveState s_states[SLOT_COUNT]; -Action s_last_action; +History s_history; // // Hooks @@ -50,7 +50,7 @@ static patch::Tramp s_set_minimap_mode_tramp([](mkb::Mini // Prevent sound effects from playing while loading states static patch::Tramp s_soundreq_tramp([](u32 id) { - if (s_last_action != Action::Load) { + if (s_history.curr_frame_action != Action::Load) { s_soundreq_tramp.chain(id); } }); @@ -291,7 +291,7 @@ SaveResult save(u32 slot) { state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; - s_last_action = Action::Save; + s_history.curr_frame_action = Action::Save; return SaveResult::Ok; } @@ -350,7 +350,7 @@ LoadResult load(u32 slot) { } state->timestamp = s_timestamp; - s_last_action = Action::Load; + s_history.curr_frame_action = Action::Load; return LoadResult::Ok; } @@ -377,7 +377,9 @@ u32 get_timestamp(u32 slot) { } void tick() { - s_last_action = Action::None; + // Rotate history + s_history.prev_frame_action = s_history.curr_frame_action; + s_history.curr_frame_action = Action::None; s_timestamp++; @@ -392,16 +394,32 @@ bool is_enabled() { return pref::get(pref::Pref::Savestates) && !pref::get(pref::Pref::Freecam); } -Action get_last_action() { - return s_last_action; +History get_history() { + return s_history; } -bool last_action_was_load() { - return s_last_action == Action::Load; +Action get_curr_frame_action() { + return s_history.curr_frame_action; } -bool last_action_was_save() { - return s_last_action == Action::Save; +bool state_changed_gameplay() { + return s_history.curr_frame_action == Action::Load; +} + +// Note: checking s_history.curr_frame_action == Action::Load is different from +// interacted_with_state() +// With the former, pressing the create state button for 1 frame won't be detected + +bool interacted_with_state() { + // Returns true if we created a state this frame, or if we continue to hold the create state + // button, or if we load state + return s_history.curr_frame_action != Action::None; +} + +bool loaded_state() { + // Won't return true if we create a state, only if we load one + return s_history.curr_frame_action == Action::Load && + s_history.prev_frame_action == Action::None; } } // namespace savest diff --git a/src/systems/savest.h b/src/systems/savest.h index 1e2b50ee..8540faec 100644 --- a/src/systems/savest.h +++ b/src/systems/savest.h @@ -36,6 +36,11 @@ enum class Action { Save, }; +struct History { + Action curr_frame_action; + Action prev_frame_action; +}; + void init(); void tick(); @@ -46,8 +51,10 @@ bool is_empty(u32 slot); u32 get_timestamp(u32 slot); bool is_enabled(); -Action get_last_action(); -bool last_action_was_load(); -bool last_action_was_save(); +History get_history(); +Action get_curr_frame_action(); +bool state_changed_gameplay(); +bool interacted_with_state(); +bool loaded_state(); } // namespace savest From 4928a9e15b38724e507c1edb24892669defb4b12 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:11:25 -0500 Subject: [PATCH 12/90] savest: use mkb::selected_characters instead of mkb::active_monkey_id savestates won't work with gotostory if we haven't manually selected aiai in the menu (but are using the prac mod setting to set our monkey selection) if we use mkb::active_monkey_id --- src/systems/savest.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index 07b27909..91d71c37 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -24,7 +24,7 @@ struct SaveState { u32 flags; u32 timestamp; s32 stage_id; - u32 character; + u8 character; store::Store store; u8 pause_menu_sprite_status; mkb::Sprite pause_menu_sprite; @@ -287,7 +287,8 @@ SaveResult save(u32 slot) { pass_over_regions(&state->store, store::save); handle_pause_menu_save(state); state->stage_id = mkb::current_stage_id; - state->character = mkb::active_monkey_id[mkb::curr_player_idx]; + // using mkb::selected_characters instead of mkb::active_monkey_id works with gotostory + state->character = mkb::selected_characters[mkb::curr_player_idx]; state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; From ffd33997434d381eb99b37acdc68001486bea280 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:12:06 -0500 Subject: [PATCH 13/90] update goal to use the new interacted_with_state() func --- src/systems/goal.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index 6f9ceab7..52c24a23 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -69,11 +69,10 @@ bool is_gameplay_exact() { // When entering the goal, set our flags to true and start the frames until goal submode timer static patch::Tramp s_goal_tramp( [](mkb::Ball *ball, int *out_stage_goal_idx, int *out_itemgroup_id, mkb::byte *out_goal_flags) { - // mkb::BOOL32 orig_result = s_did_ball_fallout_tramp.chain(ball); - bool result = + bool orig_result = s_goal_tramp.chain(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); - if (result) { + if (orig_result) { set_goal_flags(); s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; } @@ -83,7 +82,7 @@ static patch::Tramp s_goal_tramp( s_frames_until_goal_submode -= 1; } - return result; + return orig_result; }); // When entering the goal, set our flags to true and start the frames until goal submode timer @@ -117,7 +116,7 @@ void reset_tape_break_counter() { // will be 0 // libsavest::state_loaded_this_frame() // TODO: test that updating this didn't break anything! - if (mode::is_stage_exit_init(mkb::sub_mode) || savest::last_action_was_load() || + if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { s_frames_until_goal_submode = 0; } From e2dbfbded666d945da0b32bae2211697aaa02d70 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:12:56 -0500 Subject: [PATCH 14/90] import deathcounter.cpp/h --- src/mods/deathcounter.cpp | 168 ++++++++++++++++++++++++++++++++++++++ src/mods/deathcounter.h | 21 +++++ src/systems/main.cpp | 5 ++ 3 files changed, 194 insertions(+) create mode 100644 src/mods/deathcounter.cpp create mode 100644 src/mods/deathcounter.h diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp new file mode 100644 index 00000000..e84bc9a6 --- /dev/null +++ b/src/mods/deathcounter.cpp @@ -0,0 +1,168 @@ +#include "deathcounter.h" + +#include "../mkb/mkb.h" + +#include "../systems/goal.h" +#include "../systems/pref.h" +#include "../systems/savest.h" +#include "../utils/draw.h" +#include "../utils/macro_utils.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/timerdisp.h" // for testing +#include "freecam.h" +#include "storyreset.h" + +namespace deathcounter { + +constexpr u16 COUNTER_DISPLAY_Y_POS = 56; +constexpr u16 COUNTER_DISPLAY_X_POS = 18; +constexpr u16 COUNTER_NUMBER_X_POS = COUNTER_DISPLAY_X_POS + 7 * draw::DEBUG_CHAR_WIDTH; + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; + +static u32 s_world_death_count[WORLD_COUNT] = {}; +// Flag to determine when we should/shouldn't increment the death counter +static bool s_can_incr_death_counter = false; + +u32 get_total_death_count() { + u32 total = 0; + for (u16 k = 0; k < WORLD_COUNT; k++) { + total += s_world_death_count[k]; + } + return total; +} + +u32 get_world_death_count(u16 world_idx) { + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); // clamp for safety + return s_world_death_count[clamped_idx]; +} + +void increment_world_death_counter() { + // Check the pref for count first stage deaths and if we're on the first stage + if (!pref::get(pref::Pref::CountFirstStageDeaths) && + mode::get_storymode_total_clear_count() == 0) { + return; + } + s_world_death_count[mkb::scen_info.world] += 1; // death counter for the current world + s_can_incr_death_counter = false; +} + +void reset_flag() { + s_can_incr_death_counter = false; +} + +void reset_death_counters() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + s_world_death_count[k] = 0; + } + reset_flag(); +} + +// When we're done holding the savestate button/when gameplay resumes +void update_flag_on_state_release() { + // goal::is_gameplay_exact() && !libsavest::state_loaded_this_frame() + if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // PLACEHOLDER + // As soon as we're done holding the load state button (or just any time we're controlling + // the monkey on the stage), we're allowed to die + s_can_incr_death_counter = true; + } +} + +bool should_count_as_normal_death() { + bool retried_without_clearing = + (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); + bool left_stage_without_clearing = + mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; + bool died = mode::is_death_init(mkb::sub_mode); + return retried_without_clearing || left_stage_without_clearing || died; +} + +bool should_count_as_savestate_death() { + // return libsavest::state_loaded_this_frame() && s_can_incr_death_counter; + return savest::loaded_state() && s_can_incr_death_counter; +} + +void count_deaths() { + if (goal::is_postgoal_exact()) { + s_can_incr_death_counter = false; + } + + if (should_count_as_normal_death()) { + increment_world_death_counter(); + } + + if (should_count_as_savestate_death()) { + increment_world_death_counter(); + } +} + +void tick() { + if (storyreset::should_reset_run()) { + reset_death_counters(); + } + + // Whenever entering a new stage, reset our flag + if (mode::is_spin_in_first_init(mkb::sub_mode)) { + reset_flag(); + } + + count_deaths(); + update_flag_on_state_release(); +} + +bool should_display_death_counter() { + u8 pref = pref::get(pref::Pref::DeathCounterDisplayOptions); + + using DeathCounterOptions = storyreset::StoryDisplayOptions; + + switch (DeathCounterOptions(pref)) { + case DeathCounterOptions::AlwaysShow: + return true; + case DeathCounterOptions::BetweenWorlds: + return goal::is_between_worlds(); + case DeathCounterOptions::EndOfRun: + return goal::is_run_complete(); + case DeathCounterOptions::DontShow: + return false; + } + + // Unreachable, silence compiler warning? + return false; +} + +bool should_not_display_counter_at_all() { + if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + // If we're in the menus outside of a story mode run due to an accidental exit game, we + // still want to be able to display the counter if we haven't reset it yet + return !storyreset::is_run_active(); + } + return freecam::should_hide_hud(); +} + +void disp() { + if (should_not_display_counter_at_all()) { + return; + } + + if (should_display_death_counter()) { + draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); + draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", + get_total_death_count()); + } + + /* u8 pos; + if (should_display_death_counter()) { + pos = 2; + } else { + pos = 0; + } + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 1 + pos, 44, + "Dbg:", 60 * storyreset::is_run_active(), true, draw::WHITE); + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 2 + pos, 44, "Sub:", 60 * mkb::sub_mode, true, + draw::WHITE); + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 3 + pos, 44, + "Gol:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); */ +} + +} // namespace deathcounter diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h new file mode 100644 index 00000000..0672b58c --- /dev/null +++ b/src/mods/deathcounter.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace deathcounter { + +/* enum class DeathCounterOptions { + DontShow = 0, + AlwaysShow = 1, + BetweenWorlds = 2, + EndOfRun = 3, +}; */ + +u32 get_total_death_count(); +u32 get_world_death_count(u16 world_idx); +bool should_display_death_counter(); + +void tick(); +void disp(); + +} // namespace deathcounter \ No newline at end of file diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 502ea6ef..2ee636d2 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -21,6 +21,7 @@ #include "mods/banans.h" #include "mods/camera.h" #include "mods/cmseg.h" +#include "mods/deathcounter.h" #include "mods/dpad.h" #include "mods/fallout.h" #include "mods/freecam.h" @@ -37,6 +38,7 @@ #include "mods/scratch.h" #include "mods/sfx.h" #include "mods/stage_edits.h" +#include "mods/storyreset.h" #include "mods/tetris.h" #include "mods/timer.h" #include "mods/unlock.h" @@ -87,6 +89,8 @@ static patch::Tramp s_process_inputs_tramp([]() { cardio::tick(); unlock::tick(); iw::tick(); + storyreset::tick(); + deathcounter::tick(); savest::tick(); savest_ui::tick(); menu_impl::tick(); // anything checking for pref changes should run after menu_impl::tick() @@ -128,6 +132,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); + deathcounter::disp(); Tetris::get_instance().disp(); ilbattle::disp(); cmseg::disp(); From 6bd61812e86f76159dd72d152c3a14cc35f076a9 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:13:26 -0500 Subject: [PATCH 15/90] import storytimer.cpp/h --- src/mods/storytimer.cpp | 362 ++++++++++++++++++++++++++++++++++++++++ src/mods/storytimer.h | 32 ++++ src/systems/main.cpp | 4 + 3 files changed, 398 insertions(+) create mode 100644 src/mods/storytimer.cpp create mode 100644 src/mods/storytimer.h diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp new file mode 100644 index 00000000..b2539c5e --- /dev/null +++ b/src/mods/storytimer.cpp @@ -0,0 +1,362 @@ +#include "storytimer.h" + +#include "../mkb/mkb.h" + +#include "../systems/goal.h" +#include "../systems/pad.h" // for testing +#include "../systems/pref.h" +#include "../systems/savest.h" // for testing +#include "../utils/draw.h" +#include "../utils/macro_utils.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/timerdisp.h" +#include "deathcounter.h" +#include "freecam.h" +#include "storyreset.h" + +namespace storytimer { + +constexpr u16 FULLGAME_TIMER_LOCATION_X = 18 + 24; +constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; +constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; +constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; +constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr u16 STARTING_ROW = 2; + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; +constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; + +static TimerGroup s_timer_group[WORLD_COUNT]; // timer info for each world + +// --- some getters that other files can use (if needed) --- + +TimerGroup get_world_timer_info(u16 world_idx) { + // clamp for safety so we don't access outside the bounds of the array + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); + return s_timer_group[clamped_idx]; +} + +// Used to calculate our split times for the breakdown screen; also gives us a +// convenient way to calculate fullgame loadless time +u32 get_split_timer_for_world(u16 world_idx) { + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); + u32 prev_world_sum = 0; + for (u16 k = 0; k < clamped_idx; k++) { + prev_world_sum += s_timer_group[k].full_world; + } + return prev_world_sum + s_timer_group[clamped_idx].segment; +} + +u32 get_loadless_time() { + return get_split_timer_for_world(WORLD_COUNT - 1); +} + +bool is_run_active() { + return get_loadless_time() != 0 && !goal::is_run_complete(); +} + +// --- main timer logic --- + +void reset_timer() { + if (get_loadless_time() != 0) { + for (u16 k = 0; k < WORLD_COUNT; k++) { + s_timer_group[k] = {}; + } + storyreset::reset_active_run_info(); + storyreset::display_reset_run_message(); + // mkb::OSReport("Reset timer \n"); + } +} + +// Importantly, we don't increment the timer on the 10 ball screen after selecting a stage +// since the time between doing so and entering the next stage can be highly variable +// To properly increment the timer on the 10 ball screen, we run this +// update function inside a hook for mkb::g_handle_storymode_stageselect_state +void update_timers_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { // World (k+1)'s timer + if (mode::is_on_10_ball_spin_in(state) || mode::is_idle_on_10_ball_screen(state) || + mode::has_selected_stage_on_10_ball_screen_init(state)) { + s_timer_group[k].full_world += 1; + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } +} + +// The above function does not get run when the game is paused, so we also need this +void update_timers_while_paused_on_10_ball_screen() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { + if (mode::is_on_10_ball_screen(mkb::sub_mode, mkb::scen_info) && mode::is_paused()) { + s_timer_group[k].full_world += 1; + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } +} + +// mode::is_on_stage_with_endpoints(mkb::sub_mode) +// Increment the timer on every submode on the stage (and exit game screen) +void update_timers_on_stage() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { // World (k+1)'s timer + if (mode::is_on_stage(mkb::sub_mode) || mode::is_story_exit_game(mkb::sub_mode)) { + s_timer_group[k].full_world += 1; + + // Increment the segment timer until tape breek on the last stage of the world + if (!goal::is_between_worlds()) { + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } + } +} + +// for if we fully exit game by accident +void update_timers_on_menus() { + if ((mode::is_sel_ngc(mkb::sub_mode) || mode::is_storymode_file_screen_main(mkb::scen_info)) && + is_run_active()) { + u8 active_world_idx = storyreset::get_active_world(); + s_timer_group[active_world_idx].full_world += 1; + s_timer_group[active_world_idx].segment = s_timer_group[active_world_idx].full_world; + } +} + +// So deathcounter.cpp can know when the run is active +void update_run_active_flag() { + storyreset::set_run_active_status(is_run_active()); +} + +void tick() { + if (storyreset::should_reset_run()) { + reset_timer(); + } + + if (mode::is_main_game_mode_story(mkb::main_game_mode)) { + update_timers_on_stage(); + update_timers_while_paused_on_10_ball_screen(); + } + + update_timers_on_menus(); + update_run_active_flag(); +} + +// --- display stuff --- + +// The run breakdown screen replaces the segment timer at the end of the run +// if the pref for it is on +bool should_display_timer(TimerType type) { + u8 pref; + if (type == TimerType::Fullgame) { + pref = pref::get(pref::Pref::FullgameTimerOptions); + } else { // Segment timer + pref = pref::get(pref::Pref::SegmentTimerOptions); + } + + using TimerOptions = storyreset::StoryDisplayOptions; + + switch (TimerOptions(pref)) { + case TimerOptions::AlwaysShow: + if (type == TimerType::Fullgame) { + return true; + } else if (pref::get(pref::Pref::ShowRunBreakdown)) { + // type is segment timer + show breakdown on + return !goal::is_run_complete(); + } else { // type is segment timer + show breakdown off + return true; + } + case TimerOptions::BetweenWorlds: + if (type == TimerType::Fullgame) { + return goal::is_between_worlds(); + } else if (pref::get(pref::Pref::ShowRunBreakdown)) { + return goal::is_between_worlds() && !goal::is_run_complete(); + } else { + return goal::is_between_worlds(); + } + case TimerOptions::EndOfRun: + if (type == TimerType::Fullgame) { + return goal::is_run_complete(); + } else { + // We don't have an end of run pref for the segment timer (since it doesn't really + // make sense to have one) + return false; + } + case TimerOptions::DontShow: + return false; + } + + // Unreachable + return false; +} + +// Doing this for now until a better display setup is figured out +u16 get_timer_y_pos(TimerType type) { // maybe rename to get_timer_row()? + u16 y_pos = STARTING_ROW; + bool is_displaying_death_counter = deathcounter::should_display_death_counter(); + + if (type == TimerType::Fullgame) { + if (is_displaying_death_counter) { + y_pos++; + } + } else { // Segment timer + if (should_display_timer(TimerType::Fullgame)) { + y_pos++; + } + if (is_displaying_death_counter) { + y_pos++; + } + } + + return y_pos; +} + +// Bundle up the info timerdisp::draw_timer uses into a struct for convenience +struct TimerDisplayInfo { + u16 pos_x; + u16 pos_y; + u16 text_offset; +}; + +TimerDisplayInfo get_timer_display_info(TimerType type) { + if (type == TimerType::Fullgame) { + return {FULLGAME_TIMER_LOCATION_X, get_timer_y_pos(type), FULLGAME_TIMER_TEXT_OFFSET}; + } else { + return {SEGMENT_TIMER_LOCATION_X, get_timer_y_pos(type), SEGMENT_TIMER_TEXT_OFFSET}; + } +} + +u16 get_current_segment_idx() { + // mkb::scen_info.world gets reset to 0 on the file screen, so the only case we ever need to + // worry about is if we fully exit game and are on the file screen with the timer still running + if (mode::is_storymode_file_screen_main(mkb::scen_info) && get_loadless_time() != 0) { + // The world we were on before exit-gaming + return storyreset::get_active_world(); + } else { + // if we're in a run (not on the menus), this is the index of the current world (between 0 + // and 9 inclusive) + return mkb::scen_info.world; + } +} + +void draw_timers() { + TimerDisplayInfo fullgame_info = get_timer_display_info(TimerType::Fullgame); + u32 loadless_time = get_loadless_time(); + + if (should_display_timer(TimerType::Fullgame)) { + timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.pos_y, fullgame_info.text_offset, + "Time:", loadless_time, false, draw::WHITE); + } + + u16 world_idx = + get_current_segment_idx(); // index of the current world (between 0 and 9 inclusive) + TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); + + if (should_display_timer(TimerType::Segment)) { + timerdisp::draw_timer(seg_info.pos_x, seg_info.pos_y, seg_info.text_offset, + "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); + } +} + +// We only use this function for 0 <= row <= 9 +Vec2d get_breakdown_row_position(u16 row) { + u16 starting_row = get_timer_y_pos(TimerType::Segment); + float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); + if (row < WORLD_COUNT - 1) { + return {BREAKDOWN_ROW_LOCATION_X, pos_y}; + } else { // "W10" takes up more space than "Wk" for "1 <= k <= 9" + return {SEGMENT_TIMER_LOCATION_X, pos_y}; + } +} + +void draw_breakdown_screen() { // TODO: totals row? + // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" + char split_buf[WORLD_COUNT][32] = {}; + char seg_buf[WORLD_COUNT][32] = {}; + char row_info_buf[WORLD_COUNT][32] = {}; + + for (u16 idx = 0; idx < WORLD_COUNT; idx++) { + Vec2d pos = get_breakdown_row_position(idx); + u32 world_deaths = deathcounter::get_world_death_count(idx); + + timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), + timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, + timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], + world_deaths); + + draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); + } +} + +bool should_not_display_timer_at_all() { + // The only time we can ever display the timer outside of story mode is + // when we fully exit game and the timer is still running + if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + return !is_run_active(); + } + // If in story, hide the timer if freecamming (if the pref is on) + return freecam::should_hide_hud(); +} + +void disp() { + if (should_not_display_timer_at_all()) { + // return; // testing, remember to uncomment + } + + draw_timers(); + + if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { + draw_breakdown_screen(); + } + + // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); + // u32 new_val = val & 0x0000ffff; + + u16 pos_y = get_timer_y_pos(TimerType::Segment); + + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, + "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, + "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, + "Cur:", 60 * static_cast(savest::get_history().curr_frame_action), + true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, + "Prv:", 60 * static_cast(savest::get_history().prev_frame_action), + true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, + "gp:", 60 * savest::state_changed_gameplay(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, + "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); +} + +// for easier timer testing +// static patch::Tramp s_fade_screen_to_color_tramp; +/* static patch::Tramp + s_g_handle_storymode_stageselect_state_tramp; */ + +static patch::Tramp + s_g_handle_storymode_stageselect_state_tramp([]() { + s_g_handle_storymode_stageselect_state_tramp.chain(); + update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); + }); + +void init_main_game() { + /* patch::hook_function(s_fade_screen_to_color_tramp, mkb::fade_screen_to_color, + [](mkb::uint flags, u32 color, mkb::uint frames) { + // don't run for testing purposes + // s_fade_screen_to_color_tramp.dest(flags, color, frames); + }); */ + /* patch::hook_function(s_g_handle_storymode_stageselect_state_tramp, + mkb::g_handle_storymode_stageselect_state, []() { + s_g_handle_storymode_stageselect_state_tramp.dest(); + update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); + // update_run_status(); + }); */ + s_g_handle_storymode_stageselect_state_tramp.hook(); +} + +} // namespace storytimer \ No newline at end of file diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h new file mode 100644 index 00000000..0de3105c --- /dev/null +++ b/src/mods/storytimer.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace storytimer { + +/* enum class TimerOptions { + DontShow = 0, + AlwaysShow = 1, + BetweenWorlds = 2, + EndOfRun = 3, +}; */ + +enum class TimerType { + Fullgame, + Segment, +}; + +struct TimerGroup { + u32 segment; // the time taken to complete a world up until tape break on the last stage + u32 full_world; // the time taken to complete a world until the fade to white on the last stage +}; + +TimerGroup get_world_timer_info(u16 world_idx); +u32 get_split_timer_for_world(u16 world_idx); +u32 get_loadless_time(); + +void init_main_game(); +void tick(); +void disp(); + +} // namespace storytimer \ No newline at end of file diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 2ee636d2..e895a735 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -39,6 +39,7 @@ #include "mods/sfx.h" #include "mods/stage_edits.h" #include "mods/storyreset.h" +#include "mods/storytimer.h" #include "mods/tetris.h" #include "mods/timer.h" #include "mods/unlock.h" @@ -90,6 +91,7 @@ static patch::Tramp s_process_inputs_tramp([]() { unlock::tick(); iw::tick(); storyreset::tick(); + storytimer::tick(); deathcounter::tick(); savest::tick(); savest_ui::tick(); @@ -132,6 +134,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); + storytimer::disp(); deathcounter::disp(); Tetris::get_instance().disp(); ilbattle::disp(); @@ -167,6 +170,7 @@ static patch::Tramp s_OSLink_tramp([](mkb::OSModuleHeader *rel_buff s_smd_game_ready_init_tramp.hook(); s_smd_game_play_tick_tramp.hook(); jump::patch_minimap(); + storytimer::init_main_game(); } // Sel_ngc init functions // else if (relutil::ModuleId(rel_buffer->info.id) == relutil::ModuleId::SelNgc) { From a89263f0c7c2ac2a9790f54cc2b52c7f01da6d53 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:15:05 -0500 Subject: [PATCH 16/90] import ilmark changes --- src/mods/ilmark.cpp | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index b8e38ed8..6f7e6731 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -3,18 +3,26 @@ #include "mkb/mkb.h" #include "mods/freecam.h" #include "mods/physics.h" +#include "mods/storyreset.h" #include "mods/validate.h" +#include "systems/goal.h" #include "systems/menu_impl.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/savest.h" #include "systems/version.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" -#include "systems/savest.h" namespace ilmark { +enum class StoryIlMarkSettings { + DontShow, + AlwaysInStory, + AtRunEnd, +}; + static bool s_valid_run = false; static bool s_is_romhack = false; @@ -44,23 +52,40 @@ void tick() { } } +bool show_in_story() { + u8 story_pref = pref::get(pref::Pref::IlMarkStory); + switch (StoryIlMarkSettings(story_pref)) { + case StoryIlMarkSettings::DontShow: + return false; + case StoryIlMarkSettings::AtRunEnd: + // show the mark if at least one timer related pref is on and if we've finished the run + return !storyreset::all_loadless_timer_prefs_off() && goal::is_run_complete(); + case StoryIlMarkSettings::AlwaysInStory: + return true; + } + + // Unreachable + return true; +} + bool is_ilmark_enabled() { if (mkb::main_mode != mkb::MD_GAME) return false; + // If in a romhack and the pref is off, don't show any validation marks whatsoever + if (s_is_romhack && !pref::get(pref::Pref::IlMarkRomhacks)) { + return false; + } + if (mkb::main_game_mode == mkb::PRACTICE_MODE) { if (!pref::get(pref::Pref::IlMarkPractice)) return false; } else if (mkb::main_game_mode == mkb::STORY_MODE) { - if (!pref::get(pref::Pref::IlMarkStory)) return false; + if (!show_in_story()) return false; } else if (mkb::main_game_mode == mkb::CHALLENGE_MODE) { if (!pref::get(pref::Pref::IlMarkChallenge)) return false; } else { return false; } - if (s_is_romhack && !pref::get(pref::Pref::IlMarkRomhacks)) { - return false; - } - return true; } From ea3e36dbcf1c07d7c2a788c609c6ec248e7fea11 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:16:08 -0500 Subject: [PATCH 17/90] update ilmark, validate to use the new func in savest --- src/mods/ilmark.cpp | 5 ++++- src/mods/validate.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index 6f7e6731..b528d3f7 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -47,7 +47,10 @@ void tick() { s_valid_run = false; } - if (savest::get_last_action() == savest::Action::Load) { + // savest::get_last_action() == savest::Action::Load + // savest::interacted_with_state() + if (savest::state_changed_gameplay()) { + // why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? s_valid_run = false; } } diff --git a/src/mods/validate.cpp b/src/mods/validate.cpp index e36343d9..80ba5ff0 100644 --- a/src/mods/validate.cpp +++ b/src/mods/validate.cpp @@ -4,10 +4,10 @@ #include "systems/menu_impl.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/savest.h" #include "utils/macro_utils.h" #include "utils/patch.h" #include "utils/relutil.h" -#include "systems/savest.h" namespace validate { @@ -54,7 +54,9 @@ void validate_run() { } // Track savestates - if (savest::get_last_action() == savest::Action::Load) s_loaded_savestate = true; + // savest::get_last_action() == savest::Action::Load + // savest::interacted_with_state() + if (savest::state_changed_gameplay()) s_loaded_savestate = true; // Using dpad controls is disallowed bool dpad_down = From 959b589f7e0bd1b362da1627320dad57021408f2 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:25:53 -0500 Subject: [PATCH 18/90] import menu_accel.cpp/h --- src/mods/menu_accel.cpp | 47 +++++++++++++++++++++++++++++++++++++++++ src/mods/menu_accel.h | 15 +++++++++++++ src/systems/main.cpp | 2 ++ 3 files changed, 64 insertions(+) create mode 100644 src/mods/menu_accel.cpp create mode 100644 src/mods/menu_accel.h diff --git a/src/mods/menu_accel.cpp b/src/mods/menu_accel.cpp new file mode 100644 index 00000000..c1a0a12b --- /dev/null +++ b/src/mods/menu_accel.cpp @@ -0,0 +1,47 @@ +#include "menu_accel.h" + +#include "../systems/pref.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/ppcutil.h" +#include "../utils/relutil.h" + +namespace menu_accel { + +void accelerate_pause_menu() { + // nop the instructions that check for R held down to accelerate the menu + patch::write_nop(relutil::relocate_addr(0x80273478)); // for up inputs + patch::write_nop(relutil::relocate_addr(0x802735C8)); // for down inputs +} + +void apply_vanilla_menu_behavior() { + // Otherwise, recover the original vanilla instructions + patch::write_word(relutil::relocate_addr(0x80273478), + PPC_INSTR_CMPWI(PPC_R3, 0)); // for up inputs + patch::write_word(relutil::relocate_addr(0x802735C8), + PPC_INSTR_RLWINM(PPC_R3, PPC_R3, 0, 0x16, 0x16, 0)); // for down inputs +} + +void tick() { + u8 pref = pref::get(pref::Pref::MenuAcceleration); + switch (MenuAccelOptions(pref)) { + case MenuAccelOptions::AlwaysEnabled: { + accelerate_pause_menu(); + break; + } + case MenuAccelOptions::OnlyInStory: { + if (mode::is_main_game_mode_story(mkb::main_game_mode)) { + accelerate_pause_menu(); + } else { + apply_vanilla_menu_behavior(); + } + break; + } + case MenuAccelOptions::Disabled: { + apply_vanilla_menu_behavior(); + break; + } + } +} + +} // namespace menu_accel \ No newline at end of file diff --git a/src/mods/menu_accel.h b/src/mods/menu_accel.h new file mode 100644 index 00000000..d11d7d62 --- /dev/null +++ b/src/mods/menu_accel.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace menu_accel { + +enum class MenuAccelOptions { + Disabled, + AlwaysEnabled, + OnlyInStory, +}; + +void tick(); + +} // namespace menu_accel diff --git a/src/systems/main.cpp b/src/systems/main.cpp index e895a735..0cee4f24 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -33,6 +33,7 @@ #include "mods/iw.h" #include "mods/jump.h" #include "mods/marathon.h" +#include "mods/menu_accel.h" #include "mods/physics.h" #include "mods/savest_ui.h" #include "mods/scratch.h" @@ -102,6 +103,7 @@ static patch::Tramp s_process_inputs_tramp([]() { inputdisp::tick(); gotostory::tick(); cmseg::tick(); + menu_accel::tick(); banans::tick(); marathon::tick(); ballcolor::tick(); From 5083c468f80e153fb67e18fc4f403d1087953fbb Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 17:00:35 -0500 Subject: [PATCH 19/90] fix edge case bug with deathcounter + some cleanup --- src/mods/deathcounter.cpp | 4 +++- src/mods/storyreset.cpp | 2 +- src/mods/storytimer.cpp | 17 +---------------- src/systems/goal.cpp | 33 --------------------------------- 4 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index e84bc9a6..d827d802 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -74,7 +74,9 @@ bool should_count_as_normal_death() { (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); bool left_stage_without_clearing = mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; - bool died = mode::is_death_init(mkb::sub_mode); + // Need to also check the flag for death init submodes in case we fall out and let the animation + // play out (ie we enter 2 "death init" submodes before resuming gameplay) + bool died = mode::is_death_init(mkb::sub_mode) && s_can_incr_death_counter; return retried_without_clearing || left_stage_without_clearing || died; } diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index 94e4520a..67ca1359 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -149,7 +149,7 @@ void display_reset_run_message() { if (pref::get(pref::Pref::HideRunResetMessage)) { return; } - + // TODO: is all this really necessary? // If the hide run reset message pref is off, we still want to be "minimal" with displaying it // So, we only display a reset message if: // (1) it's a less obvious reset trigger (eg not go to story) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index b2539c5e..bfc91c30 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -295,6 +295,7 @@ bool should_not_display_timer_at_all() { // The only time we can ever display the timer outside of story mode is // when we fully exit game and the timer is still running if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + // return get_loadless_time() == 0; return !is_run_active(); } // If in story, hide the timer if freecamming (if the pref is on) @@ -333,11 +334,6 @@ void disp() { "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); } -// for easier timer testing -// static patch::Tramp s_fade_screen_to_color_tramp; -/* static patch::Tramp - s_g_handle_storymode_stageselect_state_tramp; */ - static patch::Tramp s_g_handle_storymode_stageselect_state_tramp([]() { s_g_handle_storymode_stageselect_state_tramp.chain(); @@ -345,17 +341,6 @@ static patch::Tramp }); void init_main_game() { - /* patch::hook_function(s_fade_screen_to_color_tramp, mkb::fade_screen_to_color, - [](mkb::uint flags, u32 color, mkb::uint frames) { - // don't run for testing purposes - // s_fade_screen_to_color_tramp.dest(flags, color, frames); - }); */ - /* patch::hook_function(s_g_handle_storymode_stageselect_state_tramp, - mkb::g_handle_storymode_stageselect_state, []() { - s_g_handle_storymode_stageselect_state_tramp.dest(); - update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); - // update_run_status(); - }); */ s_g_handle_storymode_stageselect_state_tramp.hook(); } diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index 52c24a23..e7c69c7f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -25,9 +25,6 @@ static bool s_goal_flag_exit_game; // Unset outside of exit game and game sce static bool s_goal_flag_last_stage; // Unset on spin in, gameplay pre tape break, and game scenario // main -// static patch::Tramp s_goal_tramp; -// static patch::Tramp s_mode_tick_tramp; - u8 get_frames_until_goal_submode() { return s_frames_until_goal_submode; } @@ -85,25 +82,6 @@ static patch::Tramp s_goal_tramp( return orig_result; }); -// When entering the goal, set our flags to true and start the frames until goal submode timer -/* bool did_ball_enter_goal_hook(mkb::Ball *ball, - int *out_stage_goal_idx, - int *out_itemgroup_id, - mkb::byte *out_goal_flags) { - bool result = s_goal_tramp.dest(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); - - if (result) { - set_goal_flags(); - s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; - } - // We don't need to check if we're paused because this function doesn't - // get run while paused - if (s_frames_until_goal_submode != 0) { - s_frames_until_goal_submode -= 1; - } - return result; -} */ - // We also need to handle properly zeroing the timer and unsetting our flags in the appropriate // submodes @@ -147,18 +125,7 @@ static patch::Tramp s_mode_tick_tramp([]() { unset_goal_flags(); }); -/* void mode_tick_hook() { - s_mode_tick_tramp.dest(); - // Run this after the game updates the submode - // This ensures that our flags that should get unset during a submode switch don't get delayed - // by a frame - reset_tape_break_counter(); - unset_goal_flags(); -} */ - void init() { - // patch::hook_function(s_goal_tramp, &mkb::did_ball_enter_goal, did_ball_enter_goal_hook); - // patch::hook_function(s_mode_tick_tramp, mkb::mode_tick, mode_tick_hook); s_goal_tramp.hook(); s_mode_tick_tramp.hook(); } From 81243aa6cc27f43272840e2c77fdf788b8cb4121 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 20:40:20 -0500 Subject: [PATCH 20/90] add totals row to breakdown screen also renamed get_timer_y_pos() to get_timer_row() --- src/mods/storytimer.cpp | 71 +++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index bfc91c30..4ff13ecc 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -22,6 +22,7 @@ constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr u16 TOTALS_ROW_LOCATION_X = 18; constexpr u16 STARTING_ROW = 2; constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; @@ -185,51 +186,52 @@ bool should_display_timer(TimerType type) { } case TimerOptions::DontShow: return false; + default: + // Unreachable + return false; } - - // Unreachable - return false; } // Doing this for now until a better display setup is figured out -u16 get_timer_y_pos(TimerType type) { // maybe rename to get_timer_row()? - u16 y_pos = STARTING_ROW; +u16 get_timer_row(TimerType type) { + u16 row = STARTING_ROW; bool is_displaying_death_counter = deathcounter::should_display_death_counter(); if (type == TimerType::Fullgame) { if (is_displaying_death_counter) { - y_pos++; + row++; } } else { // Segment timer if (should_display_timer(TimerType::Fullgame)) { - y_pos++; + row++; } if (is_displaying_death_counter) { - y_pos++; + row++; } } - return y_pos; + return row; } // Bundle up the info timerdisp::draw_timer uses into a struct for convenience struct TimerDisplayInfo { u16 pos_x; - u16 pos_y; + u16 row; u16 text_offset; }; TimerDisplayInfo get_timer_display_info(TimerType type) { if (type == TimerType::Fullgame) { - return {FULLGAME_TIMER_LOCATION_X, get_timer_y_pos(type), FULLGAME_TIMER_TEXT_OFFSET}; + return {FULLGAME_TIMER_LOCATION_X, get_timer_row(type), FULLGAME_TIMER_TEXT_OFFSET}; } else { - return {SEGMENT_TIMER_LOCATION_X, get_timer_y_pos(type), SEGMENT_TIMER_TEXT_OFFSET}; + return {SEGMENT_TIMER_LOCATION_X, get_timer_row(type), SEGMENT_TIMER_TEXT_OFFSET}; } } u16 get_current_segment_idx() { - // mkb::scen_info.world gets reset to 0 on the file screen, so the only case we ever need to - // worry about is if we fully exit game and are on the file screen with the timer still running + // mkb::scen_info.world gets reset to 0 on the file screen, so if we accidentally exit game to + // the menus, the only case where we can't use mkb::scen_info.world is when we are on the file + // screen with the timer still running if (mode::is_storymode_file_screen_main(mkb::scen_info) && get_loadless_time() != 0) { // The world we were on before exit-gaming return storyreset::get_active_world(); @@ -245,32 +247,33 @@ void draw_timers() { u32 loadless_time = get_loadless_time(); if (should_display_timer(TimerType::Fullgame)) { - timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.pos_y, fullgame_info.text_offset, + timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, "Time:", loadless_time, false, draw::WHITE); } - u16 world_idx = - get_current_segment_idx(); // index of the current world (between 0 and 9 inclusive) + u16 world_idx = get_current_segment_idx(); // index of the current world TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); if (should_display_timer(TimerType::Segment)) { - timerdisp::draw_timer(seg_info.pos_x, seg_info.pos_y, seg_info.text_offset, + timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); } } -// We only use this function for 0 <= row <= 9 +// We only use this function for 0 <= row <= 10 Vec2d get_breakdown_row_position(u16 row) { - u16 starting_row = get_timer_y_pos(TimerType::Segment); + u16 starting_row = get_timer_row(TimerType::Segment); float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); if (row < WORLD_COUNT - 1) { return {BREAKDOWN_ROW_LOCATION_X, pos_y}; - } else { // "W10" takes up more space than "Wk" for "1 <= k <= 9" - return {SEGMENT_TIMER_LOCATION_X, pos_y}; + } else if (row == WORLD_COUNT - 1) { // "W10" takes up more space than "Wk" for "1 <= k <= 9" + return {BREAKDOWN_ROW_LOCATION_X - draw::DEBUG_CHAR_WIDTH, pos_y}; + } else { // For the totals row + return {TOTALS_ROW_LOCATION_X, pos_y}; } } -void draw_breakdown_screen() { // TODO: totals row? +void draw_breakdown_screen() { // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" char split_buf[WORLD_COUNT][32] = {}; char seg_buf[WORLD_COUNT][32] = {}; @@ -289,6 +292,14 @@ void draw_breakdown_screen() { // TODO: totals row? draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); } + + // For the totals row + char totals_row_buf[32] = {}; + Vec2d totals_row_pos = get_breakdown_row_position(WORLD_COUNT); + u32 total_deaths = deathcounter::get_total_death_count(); + + mkb::sprintf(totals_row_buf, "Totals:%s (%d)", split_buf[WORLD_COUNT - 1], total_deaths); + draw::debug_text(totals_row_pos.x, totals_row_pos.y, draw::WHITE, "%s", totals_row_buf); } bool should_not_display_timer_at_all() { @@ -304,7 +315,7 @@ bool should_not_display_timer_at_all() { void disp() { if (should_not_display_timer_at_all()) { - // return; // testing, remember to uncomment + return; // testing, remember to uncomment } draw_timers(); @@ -316,22 +327,20 @@ void disp() { // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); // u32 new_val = val & 0x0000ffff; - u16 pos_y = get_timer_y_pos(TimerType::Segment); + /* u16 pos_y = get_timer_row(TimerType::Segment); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, - "Cur:", 60 * static_cast(savest::get_history().curr_frame_action), - true, draw::WHITE); + "Btw:", 60 * goal::is_between_worlds(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, - "Prv:", 60 * static_cast(savest::get_history().prev_frame_action), - true, draw::WHITE); + "Sub:", 60 * mkb::sub_mode, true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, - "gp:", 60 * savest::state_changed_gameplay(), true, draw::WHITE); + "Ext:", 60 * goal::get_goal_flag_exit_game(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, - "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); + "Lst:", 60 * goal::get_goal_flag_last_stage(), true, draw::WHITE); */ } static patch::Tramp From de48a3b345ee7304230291b7583c57580673fe40 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 22:43:00 -0500 Subject: [PATCH 21/90] small cleanup --- src/mods/deathcounter.cpp | 16 ++++++---------- src/mods/ilmark.cpp | 8 ++++---- src/systems/goal.cpp | 5 +++-- src/utils/mode.cpp | 4 ++++ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index d827d802..df0dc91f 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -62,7 +62,7 @@ void reset_death_counters() { // When we're done holding the savestate button/when gameplay resumes void update_flag_on_state_release() { // goal::is_gameplay_exact() && !libsavest::state_loaded_this_frame() - if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // PLACEHOLDER + if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // As soon as we're done holding the load state button (or just any time we're controlling // the monkey on the stage), we're allowed to die s_can_incr_death_counter = true; @@ -71,7 +71,7 @@ void update_flag_on_state_release() { bool should_count_as_normal_death() { bool retried_without_clearing = - (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); + mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter; bool left_stage_without_clearing = mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; // Need to also check the flag for death init submodes in case we fall out and let the animation @@ -90,11 +90,7 @@ void count_deaths() { s_can_incr_death_counter = false; } - if (should_count_as_normal_death()) { - increment_world_death_counter(); - } - - if (should_count_as_savestate_death()) { + if (should_count_as_normal_death() || should_count_as_savestate_death()) { increment_world_death_counter(); } } @@ -127,10 +123,10 @@ bool should_display_death_counter() { return goal::is_run_complete(); case DeathCounterOptions::DontShow: return false; + default: + // Unreachable + return false; } - - // Unreachable, silence compiler warning? - return false; } bool should_not_display_counter_at_all() { diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index b528d3f7..b7fe2d21 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -50,7 +50,7 @@ void tick() { // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() if (savest::state_changed_gameplay()) { - // why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? + // TODO: why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? s_valid_run = false; } } @@ -65,10 +65,10 @@ bool show_in_story() { return !storyreset::all_loadless_timer_prefs_off() && goal::is_run_complete(); case StoryIlMarkSettings::AlwaysInStory: return true; + default: + // Unreachable + return false; } - - // Unreachable - return true; } bool is_ilmark_enabled() { diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index e7c69c7f..ef22ad2f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -90,9 +90,10 @@ void reset_tape_break_counter() { // after breaking the tape // This gets run after mkb::mode_tick ie after the game uppdates the submode // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, - // which means that at the end of the frame that we load the state, s_frames_until_goal_submode + // which means that at the end of any frame where we load a state, s_frames_until_goal_submode // will be 0 - // libsavest::state_loaded_this_frame() + // libsavest::state_loaded_this_frame() // savest::interacted_with_state() + // savest::loaded_state() // TODO: test that updating this didn't break anything! if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp index 7e442a66..7c087b66 100644 --- a/src/utils/mode.cpp +++ b/src/utils/mode.cpp @@ -251,6 +251,10 @@ bool is_storymode_file_screen_main(mkb::ScenInfo scen_info) { return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_MAIN); } +bool is_storymode_file_screen(mkb::ScenInfo scen_info) { + return is_storymode_file_screen_init(scen_info) || is_storymode_file_screen_main(scen_info); +} + bool is_storymode_name_entry_init(mkb::ScenInfo scen_info) { // For some reason causes crashes when you check this during the file screen init return scen_info.mode == mkb::DMD_SCEN_ENTRY_INIT; From f6128ec56a21e53b6f70644c3ba087034117c145 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 00:25:01 -0500 Subject: [PATCH 22/90] update draw_timer to use format_time_to_buffer + other various cleanup in the file --- src/utils/timerdisp.cpp | 164 ++++++++++++++++++---------------------- src/utils/timerdisp.h | 17 ++--- 2 files changed, 81 insertions(+), 100 deletions(-) diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index b07750f3..1eada276 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -11,6 +11,64 @@ static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; static constexpr s32 X = 378; static constexpr s32 Y = 24; +TimeComp get_time_components(u32 frames) { + u32 time_hours = frames / HOUR_FRAMES; + u32 time_minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; + u32 time_seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; + u32 time_centiseconds = (frames % SECOND_FRAMES) * 100 / 60; + return {time_hours, time_minutes, time_seconds, time_centiseconds}; +} + +void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type) { + TimeComp time = get_time_components(frames); + + switch (format_type) { + case TimeFormatType::SecondsOnly: { + u32 total_seconds = frames / SECOND_FRAMES; + mkb::sprintf(buffer, "%d.%02d", total_seconds, time.centiseconds); + break; + } + case TimeFormatType::HoursAlways: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + break; + } + case TimeFormatType::MinutesAlwaysLeadingZero: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d:%02d.%02d", time.minutes, time.seconds, + time.centiseconds); + } + break; + } + case TimeFormatType::MinimalLeading: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else if (time.minutes > 0) { + mkb::sprintf(buffer, "%d:%02d.%02d", time.minutes, time.seconds, time.centiseconds); + } else { + mkb::sprintf(buffer, "%d.%02d", time.seconds, time.centiseconds); + } + break; + } + case TimeFormatType::AlwaysLeadNonHours: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else if (time.minutes > 0) { + mkb::sprintf(buffer, "%02d:%02d.%02d", time.minutes, time.seconds, + time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d.%02d", time.seconds, time.centiseconds); + } + break; + } + } +} + s32 row_number_to_vertical_pos(u32 row_num) { return Y + 16 * row_num; } @@ -26,28 +84,17 @@ void draw_timer(u32 pos_x, if (!positive) frames = -frames; const char *sign = positive ? "" : "-"; - u32 hours = frames / HOUR_FRAMES; - u32 minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; - u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; - u32 centiseconds = (frames % SECOND_FRAMES) * 100 / 60; - - s32 A = pos_x; - s32 a = A + text_offset; - s32 b = row_number_to_vertical_pos(row); - - if (hours > 0 && !show_seconds) { - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, - centiseconds); - } else if (minutes > 0 && !show_seconds) { - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%02d:%02d.%02d", sign, minutes, seconds, centiseconds); + s32 pos_y = row_number_to_vertical_pos(row); + + char buf[16] = {}; + if (!show_seconds) { + format_time_to_buffer(buf, frames, TimeFormatType::AlwaysLeadNonHours); } else { - u32 total_seconds = - seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%02d.%02d", sign, total_seconds, centiseconds); + format_time_to_buffer(buf, frames, TimeFormatType::SecondsOnly); } + + draw::debug_text(pos_x, pos_y, color, prefix); + draw::debug_text(pos_x + text_offset, pos_y, color, "%s%s", sign, buf); } // Special case of the above function with a hardcoded x-pos and text offset that covers a lot of @@ -77,88 +124,23 @@ void draw_subtick_timer(s32 frames, u32 milliseconds = ((frames % SECOND_FRAMES) * 100 + framesave) / 6; // 3 digit u32 extra = (((frames % SECOND_FRAMES) * 100 + framesave) * 10) / 6; // 4 digit - s32 y = Y + row * 16; + s32 y = row_number_to_vertical_pos(row); u32 total_seconds = seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; draw::debug_text(X, y, color, prefix); if (extra_precision) { - draw::debug_text(X + 48, y, color, "%s%02d.%04d", sign, total_seconds, extra); + draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%04d", sign, + total_seconds, extra); } else { - draw::debug_text(X + 48, y, color, "%s%02d.%03d", sign, total_seconds, milliseconds); + draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%03d", sign, + total_seconds, milliseconds); } } void draw_percentage(s32 fsave, const char *prefix, u32 row, mkb::GXColor color) { - s32 y = Y + row * 16; + s32 y = row_number_to_vertical_pos(row); draw::debug_text(X, y, color, prefix); draw::debug_text(X + 48, y, color, "%2d%", fsave); } -TimeComp format_time(u32 time) { // input time in frames - u32 time_hours = time / HOUR_FRAMES; - u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; - u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; - u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; - return {time_hours, time_minutes, time_seconds, time_centiseconds}; -} - -void format_time_to_buffer(char *buffer, - u32 time, - TimeFormatType format_type) { // time is in frames - TimeComp formatted_time = format_time(time); - - switch (format_type) { - case TimeFormatType::SECONDS_ONLY: { - u32 seconds = time / SECOND_FRAMES; - u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; - mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); - break; - } - case TimeFormatType::HOURS_ALWAYS: { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - break; - } - case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } - break; - } - case TimeFormatType::MINIMAL_LEADING: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - } -} - } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index e7e63873..0dfd75ba 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -13,14 +13,16 @@ struct TimeComp { }; enum class TimeFormatType { - SECONDS_ONLY, // ss.cc - HOURS_ALWAYS, // h:mm:ss.cc or 0:mm:ss.cc - MINUTES_ALWAYS_LEADING_ZERO, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise - MINIMAL_LEADING, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, - // h:mm:ss.cc otherwise - ALWAYS_LEAD_NON_HOURS // mm:ss.cc if < 1 hour, ss.cc if < 1 min + SecondsOnly, // ss.cc + HoursAlways, // h:mm:ss.cc or 0:mm:ss.cc + MinutesAlwaysLeadingZero, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise + MinimalLeading, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, + // h:mm:ss.cc otherwise + AlwaysLeadNonHours // mm:ss.cc if < 1 hour, ss.cc if < 1 min }; +void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type); + s32 row_number_to_vertical_pos(u32 row_num); void draw_timer(u32 pos_x, u32 row, @@ -43,7 +45,4 @@ void draw_subtick_timer(s32 frames, bool extra_precision); void draw_percentage(s32 fsave, const char *prefix, u32 row, mkb::GXColor color); -TimeComp format_time(u32 time); -void format_time_to_buffer(char *buffer, u32 time, TimeFormatType format_type); - } // namespace timerdisp From df58f001326540e6f811111af679a91a68b694a3 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 00:27:08 -0500 Subject: [PATCH 23/90] update run order in main storyreset, storytimer, and deathcounter ticks run after menu_impl and savestate related things now --- src/mods/deathcounter.cpp | 1 - src/mods/deathcounter.h | 7 ------- src/mods/storytimer.cpp | 4 ++-- src/mods/storytimer.h | 7 ------- src/systems/goal.cpp | 3 --- src/systems/main.cpp | 8 ++++---- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index df0dc91f..465a584a 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -81,7 +81,6 @@ bool should_count_as_normal_death() { } bool should_count_as_savestate_death() { - // return libsavest::state_loaded_this_frame() && s_can_incr_death_counter; return savest::loaded_state() && s_can_incr_death_counter; } diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h index 0672b58c..a093e863 100644 --- a/src/mods/deathcounter.h +++ b/src/mods/deathcounter.h @@ -4,13 +4,6 @@ namespace deathcounter { -/* enum class DeathCounterOptions { - DontShow = 0, - AlwaysShow = 1, - BetweenWorlds = 2, - EndOfRun = 3, -}; */ - u32 get_total_death_count(); u32 get_world_death_count(u16 world_idx); bool should_display_death_counter(); diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 4ff13ecc..3f07873f 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -284,9 +284,9 @@ void draw_breakdown_screen() { u32 world_deaths = deathcounter::get_world_death_count(idx); timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), - timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, - timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index 0de3105c..dec3a7aa 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -4,13 +4,6 @@ namespace storytimer { -/* enum class TimerOptions { - DontShow = 0, - AlwaysShow = 1, - BetweenWorlds = 2, - EndOfRun = 3, -}; */ - enum class TimerType { Fullgame, Segment, diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index ef22ad2f..a314e30f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -92,9 +92,6 @@ void reset_tape_break_counter() { // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, // which means that at the end of any frame where we load a state, s_frames_until_goal_submode // will be 0 - // libsavest::state_loaded_this_frame() // savest::interacted_with_state() - // savest::loaded_state() - // TODO: test that updating this didn't break anything! if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { s_frames_until_goal_submode = 0; diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 0cee4f24..c58369c1 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -91,12 +91,12 @@ static patch::Tramp s_process_inputs_tramp([]() { cardio::tick(); unlock::tick(); iw::tick(); - storyreset::tick(); - storytimer::tick(); - deathcounter::tick(); savest::tick(); savest_ui::tick(); menu_impl::tick(); // anything checking for pref changes should run after menu_impl::tick() + storyreset::tick(); + deathcounter::tick(); + storytimer::tick(); fallout::tick(); jump::tick(); // (edits physics preset) physics::tick(); // anything editing physics presets must run before physics::tick() @@ -136,8 +136,8 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); - storytimer::disp(); deathcounter::disp(); + storytimer::disp(); Tetris::get_instance().disp(); ilbattle::disp(); cmseg::disp(); From e7b45446d10311bde78eb39fe0f107cae65516fa Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 17:59:51 -0500 Subject: [PATCH 24/90] add hide reset message button to deathcounter menu + small things also rename timer_group -> world_timer and make draw_timer arg signed --- src/mods/storytimer.cpp | 42 +++++++++++++++++++-------------------- src/mods/storytimer.h | 4 ++-- src/systems/menu_defn.cpp | 10 ++++++++++ src/utils/timerdisp.cpp | 2 +- src/utils/timerdisp.h | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 3f07873f..a3490d19 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -17,25 +17,25 @@ namespace storytimer { -constexpr u16 FULLGAME_TIMER_LOCATION_X = 18 + 24; +constexpr s16 FULLGAME_TIMER_LOCATION_X = 18 + 24; constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; -constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; +constexpr s16 SEGMENT_TIMER_LOCATION_X = 30 + 24; constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; -constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; -constexpr u16 TOTALS_ROW_LOCATION_X = 18; +constexpr s16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr s16 TOTALS_ROW_LOCATION_X = 18; constexpr u16 STARTING_ROW = 2; constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; -static TimerGroup s_timer_group[WORLD_COUNT]; // timer info for each world +static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world // --- some getters that other files can use (if needed) --- -TimerGroup get_world_timer_info(u16 world_idx) { +WorldTimer get_world_timer_info(u16 world_idx) { // clamp for safety so we don't access outside the bounds of the array u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); - return s_timer_group[clamped_idx]; + return s_world_timer[clamped_idx]; } // Used to calculate our split times for the breakdown screen; also gives us a @@ -44,9 +44,9 @@ u32 get_split_timer_for_world(u16 world_idx) { u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); u32 prev_world_sum = 0; for (u16 k = 0; k < clamped_idx; k++) { - prev_world_sum += s_timer_group[k].full_world; + prev_world_sum += s_world_timer[k].full_world; } - return prev_world_sum + s_timer_group[clamped_idx].segment; + return prev_world_sum + s_world_timer[clamped_idx].segment; } u32 get_loadless_time() { @@ -62,7 +62,7 @@ bool is_run_active() { void reset_timer() { if (get_loadless_time() != 0) { for (u16 k = 0; k < WORLD_COUNT; k++) { - s_timer_group[k] = {}; + s_world_timer[k] = {}; } storyreset::reset_active_run_info(); storyreset::display_reset_run_message(); @@ -79,8 +79,8 @@ void update_timers_on_10_ball_screen(mkb::StoryModeStageSelectState state) { if (mkb::scen_info.world == k) { // World (k+1)'s timer if (mode::is_on_10_ball_spin_in(state) || mode::is_idle_on_10_ball_screen(state) || mode::has_selected_stage_on_10_ball_screen_init(state)) { - s_timer_group[k].full_world += 1; - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } @@ -91,8 +91,8 @@ void update_timers_while_paused_on_10_ball_screen() { for (u16 k = 0; k < WORLD_COUNT; k++) { if (mkb::scen_info.world == k) { if (mode::is_on_10_ball_screen(mkb::sub_mode, mkb::scen_info) && mode::is_paused()) { - s_timer_group[k].full_world += 1; - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } @@ -104,24 +104,24 @@ void update_timers_on_stage() { for (u16 k = 0; k < WORLD_COUNT; k++) { if (mkb::scen_info.world == k) { // World (k+1)'s timer if (mode::is_on_stage(mkb::sub_mode) || mode::is_story_exit_game(mkb::sub_mode)) { - s_timer_group[k].full_world += 1; + s_world_timer[k].full_world += 1; // Increment the segment timer until tape breek on the last stage of the world if (!goal::is_between_worlds()) { - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } } } -// for if we fully exit game by accident +// For if we fully exit game by accident void update_timers_on_menus() { if ((mode::is_sel_ngc(mkb::sub_mode) || mode::is_storymode_file_screen_main(mkb::scen_info)) && is_run_active()) { u8 active_world_idx = storyreset::get_active_world(); - s_timer_group[active_world_idx].full_world += 1; - s_timer_group[active_world_idx].segment = s_timer_group[active_world_idx].full_world; + s_world_timer[active_world_idx].full_world += 1; + s_world_timer[active_world_idx].segment = s_world_timer[active_world_idx].full_world; } } @@ -256,7 +256,7 @@ void draw_timers() { if (should_display_timer(TimerType::Segment)) { timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, - "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); + "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); } } @@ -285,7 +285,7 @@ void draw_breakdown_screen() { timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); - timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, + timerdisp::format_time_to_buffer(seg_buf[idx], s_world_timer[idx].segment, timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index dec3a7aa..c07413ef 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -9,12 +9,12 @@ enum class TimerType { Segment, }; -struct TimerGroup { +struct WorldTimer { u32 segment; // the time taken to complete a world up until tape break on the last stage u32 full_world; // the time taken to complete a world until the fade to white on the last stage }; -TimerGroup get_world_timer_info(u16 world_idx); +WorldTimer get_world_timer_info(u16 world_idx); u32 get_split_timer_for_world(u16 world_idx); u32 get_loadless_time(); diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index 71fd76f8..a27a4c4c 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1051,6 +1051,7 @@ static Widget s_loadless_timers_widgets[] = { .type = WidgetType::Checkbox, .checkbox = { + // Applies to both loadless timer and deathcounter (they share the same pref) .label = "Hide Reset Message", .pref = pref::Pref::HideRunResetMessage, }, @@ -1083,6 +1084,15 @@ static Widget s_deathcounter_widgets[] = { .pref = pref::Pref::CountFirstStageDeaths, }, }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + // Applies to both loadless timer and deathcounter (they share the same pref) + .label = "Hide Reset Message", + .pref = pref::Pref::HideRunResetMessage, + }, + }, }; static Widget s_timers_widgets[] = { diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 1eada276..6f8e8e5f 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -73,7 +73,7 @@ s32 row_number_to_vertical_pos(u32 row_num) { return Y + 16 * row_num; } -void draw_timer(u32 pos_x, +void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 0dfd75ba..c5af9ecb 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -24,7 +24,7 @@ enum class TimeFormatType { void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type); s32 row_number_to_vertical_pos(u32 row_num); -void draw_timer(u32 pos_x, +void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, From 204c470d631d3fc8efaaeef091b50a974a549df7 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 19:17:51 -0500 Subject: [PATCH 25/90] cleanup storyreset some --- src/mods/storyreset.cpp | 34 +++++++++++----------------------- src/mods/storyreset.h | 11 ++++------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index 67ca1359..d77371a7 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -96,8 +96,6 @@ bool should_reset_run() { should_reset_completed_run(); } -// TODO: reset run if selecting active file and active file has 0 stages cleared? - void tick() { if (mode::is_main_game_mode_story(mkb::main_game_mode) && mode::is_story_exit_game_init(mkb::sub_mode)) { @@ -105,6 +103,8 @@ void tick() { } } +// --- stuff for handling displaying the run reset message --- + bool all_loadless_timer_prefs_off() { StoryDisplayOptions fullgame_pref = StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); @@ -136,12 +136,8 @@ bool displaying_on_menus_during_accidental_exit_game() { death_counter_pref == StoryDisplayOptions::AlwaysShow; } -bool run_active_but_not_displaying_on_menus() { - return !displaying_on_menus_during_accidental_exit_game() && - !(all_loadless_timer_prefs_off() && death_counter_pref_off()); -} - bool is_silent_reset_type() { + // ie, any reset that's not triggered by gotostory or on the menu after completing a run return should_reset_on_file_screen() || is_on_wrong_menu(); } @@ -149,15 +145,16 @@ void display_reset_run_message() { if (pref::get(pref::Pref::HideRunResetMessage)) { return; } - // TODO: is all this really necessary? + // If the hide run reset message pref is off, we still want to be "minimal" with displaying it // So, we only display a reset message if: - // (1) it's a less obvious reset trigger (eg not go to story) - // (2) at least one timer/death counter pref is on (don't bother displaying a reset message if - // everything is turned off) - // (3) the player has "always show" turned on for at least one of the timers/death counter. The - // idea here is if always show is turned on and if the timer gets zero-ed, it's obvious that the - // run was reset and no message needs to be displayed + // (1) it's a less obvious reset trigger (ie not go to story, or if we've already completed the + // run) + // (2) at least one timer/death counter pref is on (don't bother displaying a reset message + // if everything is turned off) + // (3) the player has "always show" turned on for at least one of + // the timers/death counter. The idea here is if always show is turned on and if the timer gets + // zero-ed, it's obvious that the run was reset and no message needs to be displayed if (!is_silent_reset_type()) { return; @@ -174,13 +171,4 @@ void display_reset_run_message() { draw::notify(draw::WHITE, "Run Was Reset"); } -/* if ((!is_silent_reset_type() || !run_active_but_not_displaying_on_menus())) { - return; - } */ - -// && death_counter_pref_off() -/* if (all_loadless_timer_prefs_off()) { - return; -} */ - } // namespace storyreset \ No newline at end of file diff --git a/src/mods/storyreset.h b/src/mods/storyreset.h index a73ea431..9239b48b 100644 --- a/src/mods/storyreset.h +++ b/src/mods/storyreset.h @@ -4,19 +4,16 @@ namespace storyreset { -// Does it make more sense to move this enum class to pref.h? -// We need this enum class in storyreset.cpp, but we also need it in both deathcounter.cpp and -// storytimer.cpp +// The fullgame loadless timer and death counter share the same display options. The loadless +// segment timer options are a subset of this enum class (which consist of everything here except an +// EndOfRun setting), and the segment timer options show up in the same order in the menu as the +// corresponding fullgame one, so we don't need to make a separate enum class for it enum class StoryDisplayOptions { DontShow, AlwaysShow, BetweenWorlds, EndOfRun, }; -// The fullgame loadless timer and death counter share the same display options. The loadless -// segment timer options are a subset of this enum class (everything except an EndOfRun setting), -// and the segment timer options show up in the same order in the menu as the corresponding fullgame -// one, so we don't need to make a separate enum class for it bool is_run_active(); void set_run_active_status(bool is_active); From 5812ff36ad1011479c7ce87ce3fc9b0ddf758224 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 19:18:46 -0500 Subject: [PATCH 26/90] revert to old validate/ilmark behavior on creating a state for 1f --- src/mods/ilmark.cpp | 5 ++--- src/mods/validate.cpp | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index b7fe2d21..a9f5cd6f 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -47,10 +47,9 @@ void tick() { s_valid_run = false; } - // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() - if (savest::state_changed_gameplay()) { - // TODO: why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? + // savest::state_changed_gameplay() + if (savest::interacted_with_state()) { s_valid_run = false; } } diff --git a/src/mods/validate.cpp b/src/mods/validate.cpp index 80ba5ff0..aaa250bb 100644 --- a/src/mods/validate.cpp +++ b/src/mods/validate.cpp @@ -54,9 +54,9 @@ void validate_run() { } // Track savestates - // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() - if (savest::state_changed_gameplay()) s_loaded_savestate = true; + // savest::state_changed_gameplay() + if (savest::interacted_with_state()) s_loaded_savestate = true; // Using dpad controls is disallowed bool dpad_down = From 7b0240e40fdf4526f9441c44ebf247ab0d4c749f Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 18 Aug 2026 00:13:50 -0500 Subject: [PATCH 27/90] make split_buf, seg_buf arrays smaller + remove testing comments --- src/mods/deathcounter.cpp | 14 -------------- src/mods/storytimer.cpp | 26 +++----------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 465a584a..5b2f4a4b 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -9,7 +9,6 @@ #include "../utils/macro_utils.h" #include "../utils/mode.h" #include "../utils/patch.h" -#include "../utils/timerdisp.h" // for testing #include "freecam.h" #include "storyreset.h" @@ -147,19 +146,6 @@ void disp() { draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", get_total_death_count()); } - - /* u8 pos; - if (should_display_death_counter()) { - pos = 2; - } else { - pos = 0; - } - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 1 + pos, 44, - "Dbg:", 60 * storyreset::is_run_active(), true, draw::WHITE); - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 2 + pos, 44, "Sub:", 60 * mkb::sub_mode, true, - draw::WHITE); - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 3 + pos, 44, - "Gol:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); */ } } // namespace deathcounter diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index a3490d19..5e99b9b1 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -3,9 +3,7 @@ #include "../mkb/mkb.h" #include "../systems/goal.h" -#include "../systems/pad.h" // for testing #include "../systems/pref.h" -#include "../systems/savest.h" // for testing #include "../utils/draw.h" #include "../utils/macro_utils.h" #include "../utils/mode.h" @@ -274,9 +272,9 @@ Vec2d get_breakdown_row_position(u16 row) { } void draw_breakdown_screen() { - // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" - char split_buf[WORLD_COUNT][32] = {}; - char seg_buf[WORLD_COUNT][32] = {}; + // Format: "Wk: world k split time (world k segment time) (world k deaths)" + char split_buf[WORLD_COUNT][16] = {}; + char seg_buf[WORLD_COUNT][16] = {}; char row_info_buf[WORLD_COUNT][32] = {}; for (u16 idx = 0; idx < WORLD_COUNT; idx++) { @@ -323,24 +321,6 @@ void disp() { if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { draw_breakdown_screen(); } - - // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); - // u32 new_val = val & 0x0000ffff; - - /* u16 pos_y = get_timer_row(TimerType::Segment); - - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, - "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, - "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, - "Btw:", 60 * goal::is_between_worlds(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, - "Sub:", 60 * mkb::sub_mode, true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, - "Ext:", 60 * goal::get_goal_flag_exit_game(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, - "Lst:", 60 * goal::get_goal_flag_last_stage(), true, draw::WHITE); */ } static patch::Tramp From 3784de514237268069b1060df9a30ca488f4b373 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 20:38:27 -0500 Subject: [PATCH 28/90] port more ppcutil macros added ori, cmpwi, subi, rlwinm, subfic --- src/utils/ppcutil.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/ppcutil.h b/src/utils/ppcutil.h index 32d7f896..3f21be6b 100644 --- a/src/utils/ppcutil.h +++ b/src/utils/ppcutil.h @@ -15,10 +15,24 @@ (0x38000000 + (((u32)(dest_register)) << 21) + ((u16)value)) #define PPC_INSTR_LIS(dest_register, value) \ (0x3C000000 + (((u32)(dest_register)) << 21) + ((u16)value)) +#define PPC_INSTR_ORI(dest_register, value) \ + ((0b011000 << 26) + (((u32)(dest_register)) << 21) + (((u32)(dest_register)) << 16) + \ + ((u16)value)) #define PPC_INSTR_NOP() (0x60000000) -// TODO: PPC_INSR_CMPWI +#define PPC_INSTR_CMPWI(rA, imm) (0x2C000000u + ((u32)(rA) << 16) + (u16)(imm)) + +#define PPC_INSTR_SUBI(dest_register, src_register, value) \ + (0x38000000u + ((u32)(dest_register) << 21) + ((u32)(src_register) << 16) + \ + (u16)(-(s16)(value))) + +#define PPC_INSTR_RLWINM(dest_register, src_register, shift_amt, mask_begin, mask_end, rc) \ + ((0b010101 << 26) + ((u32)src_register << 21) + ((u32)dest_register << 16) + \ + ((u32)shift_amt << 11) + ((u32)mask_begin << 6) + ((u32)mask_end << 1) + ((u32)rc)) + +#define PPC_INSTR_SUBFIC(dest_register, src_register, simm) \ + ((0b001000 << 26) + ((u32)dest_register << 21) + ((u32)src_register << 16) + (int16_t)simm) #define PPC_R0 0 #define PPC_R1 1 From 59c22c104f232510fee6b681b9e97c8f7b805eb5 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 22:51:15 -0500 Subject: [PATCH 29/90] import timerdisp updated cmseg, iw, and timer to use draw_timer_right_side, which is a special case of the more general draw_timer function --- src/mods/cmseg.cpp | 2 +- src/mods/iw.cpp | 2 +- src/mods/timer.cpp | 4 +- src/utils/timerdisp.cpp | 177 +++++++++++++++++++++++++++++++++++++--- src/utils/timerdisp.h | 36 +++++++- 5 files changed, 203 insertions(+), 18 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index 656859cb..cd5b9df5 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -424,7 +424,7 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - timerdisp::draw_timer(static_cast(s_seg_time), "SEG:", 0, color, false); + timerdisp::draw_timer_right_side(static_cast(s_seg_time), "SEG:", 0, color, false); } } diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index feaf2589..123f10ab 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -124,7 +124,7 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - timerdisp::draw_timer(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); + timerdisp::draw_timer_right_side(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); } } // namespace iw diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index a7c475a5..ece362c4 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -59,11 +59,11 @@ void disp() { u32 row = 1; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - timerdisp::draw_timer(s_rta_timer, "RTA:", row++, draw::WHITE, true); + timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - timerdisp::draw_timer(s_pause_timer, "PAU:", row++, draw::WHITE, true); + timerdisp::draw_timer_right_side(s_pause_timer, "PAU:", row++, draw::WHITE, true); } switch (mkb::sub_mode) { diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index f96e1f8e..5ef6658b 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -1,6 +1,6 @@ #include "timerdisp.h" -#include "utils/draw.h" +#include "../utils/draw.h" namespace timerdisp { @@ -11,9 +11,18 @@ static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; static constexpr s32 X = 378; static constexpr s32 Y = 24; -void draw_timer(s32 frames, const char *prefix, u32 row, GXColor color, bool show_seconds) { - mkb::set_ui_widescreen_scale_mtx(320); +s32 row_number_to_vertical_pos(u32 row_num) { + return Y + 16 * row_num; +} +void draw_timer(u32 pos_x, + u32 row, + u32 text_offset, + const char *prefix, + s32 frames, + bool show_seconds, + GXColor color) { + mkb::set_ui_widescreen_scale_mtx(320); bool positive = frames >= 0; if (!positive) frames = -frames; const char *sign = positive ? "" : "-"; @@ -23,26 +32,36 @@ void draw_timer(s32 frames, const char *prefix, u32 row, GXColor color, bool sho u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; u32 centiseconds = (frames % SECOND_FRAMES) * 100 / 60; - s32 y = Y + row * 16; + s32 A = pos_x; + s32 a = A + text_offset; + s32 b = row_number_to_vertical_pos(row); if (hours > 0 && !show_seconds) { - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, centiseconds); } else if (minutes > 0 && !show_seconds) { - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%02d:%02d.%02d", sign, minutes, seconds, - centiseconds); + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%02d:%02d.%02d", sign, minutes, seconds, centiseconds); } else { u32 total_seconds = seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%s%02d.%02d", sign, total_seconds, centiseconds); + draw::debug_text(A, b, color, prefix); + draw::debug_text(a, b, color, "%s%02d.%02d", sign, total_seconds, centiseconds); } - mkb::reset_ui_widescreen_scale_mtx(); } +// Special case of the above function with a hardcoded x-pos and text offset that covers a lot of +// use cases, used for the RTA/pause timer, challenge mode segment timer, and IW timer +void draw_timer_right_side(s32 frames, + const char *prefix, + u32 row, + mkb::GXColor color, + bool show_seconds) { + draw_timer(X, row, 4 * draw::DEBUG_CHAR_WIDTH, prefix, frames, show_seconds, color); +} + void draw_subtick_timer(s32 frames, const char *prefix, u32 row, @@ -85,4 +104,138 @@ void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { mkb::set_ui_widescreen_scale_mtx(0); } +TimeComp format_time(u32 time) { // input time in frames + u32 time_hours = time / HOUR_FRAMES; + u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; + u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; + u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; + return {time_hours, time_minutes, time_seconds, time_centiseconds}; +} + +void format_time_to_buffer(char *buffer, + u32 time, + TimeFormatType format_type) { // time is in frames + TimeComp formatted_time = format_time(time); + + switch (format_type) { + case TimeFormatType::SECONDS_ONLY: { + u32 seconds = time / SECOND_FRAMES; + u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; + mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); + break; + } + case TimeFormatType::HOURS_ALWAYS: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + break; + } + case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } + break; + } + case TimeFormatType::MINIMAL_LEADING: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + } +} + +TimeComp format_time(u32 time) { // input time in frames + u32 time_hours = time / HOUR_FRAMES; + u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; + u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; + u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; + return {time_hours, time_minutes, time_seconds, time_centiseconds}; +} + +void format_time_to_buffer(char *buffer, + u32 time, + TimeFormatType format_type) { // time is in frames + TimeComp formatted_time = format_time(time); + + switch (format_type) { + case TimeFormatType::SECONDS_ONLY: { + u32 seconds = time / SECOND_FRAMES; + u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; + mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); + break; + } + case TimeFormatType::HOURS_ALWAYS: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + break; + } + case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } + break; + } + case TimeFormatType::MINIMAL_LEADING: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { + if (formatted_time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, + formatted_time.minutes, formatted_time.seconds, + formatted_time.centiseconds); + } else if (formatted_time.minutes > 0) { + mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, + formatted_time.seconds, formatted_time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, + formatted_time.centiseconds); + } + break; + } + } +} + } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 9f03968e..7a885f4b 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -1,10 +1,39 @@ #pragma once -#include "mkb/mkb.h" +#include "../mkb/mkb.h" namespace timerdisp { -void draw_timer(s32 frames, const char *prefix, u32 row, GXColor color, bool show_minutes); +// TimeComp = "Time Components" +struct TimeComp { + u32 hours; + u32 minutes; + u32 seconds; + u32 centiseconds; +}; + +enum class TimeFormatType { + SECONDS_ONLY, // ss.cc + HOURS_ALWAYS, // h:mm:ss.cc or 0:mm:ss.cc + MINUTES_ALWAYS_LEADING_ZERO, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise + MINIMAL_LEADING, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, + // h:mm:ss.cc otherwise + ALWAYS_LEAD_NON_HOURS // mm:ss.cc if < 1 hour, ss.cc if < 1 min +}; + +s32 row_number_to_vertical_pos(u32 row_num); +void draw_timer(u32 pos_x, + u32 row, + u32 text_offset, + const char *prefix, + s32 frames, + bool show_seconds, + GXColor color); +void draw_timer_right_side(s32 frames, + const char *prefix, + u32 row, + GXColor color, + bool show_seconds); void draw_subtick_timer(s32 frames, const char *prefix, u32 row, @@ -14,4 +43,7 @@ void draw_subtick_timer(s32 frames, bool extra_precision); void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color); +TimeComp format_time(u32 time); +void format_time_to_buffer(char *buffer, u32 time, TimeFormatType format_type); + } // namespace timerdisp From 1960881db5ec2ae2bdf975a992ba21f2c72e4f16 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 22:53:29 -0500 Subject: [PATCH 30/90] add getter to gotostory --- src/mods/gotostory.cpp | 8 ++------ src/mods/gotostory.h | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/mods/gotostory.cpp b/src/mods/gotostory.cpp index e00b6cc6..d6d42d36 100644 --- a/src/mods/gotostory.cpp +++ b/src/mods/gotostory.cpp @@ -4,14 +4,10 @@ namespace gotostory { -enum class State { - Default, - LoadMenuReq, - LoadStoryReq, -}; - static State s_state = State::Default; +State get_gotostory_state() { return s_state; } + void load_storymode() { if (mkb::main_mode == mkb::MD_SEL) { s_state = State::LoadStoryReq; diff --git a/src/mods/gotostory.h b/src/mods/gotostory.h index 40e8f579..c3ffea98 100644 --- a/src/mods/gotostory.h +++ b/src/mods/gotostory.h @@ -2,7 +2,14 @@ namespace gotostory { +enum class State { + Default, + LoadMenuReq, + LoadStoryReq, +}; + void tick(); void load_storymode(); +State get_gotostory_state(); } // namespace gotostory \ No newline at end of file From 61aa9bd1ccc97d3e582125d93697da221f18b283 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:15:58 -0500 Subject: [PATCH 31/90] import new prefs --- src/systems/pref.cpp | 9 +++++++++ src/systems/pref.h | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/systems/pref.cpp b/src/systems/pref.cpp index 2890a34f..a79d109e 100644 --- a/src/systems/pref.cpp +++ b/src/systems/pref.cpp @@ -99,6 +99,13 @@ static const Pref PREF_IDS[] = { Pref::InputDispGradientStart, Pref::InputDispGradientEnd, Pref::RgbFormat, + Pref::FullgameTimerOptions, + Pref::SegmentTimerOptions, + Pref::ShowRunBreakdown, + Pref::HideRunResetMessage, + Pref::DeathCounterDisplayOptions, + Pref::CountFirstStageDeaths, + Pref::MenuAcceleration, }; struct DefaultPref { @@ -131,6 +138,8 @@ static const DefaultPref DEFAULT_PREFS[] = { {Pref::CustomPhysicsDisp, 1}, {Pref::SavestateClearAllBind, 255}, {Pref::InputDispGradientEnd, 100}, + {Pref::HideRunResetMessage, 1}, + {Pref::CountFirstStageDeaths, 1}, }; // diff --git a/src/systems/pref.h b/src/systems/pref.h index 9566ad28..326b017d 100644 --- a/src/systems/pref.h +++ b/src/systems/pref.h @@ -103,6 +103,13 @@ enum class Pref : u8 { InputDispGradientStart = 90, InputDispGradientEnd = 91, RgbFormat = 92, + FullgameTimerOptions = 93, + SegmentTimerOptions = 94, + ShowRunBreakdown = 95, + HideRunResetMessage = 96, + DeathCounterDisplayOptions = 97, + CountFirstStageDeaths = 98, + MenuAcceleration = 99, }; void init(); From 20de4e8dec4620f9fe841234de7feef37c91f62f Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:16:11 -0500 Subject: [PATCH 32/90] import new menus --- src/systems/menu_defn.cpp | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index 5ca94c84..ce7cc4d7 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1024,6 +1024,88 @@ static Widget s_cm_seg_widgets[] = { }, }; +// put my stuff here + +static const char *FULLGAME_TIMER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", + "End of run", +}; + +static const char *SEGMENT_TIMER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", +}; + +static Widget s_loadless_timers_widgets[] = { + { + .type = WidgetType::Choose, + .choose = + { + .label = "Fullgame Timer", + .choices = FULLGAME_TIMER_OPTIONS, + .num_choices = LEN(FULLGAME_TIMER_OPTIONS), + .pref = pref::Pref::FullgameTimerOptions, + }, + }, + { + .type = WidgetType::Choose, + .choose = + { + .label = "Segment Timer", + .choices = SEGMENT_TIMER_OPTIONS, + .num_choices = LEN(SEGMENT_TIMER_OPTIONS), + .pref = pref::Pref::SegmentTimerOptions, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Show Run Breakdown", + .pref = pref::Pref::ShowRunBreakdown, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Hide Reset Message", + .pref = pref::Pref::HideRunResetMessage, + }, + }, +}; + +static const char *DEATH_COUNTER_OPTIONS[] = { + "Don't show", + "Always show", + "Between worlds", + "End of run", +}; + +static Widget s_deathcounter_widgets[] = { + { + .type = WidgetType::Choose, + .choose = + { + .label = "Death Counter", + .choices = DEATH_COUNTER_OPTIONS, + .num_choices = LEN(DEATH_COUNTER_OPTIONS), + .pref = pref::Pref::DeathCounterDisplayOptions, + }, + }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + .label = "Count Stage 1 Deaths", + .pref = pref::Pref::CountFirstStageDeaths, + }, + }, +}; + static Widget s_timers_widgets[] = { {.type = WidgetType::Header, .header = {"Realtime Timers"}}, { @@ -1061,7 +1143,7 @@ static Widget s_timers_widgets[] = { }, }, {.type = WidgetType::Separator}, - {.type = WidgetType::Header, .header = {"Segment Timers"}}, + {.type = WidgetType::Header, .header = {"Segment & Loadless Timers"}}, { .type = WidgetType::Checkbox, .checkbox = @@ -1078,6 +1160,10 @@ static Widget s_timers_widgets[] = { .pref = pref::Pref::CmTimer, }, }, + { + .type = WidgetType::Menu, + .menu = {"Loadless Timers", s_loadless_timers_widgets, LEN(s_loadless_timers_widgets)}, + }, }; static Widget s_sound_widgets[] = { @@ -1389,6 +1475,8 @@ static Widget s_savestate_widgets[] = { }, }; +static const char *MENU_ACCEL_OPTIONS[] = {"Disabled", "Always enabled", "Only in story"}; + static Widget s_tools_widgets[] = { { .type = WidgetType::Button, @@ -1447,6 +1535,16 @@ static Widget s_tools_widgets[] = { .pref = pref::Pref::DebugMode, }, }, + { + .type = WidgetType::Choose, + .choose = + { + .label = "Menu Acceleration", + .choices = MENU_ACCEL_OPTIONS, + .num_choices = LEN(MENU_ACCEL_OPTIONS), + .pref = pref::Pref::MenuAcceleration, + }, + }, }; static Widget s_reset_ilmark_widgets[] = { @@ -1474,6 +1572,8 @@ static Widget s_reset_ilmark_widgets[] = { }, }; +const char *IL_MARK_STORY_OPTIONS[] = {"Off", "Always", "Run End"}; + static Widget s_il_mark_widgets[] = { { .type = WidgetType::Header, @@ -1497,10 +1597,12 @@ static Widget s_il_mark_widgets[] = { }, }, { - .type = WidgetType::Checkbox, - .checkbox = + .type = WidgetType::Choose, + .choose = { .label = "Story Mode", + .choices = IL_MARK_STORY_OPTIONS, + .num_choices = LEN(IL_MARK_STORY_OPTIONS), .pref = pref::Pref::IlMarkStory, }, }, @@ -1540,6 +1642,10 @@ static Widget s_displays_widgets[] = { .type = WidgetType::Menu, .menu = {"Timers", s_timers_widgets, LEN(s_timers_widgets)}, }, + { + .type = WidgetType::Menu, + .menu = {"Death Counter", s_deathcounter_widgets, LEN(s_deathcounter_widgets)}, + }, { .type = WidgetType::Menu, .menu = {"IL Battle", s_il_battle_widgets, LEN(s_il_battle_widgets)}, From fb20cf12b09ab2d5b3730735a82833c355184c51 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:19:55 -0500 Subject: [PATCH 33/90] import mode.cpp/h --- src/utils/mode.cpp | 303 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/mode.h | 78 ++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 src/utils/mode.cpp create mode 100644 src/utils/mode.h diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp new file mode 100644 index 00000000..7e442a66 --- /dev/null +++ b/src/utils/mode.cpp @@ -0,0 +1,303 @@ +#include "mode.h" + +// #include "../internal/relutil.h" +#include "../mkb/mkb.h" + +namespace mode { + +// This file collects several submode, main game mode, scenario mode, and storymode stage select +// state checks all in one place. Since these kinds of checks are common, this is done with +// the hope of trying to make things more concise/convenient while retaining clarity + +// --- game scenario submodes --- + +bool is_game_scenario_return(mkb::SubMode submode) { + // this submode doesn't seem to be entered on first 10 ball screen spin in + return submode == mkb::SMD_GAME_SCENARIO_RETURN; +} + +bool is_game_scenario_main(mkb::SubMode submode) { + // submode that includes the 10 ball screen, the story mode file select, and name creation + // screens + return (submode == mkb::SMD_GAME_SCENARIO_MAIN); +} + +bool is_game_scenario(mkb::SubMode submode) { + // includes the 1 frame of game scenario return; it's useful to include this in some checks + return (submode == mkb::SMD_GAME_SCENARIO_MAIN || submode == mkb::SMD_GAME_SCENARIO_RETURN); +} + +// --- spin in --- + +bool is_spin_in_first_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_FIRST_INIT; +} + +bool is_spin_in_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_READY_INIT; +} + +bool is_spin_in(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_READY_INIT || + submode == mkb::SMD_GAME_READY_MAIN); // does not include SMD_GAME_FIRST_INIT +} + +bool is_spin_in_with_first_init(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_spin_in(submode)); +} + +// --- gameplay --- + +bool is_gameplay_init(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_INIT); +} + +bool is_gameplay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_INIT || submode == mkb::SMD_GAME_PLAY_MAIN); +} + +bool is_gameplay_main(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_PLAY_MAIN); +} + +// --- goal submodes --- + +bool is_postgoal_pre_replay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_GOAL_INIT || submode == mkb::SMD_GAME_GOAL_MAIN); +} + +bool is_postgoal_replay(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_GOAL_REPLAY_INIT || + submode == mkb::SMD_GAME_GOAL_REPLAY_MAIN); // does not include + // SMD_GAME_SCENARIO_RETURN +} + +bool is_postgoal(mkb::SubMode submode) { + return is_postgoal_pre_replay(submode) || is_postgoal_replay(submode); +} + +bool is_postgoal_with_game_return(mkb::SubMode submode) { + return is_postgoal(submode) || submode == mkb::SMD_GAME_SCENARIO_RETURN; +} + +bool is_goal_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_GOAL_INIT; +} + +// --- death submodes --- + +bool is_fallout(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RINGOUT_INIT || submode == mkb::SMD_GAME_RINGOUT_MAIN); +} + +bool is_timeover(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_TIMEOVER_INIT || submode == mkb::SMD_GAME_TIMEOVER_MAIN); +} + +bool is_story_retry_screen(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RETRY_INIT || submode == mkb::SMD_GAME_RETRY_MAIN); +} + +bool is_death_submode(mkb::SubMode submode) { + return is_fallout(submode) || is_timeover(submode) || is_story_retry_screen(submode); +} + +bool is_death_init(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_RINGOUT_INIT || submode == mkb::SMD_GAME_TIMEOVER_INIT || + submode == mkb::SMD_GAME_RETRY_INIT); +} + +bool is_timed_death_submode(mkb::SubMode submode) { + return is_fallout(submode) || is_timeover(submode); +} + +// --- actual gameplay --- + +bool is_on_stage(mkb::SubMode submode) { + return (is_spin_in(submode) || is_gameplay(submode) || is_postgoal(submode) || + is_death_submode(submode)); +} + +bool is_on_stage_with_first_init(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_on_stage(submode)); +} + +bool is_on_stage_with_endpoints(mkb::SubMode submode) { + return (is_spin_in_first_init(submode) || is_on_stage(submode) || + is_game_scenario_return(submode)); +} + +// --- menu submodes --- + +bool is_sel_ngc_init(mkb::SubMode submode) { + return (submode == mkb::SMD_SEL_NGC_INIT); +} + +bool is_sel_ngc_main(mkb::SubMode submode) { + return submode == mkb::SMD_SEL_NGC_MAIN; +} + +bool is_sel_ngc(mkb::SubMode submode) { + return (submode == mkb::SMD_SEL_NGC_INIT || submode == mkb::SMD_SEL_NGC_MAIN); +} + +// --- story exit game submodes --- + +bool is_story_exit_game_init(mkb::SubMode submode) { + return submode == mkb::SMD_GAME_INTR_SEL_INIT; +} + +bool is_story_exit_game_message(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_INTR_SEL_INIT || submode == mkb::SMD_GAME_INTR_SEL_MAIN); +} + +bool is_story_exit_game_save_prompt(mkb::SubMode submode) { + return (submode == mkb::SMD_GAME_SUGG_SAVE_INIT || submode == mkb::SMD_GAME_SUGG_SAVE_MAIN); +} + +bool is_story_exit_game(mkb::SubMode submode) { + return (is_story_exit_game_message(submode) || is_story_exit_game_save_prompt(submode)); +} + +// --- combinations of non-gameplay submodes --- + +bool is_stage_exit_submode(mkb::SubMode submode) { + return (is_game_scenario_main(submode) || is_sel_ngc(submode) || is_story_exit_game(submode)); +} + +bool is_stage_exit_init(mkb::SubMode submode) { + return is_game_scenario_return(submode) || is_sel_ngc_init(submode) || + is_story_exit_game_init(submode); +} + +// --- some story mode stuff --- + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode) { + return main_game_mode == mkb::STORY_MODE; +} + +bool is_story_cutscene(mkb::SubMode submode) { + return (submode == mkb::SMD_AUTHOR_PLAY_INIT || submode == mkb::SMD_AUTHOR_PLAY_MAIN || + submode == mkb::SMD_AUTHOR_PLAY_STORY_INIT || + submode == mkb::SMD_GAME_SCENSCNPLAY_RETURN); +} + +// --- 10 ball screen things using StoryModeStageSelectState --- + +bool is_on_10_ball_spin_in(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_INTRO_SEQUENCE; +} + +bool is_idle_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + return (state == mkb::STAGE_SELECT_UNK3 || // idle init + state == mkb::STAGE_SELECT_IDLE); +} + +bool has_selected_stage_on_10_ball_screen_init(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_UNK5; // selected init +} + +bool has_selected_stage_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + return state == mkb::STAGE_SELECT_SELECTED; +} + +// --- 10 ball screen related things using ScenInfo --- + +// The point of the next 3 functions is to provide safety checks for being able to access +// mkb::g_storymode_stageselect_state safely at all. We need to be careful not to access +// mkb::g_storymode_stageselect_state when not in a story mode run (we're not allowed to access it +// even on the file and name entry screens, so a main_game_mode check isn't enough) + +bool is_scen_mode_10_ball_screen(mkb::ScenInfo scen_info) { + // Some notes + // (1) Note that this will return false a little after we select + // a stage on the 10 ball screen but before entering a stage (I think when the next stage is + // loaded) + // (2) Also note that this can return true while on a stage if we do any% glitch related + // things, so be careful with that too + // (3) Note that this also returns true after exiting game on + // the 10 ball screen (during the exit game menu) + return (scen_info.mode == mkb::DMD_SCEN_RETURN_INIT || + scen_info.mode == mkb::DMD_SCEN_SEL_FLOOR_INIT || + scen_info.mode == mkb::DMD_SCEN_SEL_FLOOR_MAIN); +} + +bool is_scen_mode_on_stage(mkb::ScenInfo scen_info) { + // Returns true on a stage and the tail end of the 10 ball screen (usually, see exceptions + // explained above) + return (scen_info.mode == mkb::DMD_SCEN_GAME_INIT || scen_info.mode == mkb::DMD_SCEN_GAME_MAIN); +} + +// The next function gets rid of many of the exceptions described above by adding an additional +// submode check +bool is_on_10_ball_screen(mkb::SubMode sub_mode, mkb::ScenInfo scen_info) { + return (is_game_scenario(sub_mode) && + (is_scen_mode_10_ball_screen(scen_info) || is_scen_mode_on_stage(scen_info))); +} + +int get_story_stage_id_from_scen_info(mkb::ScenInfo scen_info) { + // useful if we want to get the stage id for the stage we're currently hovering over + // on the 10 ball screen + return mkb::get_story_mode_stage_id(scen_info.world, scen_info.world_stage); +} + +// --- story file select/name entry screens --- + +bool is_storymode_file_screen_init(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_INIT); +} + +bool is_storymode_file_screen_main(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_MAIN); +} + +bool is_storymode_name_entry_init(mkb::ScenInfo scen_info) { + // For some reason causes crashes when you check this during the file screen init + return scen_info.mode == mkb::DMD_SCEN_ENTRY_INIT; +} + +bool is_storymode_name_entry_screen_main(mkb::ScenInfo scen_info) { + return (scen_info.mode == mkb::DMD_SCEN_ENTRY_MAIN); +} + +// --- titlescreen related things --- + +bool is_titlescreen_main(mkb::SubMode submode) { + return (submode == mkb::SMD_ADV_TITLE_MAIN); +} + +// --- misc utility functions --- + +bool is_paused() { + // pre relocate_addr version: *reinterpret_cast(0x805BC474) & 8 + // note that checking mkb::g_some_other_flags == mkb::OF_GAME_PAUSED won't work, I think + // g_some_other_flags is a bitfield? + bool paused_now = (mkb::g_some_other_flags & mkb::OF_GAME_PAUSED); + return paused_now; +} + +// general utility function to get the theme id of a stage from its stage id +u16 theme_id_from_stage_id(int stage_id) { + return static_cast(mkb::get_stage_world_theme(stage_id)); +} + +// --- Story Mode Status Functions --- + +// Get the clear count of the current world we're on. Some notes: +// (1) mkb::get_world_unbeaten_stage_count() only increments after the game scenario return +// submode finishes +// (2) After completing a world, this function returns a value of 10 until the 10 ball screen +// of the next world (at which point it reverts to 0) +u16 get_clear_count_for_world() { + return mkb::get_world_unbeaten_stage_count(mkb::scen_info.world); +} + +u16 get_storymode_total_clear_count() { + u16 total = 0; + for (u16 k = 0; k < WORLD_COUNT; k++) { + total += mkb::get_world_unbeaten_stage_count(k); + } + return total; +} + +} // namespace mode diff --git a/src/utils/mode.h b/src/utils/mode.h new file mode 100644 index 00000000..c6ac5a60 --- /dev/null +++ b/src/utils/mode.h @@ -0,0 +1,78 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace mode { + +constexpr u16 WORLD_COUNT = 10; +constexpr u16 STAGES_PER_WORLD = 10; + +// submodes that are on the stage + +bool is_spin_in_first_init(mkb::SubMode submode); +bool is_spin_in_init(mkb::SubMode submode); +bool is_spin_in(mkb::SubMode submode); + +bool is_gameplay_init(mkb::SubMode submode); +bool is_gameplay_main(mkb::SubMode submode); +bool is_gameplay(mkb::SubMode submode); + +bool is_goal_init(mkb::SubMode submode); +bool is_postgoal(mkb::SubMode submode); +bool is_postgoal_replay(mkb::SubMode submode); +bool is_postgoal_with_game_return(mkb::SubMode submode); + +bool is_story_retry_screen(mkb::SubMode submode); +bool is_death_init(mkb::SubMode submode); +bool is_death_submode(mkb::SubMode submode); +bool is_timed_death_submode(mkb::SubMode submode); + +bool is_on_stage(mkb::SubMode submode); +bool is_on_stage_with_endpoints(mkb::SubMode submode); + +// other submodes + +bool is_sel_ngc_init(mkb::SubMode submode); +bool is_sel_ngc_main(mkb::SubMode submode); +bool is_sel_ngc(mkb::SubMode submode); +bool is_game_scenario_main(mkb::SubMode submode); +bool is_game_scenario_return(mkb::SubMode submode); +bool is_titlescreen_main(mkb::SubMode submode); +bool is_story_exit_game_init(mkb::SubMode submode); +bool is_story_exit_game(mkb::SubMode submode); +bool is_stage_exit_init(mkb::SubMode submode); +bool is_stage_exit_submode(mkb::SubMode submode); + +// 10 ball screen stuff + +bool is_on_10_ball_spin_in(mkb::StoryModeStageSelectState state); +bool is_idle_on_10_ball_screen(mkb::StoryModeStageSelectState state); +bool has_selected_stage_on_10_ball_screen_init(mkb::StoryModeStageSelectState state); +bool has_selected_stage_on_10_ball_screen(mkb::StoryModeStageSelectState state); + +bool is_on_10_ball_screen(mkb::SubMode sub_mode, mkb::ScenInfo scen_info); + +// Other storymode stuff + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode); + +bool is_storymode_file_screen_init(mkb::ScenInfo scen_info); +bool is_storymode_file_screen_main(mkb::ScenInfo scen_info); +bool is_storymode_name_entry_screen_main(mkb::ScenInfo scen_info); + +bool is_story_cutscene(mkb::SubMode submode); + +// story mode status + +u16 get_clear_count_for_world(); +u16 get_storymode_total_clear_count(); +// bool is_between_worlds(bool has_entered_goal); +// bool is_run_complete(bool has_entered_goal); + +// misc utility + +u16 theme_id_from_stage_id(int stage_id); +int get_story_stage_id_from_scen_info(mkb::ScenInfo scen_info); +bool is_paused(); + +} // namespace mode From 6bc2adac75b7c4c18aac3b0e634812bb32ac50fe Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:42:43 -0500 Subject: [PATCH 34/90] added last_action_was_blank() functions to savest --- src/systems/savest.cpp | 8 ++++++++ src/systems/savest.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index df382d60..2f3daf56 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -396,4 +396,12 @@ Action get_last_action() { return s_last_action; } +bool last_action_was_load() { + return s_last_action == Action::Load; +} + +bool last_action_was_save() { + return s_last_action == Action::Save; +} + } // namespace savest diff --git a/src/systems/savest.h b/src/systems/savest.h index f8516335..1e2b50ee 100644 --- a/src/systems/savest.h +++ b/src/systems/savest.h @@ -47,5 +47,7 @@ u32 get_timestamp(u32 slot); bool is_enabled(); Action get_last_action(); +bool last_action_was_load(); +bool last_action_was_save(); } // namespace savest From 69dc8d2c6d5648779ef80e44e8a4765aa65bed7f Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:43:57 -0500 Subject: [PATCH 35/90] imported and updated goal.cpp/h updated hooks to new system, updated the libesavest call haven't tested yet, will do after importing other things --- src/systems/goal.cpp | 214 +++++++++++++++++++++++++++++++++++++++++++ src/systems/goal.h | 19 ++++ src/systems/main.cpp | 2 + 3 files changed, 235 insertions(+) create mode 100644 src/systems/goal.cpp create mode 100644 src/systems/goal.h diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp new file mode 100644 index 00000000..6f9ceab7 --- /dev/null +++ b/src/systems/goal.cpp @@ -0,0 +1,214 @@ +#include "goal.h" + +#include "../mkb/mkb.h" +#include "../systems/savest.h" +#include "../utils/mode.h" +#include "../utils/patch.h" + +namespace goal { + +// Utility file that provides some functions that deal with and are based off of giving precise goal +// checks (ie the moment we break the tape and not delayed like the goal submodes are) +// This grew out of validate.cpp and was separated into its own file to avoid conflicting interests +// Namely, this file should be very low in the include chain to allow as many other files to use it + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; +constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; +constexpr u8 TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE = 3; +static u8 s_frames_until_goal_submode = 0; + +// Various goal flags that get set to true on goal tape break (and stay true during postgoal), but +// get unset at different times. Having different flags actually does have utility for us (see the +// is_between_worlds() function below and the associated notes) +static bool s_goal_flag_game_return; // Unset after game scenario return +static bool s_goal_flag_exit_game; // Unset outside of exit game and game scenario return +static bool s_goal_flag_last_stage; // Unset on spin in, gameplay pre tape break, and game scenario + // main + +// static patch::Tramp s_goal_tramp; +// static patch::Tramp s_mode_tick_tramp; + +u8 get_frames_until_goal_submode() { + return s_frames_until_goal_submode; +} +bool get_goal_flag_game_return() { + return s_goal_flag_game_return; +} +bool get_goal_flag_exit_game() { + return s_goal_flag_exit_game; +} +bool get_goal_flag_last_stage() { + return s_goal_flag_last_stage; +} + +// Important note on run order +// The run order of various relevant functions for us here is as follows: +// (1) mkb::process_inputs() (what prac mod tick() functions hook) +// (2) mkb::mode_tick() (changes the submode) +// (3) mkb::did_ball_enter_goal() +// (4) mkb::get_world_unbeaten_stage_count() (used to determine clear count, and if we're between +// worlds/at the end of a story mode run. Increments after game scenario return) +// (5) mkb::draw_debugtext (what prac mod disp() functions hook) + +void set_goal_flags() { + s_goal_flag_game_return = true; + s_goal_flag_exit_game = true; + s_goal_flag_last_stage = true; +} + +// Remedy for goal submode checks being delayed from tape break +bool is_postgoal_exact() { + return (s_frames_until_goal_submode != 0 && mode::is_gameplay_main(mkb::sub_mode)) || + mode::is_postgoal(mkb::sub_mode); +} + +bool is_gameplay_exact() { + return mode::is_gameplay(mkb::sub_mode) && !is_postgoal_exact(); +} + +// When entering the goal, set our flags to true and start the frames until goal submode timer +static patch::Tramp s_goal_tramp( + [](mkb::Ball *ball, int *out_stage_goal_idx, int *out_itemgroup_id, mkb::byte *out_goal_flags) { + // mkb::BOOL32 orig_result = s_did_ball_fallout_tramp.chain(ball); + bool result = + s_goal_tramp.chain(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); + + if (result) { + set_goal_flags(); + s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; + } + // We don't need to check if we're paused because this function doesn't + // get run while paused + if (s_frames_until_goal_submode != 0) { + s_frames_until_goal_submode -= 1; + } + + return result; + }); + +// When entering the goal, set our flags to true and start the frames until goal submode timer +/* bool did_ball_enter_goal_hook(mkb::Ball *ball, + int *out_stage_goal_idx, + int *out_itemgroup_id, + mkb::byte *out_goal_flags) { + bool result = s_goal_tramp.dest(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); + + if (result) { + set_goal_flags(); + s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; + } + // We don't need to check if we're paused because this function doesn't + // get run while paused + if (s_frames_until_goal_submode != 0) { + s_frames_until_goal_submode -= 1; + } + return result; +} */ + +// We also need to handle properly zeroing the timer and unsetting our flags in the appropriate +// submodes + +void reset_tape_break_counter() { + // Handle cases where we load state or pause (and either leave the stage or retry) immediately + // after breaking the tape + // This gets run after mkb::mode_tick ie after the game uppdates the submode + // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, + // which means that at the end of the frame that we load the state, s_frames_until_goal_submode + // will be 0 + // libsavest::state_loaded_this_frame() + // TODO: test that updating this didn't break anything! + if (mode::is_stage_exit_init(mkb::sub_mode) || savest::last_action_was_load() || + mode::is_spin_in_init(mkb::sub_mode)) { + s_frames_until_goal_submode = 0; + } +} + +void unset_goal_flags() { + if (!is_postgoal_exact()) { + if (!mode::is_game_scenario_return(mkb::sub_mode)) { + s_goal_flag_game_return = false; + } + if (!(mode::is_game_scenario_return(mkb::sub_mode) || + mode::is_story_exit_game(mkb::sub_mode))) { + s_goal_flag_exit_game = false; + } + if (mode::is_spin_in_init(mkb::sub_mode) || is_gameplay_exact() || + mode::is_game_scenario_main(mkb::sub_mode)) { + s_goal_flag_last_stage = false; + } + } +} + +static patch::Tramp s_mode_tick_tramp([]() { + s_mode_tick_tramp.chain(); + // Run this after the game updates the submode + // This ensures that our flags that should get unset during a submode switch don't get delayed + // by a frame + reset_tape_break_counter(); + unset_goal_flags(); +}); + +/* void mode_tick_hook() { + s_mode_tick_tramp.dest(); + // Run this after the game updates the submode + // This ensures that our flags that should get unset during a submode switch don't get delayed + // by a frame + reset_tape_break_counter(); + unset_goal_flags(); +} */ + +void init() { + // patch::hook_function(s_goal_tramp, &mkb::did_ball_enter_goal, did_ball_enter_goal_hook); + // patch::hook_function(s_mode_tick_tramp, mkb::mode_tick, mode_tick_hook); + s_goal_tramp.hook(); + s_mode_tick_tramp.hook(); +} + +// --- story mode status functions based on goal checks (is_between_worlds(), is_run_complete()) --- + +// Important note: mode::get_clear_count_for_world() is based off +// mkb::get_world_unbeaten_stage_count(), which only increments after game scenario return ends +// This is why it's important to make sure our flags aren't being unset 1 frame late when the +// submode switches to game scenario main (this ensures our timer/deathcounter won't flash/disappear +// for a frame when the "Between Worlds" pref is enabled). Due to the run order notes earlier, when +// the submode switches to game scenario main, first our flag gets updated, then the clear count +// increments, and then disp() functions run + +// To allow for the possibility of passing in different goal flags, we phrase the next function +// using a generic bool argument +bool is_between_worlds_main(bool goal_flag) { + u16 world_clear_count = mode::get_clear_count_for_world(); + if ((world_clear_count == STAGES_PER_WORLD - 1) && goal_flag) { + return true; + } else if (world_clear_count == STAGES_PER_WORLD) { + return true; + } + return false; +} + +// The reason we use different flags depending on if we're on the last world or not is for the +// following behavior: +// (1) If we exit game after completing the last stage in W10 and then return to the menu, we want +// our run to still be flagged as complete (so that we know we should reset the run instead of +// running the timer, due to how we handle resetting/not resetting the run if we fully exit game) +// (2) We can't use s_goal_flag_last_stage for every world, however, because of the possibility of +// doing an accidental (full) exit game all the way back out to the menus on the last stage of a +// (non world 10) world. If this happens, the game doesn't count the stage as cleared and you would +// need to reclear it. In this case, the correct behavior would be to continue running the segment +// timer on the menus and not consider us to be between worlds. Using different goal flags solves +// this edge case behavior + +bool is_between_worlds() { + u8 curr_world = mkb::scen_info.world; + if (curr_world == WORLD_COUNT - 1) { + return is_between_worlds_main(s_goal_flag_last_stage); + } else { + return is_between_worlds_main(s_goal_flag_exit_game); + } +} + +bool is_run_complete() { + return mkb::scen_info.world == WORLD_COUNT - 1 && is_between_worlds(); +} + +} // namespace goal \ No newline at end of file diff --git a/src/systems/goal.h b/src/systems/goal.h new file mode 100644 index 00000000..223cedce --- /dev/null +++ b/src/systems/goal.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace goal { + +u8 get_frames_until_goal_submode(); +bool get_goal_flag_game_return(); +bool get_goal_flag_exit_game(); +bool get_goal_flag_last_stage(); + +bool is_postgoal_exact(); +bool is_gameplay_exact(); +bool is_between_worlds(); +bool is_run_complete(); + +void init(); + +} // namespace goal diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 998d46a6..f59a6f13 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -4,6 +4,7 @@ #include "systems/assembly.h" #include "systems/binds.h" #include "systems/cardio.h" +#include "systems/goal.h" #include "systems/heap.h" #include "systems/menu_defn.h" #include "systems/menu_impl.h" @@ -183,6 +184,7 @@ void init() { physics::init(); iw::init(); savest::init(); + goal::init(); timer::init(); inputdisp::init(); cmseg::init(); From a2364c1d7abb0f65cf46892e2fbb663856210e8b Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 15 Aug 2026 23:49:28 -0500 Subject: [PATCH 36/90] import storyreset.cpp/h --- src/mods/storyreset.cpp | 186 ++++++++++++++++++++++++++++++++++++++++ src/mods/storyreset.h | 33 +++++++ 2 files changed, 219 insertions(+) create mode 100644 src/mods/storyreset.cpp create mode 100644 src/mods/storyreset.h diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp new file mode 100644 index 00000000..94e4520a --- /dev/null +++ b/src/mods/storyreset.cpp @@ -0,0 +1,186 @@ +#include "storyreset.h" + +#include "../systems/goal.h" +#include "../systems/pref.h" +#include "../utils/draw.h" +#include "../utils/mode.h" +#include "gotostory.h" + +namespace storyreset { + +// This file is used by both storytimer.cpp and deathcounter.cpp to determine when to reset the run +// since we want to allow the possibility for accidental exit games taking us back to the menus but +// still have the run be considered "active" + +static bool s_is_run_active = false; +static u8 s_active_save_file_idx = 0; // for handling resetting +static u8 s_last_active_world = 0; // for handling resetting + +bool is_run_active() { + return s_is_run_active; +} +u8 get_active_file_index() { + return s_active_save_file_idx; +} +u8 get_active_world() { + return s_last_active_world; +} + +void set_run_active_status(bool is_active) { + // setter that is called in storytimer.cpp so that deathcounter.cpp can know whether the run is + // active or not without having access to the stuff in storytimer.cpp (checking for loadless + // time being nonzero works to see if a run is active, but checking for deaths being nonzero + // doesn't work) + s_is_run_active = is_active; +} + +void reset_active_run_info() { + s_active_save_file_idx = 0; + s_last_active_world = 0; +} + +// This only gets called during the exit game init +// We store the world we were on + what file we were on so that we're able to properly handle +// resetting on the file screen +void record_run_status() { + s_last_active_world = mkb::scen_info.world; + s_active_save_file_idx = mkb::scen_info.save_file_idx; +} + +// --- checks for if we should reset --- + +bool detect_selecting_wrong_file() { + return mkb::data_select_menu_state == mkb::DSMS_OPEN_DATA && + mkb::selected_story_file_idx != s_active_save_file_idx; +} + +// Catches using the IW picker on our current active file +bool detect_wrong_world_on_active_file() { + mkb::StoryModeSaveFile active_file = mkb::storymode_save_files[s_active_save_file_idx]; + bool wrong_world = active_file.current_world != s_last_active_world; + bool active_file_empty = active_file.playtime_in_frames == 0; + return wrong_world || active_file_empty; +} + +bool should_reset_on_file_screen() { + if (mode::is_main_game_mode_story(mkb::main_game_mode) && + mode::is_storymode_file_screen_main(mkb::scen_info)) { + return detect_wrong_world_on_active_file() || detect_selecting_wrong_file(); + } + return false; +} + +// If we select challenge mode, practice mode, or go back to the main menu +bool is_on_wrong_menu() { + if (mode::is_sel_ngc_main(mkb::sub_mode)) { + mkb::MenuScreenID menu = mkb::g_currently_visible_menu_screen; + return (menu == mkb::MENUSCREEN_NUMBER_OF_PLAYERS || + menu == mkb::MENUSCREEN_CHARACTER_SELECT_1 || menu == mkb::MENUSCREEN_MODE_SELECT); + } + return false; +} + +bool used_go_to_story() { + return gotostory::get_gotostory_state() != gotostory::State::Default; +} + +bool should_reset_completed_run() { + if (mode::is_sel_ngc(mkb::sub_mode) || mode::is_titlescreen_main(mkb::sub_mode)) { + return goal::is_run_complete(); + } + return false; +} + +bool should_reset_run() { + return should_reset_on_file_screen() || is_on_wrong_menu() || used_go_to_story() || + should_reset_completed_run(); +} + +// TODO: reset run if selecting active file and active file has 0 stages cleared? + +void tick() { + if (mode::is_main_game_mode_story(mkb::main_game_mode) && + mode::is_story_exit_game_init(mkb::sub_mode)) { + record_run_status(); + } +} + +bool all_loadless_timer_prefs_off() { + StoryDisplayOptions fullgame_pref = + StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); + StoryDisplayOptions segment_pref = + StoryDisplayOptions(pref::get(pref::Pref::SegmentTimerOptions)); + bool breakdown_pref = pref::get(pref::Pref::ShowRunBreakdown); + + return fullgame_pref == StoryDisplayOptions::DontShow && + segment_pref == StoryDisplayOptions::DontShow && !breakdown_pref; +} + +bool death_counter_pref_off() { + StoryDisplayOptions death_counter_pref = + StoryDisplayOptions(pref::get(pref::Pref::DeathCounterDisplayOptions)); + return death_counter_pref == StoryDisplayOptions::DontShow; +} + +// During an accidental full exit game to the menu mid-run, the only time a timer/death counter is +// visible is if "always show" is turned on for that display +bool displaying_on_menus_during_accidental_exit_game() { + StoryDisplayOptions fullgame_pref = + StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); + StoryDisplayOptions segment_pref = + StoryDisplayOptions(pref::get(pref::Pref::SegmentTimerOptions)); + StoryDisplayOptions death_counter_pref = + StoryDisplayOptions(pref::get(pref::Pref::DeathCounterDisplayOptions)); + return fullgame_pref == StoryDisplayOptions::AlwaysShow || + segment_pref == StoryDisplayOptions::AlwaysShow || + death_counter_pref == StoryDisplayOptions::AlwaysShow; +} + +bool run_active_but_not_displaying_on_menus() { + return !displaying_on_menus_during_accidental_exit_game() && + !(all_loadless_timer_prefs_off() && death_counter_pref_off()); +} + +bool is_silent_reset_type() { + return should_reset_on_file_screen() || is_on_wrong_menu(); +} + +void display_reset_run_message() { + if (pref::get(pref::Pref::HideRunResetMessage)) { + return; + } + + // If the hide run reset message pref is off, we still want to be "minimal" with displaying it + // So, we only display a reset message if: + // (1) it's a less obvious reset trigger (eg not go to story) + // (2) at least one timer/death counter pref is on (don't bother displaying a reset message if + // everything is turned off) + // (3) the player has "always show" turned on for at least one of the timers/death counter. The + // idea here is if always show is turned on and if the timer gets zero-ed, it's obvious that the + // run was reset and no message needs to be displayed + + if (!is_silent_reset_type()) { + return; + } + + if (all_loadless_timer_prefs_off() && death_counter_pref_off()) { + return; + } + + if (displaying_on_menus_during_accidental_exit_game()) { + return; + } + + draw::notify(draw::WHITE, "Run Was Reset"); +} + +/* if ((!is_silent_reset_type() || !run_active_but_not_displaying_on_menus())) { + return; + } */ + +// && death_counter_pref_off() +/* if (all_loadless_timer_prefs_off()) { + return; +} */ + +} // namespace storyreset \ No newline at end of file diff --git a/src/mods/storyreset.h b/src/mods/storyreset.h new file mode 100644 index 00000000..a73ea431 --- /dev/null +++ b/src/mods/storyreset.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace storyreset { + +// Does it make more sense to move this enum class to pref.h? +// We need this enum class in storyreset.cpp, but we also need it in both deathcounter.cpp and +// storytimer.cpp +enum class StoryDisplayOptions { + DontShow, + AlwaysShow, + BetweenWorlds, + EndOfRun, +}; +// The fullgame loadless timer and death counter share the same display options. The loadless +// segment timer options are a subset of this enum class (everything except an EndOfRun setting), +// and the segment timer options show up in the same order in the menu as the corresponding fullgame +// one, so we don't need to make a separate enum class for it + +bool is_run_active(); +void set_run_active_status(bool is_active); + +u8 get_active_file_index(); +u8 get_active_world(); +void reset_active_run_info(); +bool should_reset_run(); +void display_reset_run_message(); +bool all_loadless_timer_prefs_off(); + +void tick(); + +} // namespace storyreset From 2b41734c4b3f14e75f9e90c13efd97a95c537167 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 02:59:36 -0500 Subject: [PATCH 37/90] fix unresolved external symbol error with gotostory very fun and wonderful bug --- src/mods/gotostory.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mods/gotostory.cpp b/src/mods/gotostory.cpp index d6d42d36..f1038e26 100644 --- a/src/mods/gotostory.cpp +++ b/src/mods/gotostory.cpp @@ -6,7 +6,9 @@ namespace gotostory { static State s_state = State::Default; -State get_gotostory_state() { return s_state; } +State get_gotostory_state() { + return s_state; +} void load_storymode() { if (mkb::main_mode == mkb::MD_SEL) { From 3100fe9873ddddc34db52dabc7c60e1c2a9cc01d Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:09:02 -0500 Subject: [PATCH 38/90] Revert to using History struct in savest i like it --- src/systems/savest.cpp | 40 +++++++++++++++++++++++++++++----------- src/systems/savest.h | 13 ++++++++++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index 2f3daf56..07b27909 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -32,7 +32,7 @@ struct SaveState { u32 s_timestamp; SaveState s_states[SLOT_COUNT]; -Action s_last_action; +History s_history; // // Hooks @@ -50,7 +50,7 @@ static patch::Tramp s_set_minimap_mode_tramp([](mkb::Mini // Prevent sound effects from playing while loading states static patch::Tramp s_soundreq_tramp([](u32 id) { - if (s_last_action != Action::Load) { + if (s_history.curr_frame_action != Action::Load) { s_soundreq_tramp.chain(id); } }); @@ -291,7 +291,7 @@ SaveResult save(u32 slot) { state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; - s_last_action = Action::Save; + s_history.curr_frame_action = Action::Save; return SaveResult::Ok; } @@ -350,7 +350,7 @@ LoadResult load(u32 slot) { } state->timestamp = s_timestamp; - s_last_action = Action::Load; + s_history.curr_frame_action = Action::Load; return LoadResult::Ok; } @@ -377,7 +377,9 @@ u32 get_timestamp(u32 slot) { } void tick() { - s_last_action = Action::None; + // Rotate history + s_history.prev_frame_action = s_history.curr_frame_action; + s_history.curr_frame_action = Action::None; s_timestamp++; @@ -392,16 +394,32 @@ bool is_enabled() { return pref::get(pref::Pref::Savestates) && !pref::get(pref::Pref::Freecam); } -Action get_last_action() { - return s_last_action; +History get_history() { + return s_history; } -bool last_action_was_load() { - return s_last_action == Action::Load; +Action get_curr_frame_action() { + return s_history.curr_frame_action; } -bool last_action_was_save() { - return s_last_action == Action::Save; +bool state_changed_gameplay() { + return s_history.curr_frame_action == Action::Load; +} + +// Note: checking s_history.curr_frame_action == Action::Load is different from +// interacted_with_state() +// With the former, pressing the create state button for 1 frame won't be detected + +bool interacted_with_state() { + // Returns true if we created a state this frame, or if we continue to hold the create state + // button, or if we load state + return s_history.curr_frame_action != Action::None; +} + +bool loaded_state() { + // Won't return true if we create a state, only if we load one + return s_history.curr_frame_action == Action::Load && + s_history.prev_frame_action == Action::None; } } // namespace savest diff --git a/src/systems/savest.h b/src/systems/savest.h index 1e2b50ee..8540faec 100644 --- a/src/systems/savest.h +++ b/src/systems/savest.h @@ -36,6 +36,11 @@ enum class Action { Save, }; +struct History { + Action curr_frame_action; + Action prev_frame_action; +}; + void init(); void tick(); @@ -46,8 +51,10 @@ bool is_empty(u32 slot); u32 get_timestamp(u32 slot); bool is_enabled(); -Action get_last_action(); -bool last_action_was_load(); -bool last_action_was_save(); +History get_history(); +Action get_curr_frame_action(); +bool state_changed_gameplay(); +bool interacted_with_state(); +bool loaded_state(); } // namespace savest From 6733433d7d6f43513866a574af1d65718054fd75 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:11:25 -0500 Subject: [PATCH 39/90] savest: use mkb::selected_characters instead of mkb::active_monkey_id savestates won't work with gotostory if we haven't manually selected aiai in the menu (but are using the prac mod setting to set our monkey selection) if we use mkb::active_monkey_id --- src/systems/savest.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index 07b27909..91d71c37 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -24,7 +24,7 @@ struct SaveState { u32 flags; u32 timestamp; s32 stage_id; - u32 character; + u8 character; store::Store store; u8 pause_menu_sprite_status; mkb::Sprite pause_menu_sprite; @@ -287,7 +287,8 @@ SaveResult save(u32 slot) { pass_over_regions(&state->store, store::save); handle_pause_menu_save(state); state->stage_id = mkb::current_stage_id; - state->character = mkb::active_monkey_id[mkb::curr_player_idx]; + // using mkb::selected_characters instead of mkb::active_monkey_id works with gotostory + state->character = mkb::selected_characters[mkb::curr_player_idx]; state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; From 0bade0f8a6bf0b3764714f0d47332ac3d55152f7 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:12:06 -0500 Subject: [PATCH 40/90] update goal to use the new interacted_with_state() func --- src/systems/goal.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index 6f9ceab7..52c24a23 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -69,11 +69,10 @@ bool is_gameplay_exact() { // When entering the goal, set our flags to true and start the frames until goal submode timer static patch::Tramp s_goal_tramp( [](mkb::Ball *ball, int *out_stage_goal_idx, int *out_itemgroup_id, mkb::byte *out_goal_flags) { - // mkb::BOOL32 orig_result = s_did_ball_fallout_tramp.chain(ball); - bool result = + bool orig_result = s_goal_tramp.chain(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); - if (result) { + if (orig_result) { set_goal_flags(); s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; } @@ -83,7 +82,7 @@ static patch::Tramp s_goal_tramp( s_frames_until_goal_submode -= 1; } - return result; + return orig_result; }); // When entering the goal, set our flags to true and start the frames until goal submode timer @@ -117,7 +116,7 @@ void reset_tape_break_counter() { // will be 0 // libsavest::state_loaded_this_frame() // TODO: test that updating this didn't break anything! - if (mode::is_stage_exit_init(mkb::sub_mode) || savest::last_action_was_load() || + if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { s_frames_until_goal_submode = 0; } From 7f00f7b684628495e79ca5c92e65d557789a9041 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:12:56 -0500 Subject: [PATCH 41/90] import deathcounter.cpp/h --- src/mods/deathcounter.cpp | 168 ++++++++++++++++++++++++++++++++++++++ src/mods/deathcounter.h | 21 +++++ src/systems/main.cpp | 5 ++ 3 files changed, 194 insertions(+) create mode 100644 src/mods/deathcounter.cpp create mode 100644 src/mods/deathcounter.h diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp new file mode 100644 index 00000000..e84bc9a6 --- /dev/null +++ b/src/mods/deathcounter.cpp @@ -0,0 +1,168 @@ +#include "deathcounter.h" + +#include "../mkb/mkb.h" + +#include "../systems/goal.h" +#include "../systems/pref.h" +#include "../systems/savest.h" +#include "../utils/draw.h" +#include "../utils/macro_utils.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/timerdisp.h" // for testing +#include "freecam.h" +#include "storyreset.h" + +namespace deathcounter { + +constexpr u16 COUNTER_DISPLAY_Y_POS = 56; +constexpr u16 COUNTER_DISPLAY_X_POS = 18; +constexpr u16 COUNTER_NUMBER_X_POS = COUNTER_DISPLAY_X_POS + 7 * draw::DEBUG_CHAR_WIDTH; + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; + +static u32 s_world_death_count[WORLD_COUNT] = {}; +// Flag to determine when we should/shouldn't increment the death counter +static bool s_can_incr_death_counter = false; + +u32 get_total_death_count() { + u32 total = 0; + for (u16 k = 0; k < WORLD_COUNT; k++) { + total += s_world_death_count[k]; + } + return total; +} + +u32 get_world_death_count(u16 world_idx) { + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); // clamp for safety + return s_world_death_count[clamped_idx]; +} + +void increment_world_death_counter() { + // Check the pref for count first stage deaths and if we're on the first stage + if (!pref::get(pref::Pref::CountFirstStageDeaths) && + mode::get_storymode_total_clear_count() == 0) { + return; + } + s_world_death_count[mkb::scen_info.world] += 1; // death counter for the current world + s_can_incr_death_counter = false; +} + +void reset_flag() { + s_can_incr_death_counter = false; +} + +void reset_death_counters() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + s_world_death_count[k] = 0; + } + reset_flag(); +} + +// When we're done holding the savestate button/when gameplay resumes +void update_flag_on_state_release() { + // goal::is_gameplay_exact() && !libsavest::state_loaded_this_frame() + if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // PLACEHOLDER + // As soon as we're done holding the load state button (or just any time we're controlling + // the monkey on the stage), we're allowed to die + s_can_incr_death_counter = true; + } +} + +bool should_count_as_normal_death() { + bool retried_without_clearing = + (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); + bool left_stage_without_clearing = + mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; + bool died = mode::is_death_init(mkb::sub_mode); + return retried_without_clearing || left_stage_without_clearing || died; +} + +bool should_count_as_savestate_death() { + // return libsavest::state_loaded_this_frame() && s_can_incr_death_counter; + return savest::loaded_state() && s_can_incr_death_counter; +} + +void count_deaths() { + if (goal::is_postgoal_exact()) { + s_can_incr_death_counter = false; + } + + if (should_count_as_normal_death()) { + increment_world_death_counter(); + } + + if (should_count_as_savestate_death()) { + increment_world_death_counter(); + } +} + +void tick() { + if (storyreset::should_reset_run()) { + reset_death_counters(); + } + + // Whenever entering a new stage, reset our flag + if (mode::is_spin_in_first_init(mkb::sub_mode)) { + reset_flag(); + } + + count_deaths(); + update_flag_on_state_release(); +} + +bool should_display_death_counter() { + u8 pref = pref::get(pref::Pref::DeathCounterDisplayOptions); + + using DeathCounterOptions = storyreset::StoryDisplayOptions; + + switch (DeathCounterOptions(pref)) { + case DeathCounterOptions::AlwaysShow: + return true; + case DeathCounterOptions::BetweenWorlds: + return goal::is_between_worlds(); + case DeathCounterOptions::EndOfRun: + return goal::is_run_complete(); + case DeathCounterOptions::DontShow: + return false; + } + + // Unreachable, silence compiler warning? + return false; +} + +bool should_not_display_counter_at_all() { + if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + // If we're in the menus outside of a story mode run due to an accidental exit game, we + // still want to be able to display the counter if we haven't reset it yet + return !storyreset::is_run_active(); + } + return freecam::should_hide_hud(); +} + +void disp() { + if (should_not_display_counter_at_all()) { + return; + } + + if (should_display_death_counter()) { + draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); + draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", + get_total_death_count()); + } + + /* u8 pos; + if (should_display_death_counter()) { + pos = 2; + } else { + pos = 0; + } + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 1 + pos, 44, + "Dbg:", 60 * storyreset::is_run_active(), true, draw::WHITE); + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 2 + pos, 44, "Sub:", 60 * mkb::sub_mode, true, + draw::WHITE); + timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 3 + pos, 44, + "Gol:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); */ +} + +} // namespace deathcounter diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h new file mode 100644 index 00000000..0672b58c --- /dev/null +++ b/src/mods/deathcounter.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace deathcounter { + +/* enum class DeathCounterOptions { + DontShow = 0, + AlwaysShow = 1, + BetweenWorlds = 2, + EndOfRun = 3, +}; */ + +u32 get_total_death_count(); +u32 get_world_death_count(u16 world_idx); +bool should_display_death_counter(); + +void tick(); +void disp(); + +} // namespace deathcounter \ No newline at end of file diff --git a/src/systems/main.cpp b/src/systems/main.cpp index f59a6f13..40c85345 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -20,6 +20,7 @@ #include "mods/banans.h" #include "mods/camera.h" #include "mods/cmseg.h" +#include "mods/deathcounter.h" #include "mods/dpad.h" #include "mods/fallout.h" #include "mods/freecam.h" @@ -36,6 +37,7 @@ #include "mods/scratch.h" #include "mods/sfx.h" #include "mods/stage_edits.h" +#include "mods/storyreset.h" #include "mods/timer.h" #include "mods/unlock.h" @@ -87,6 +89,8 @@ static patch::Tramp s_process_inputs_tramp([]() { cardio::tick(); unlock::tick(); iw::tick(); + storyreset::tick(); + deathcounter::tick(); savest::tick(); savest_ui::tick(); menu_impl::tick(); // anything checking for pref changes should run after menu_impl::tick() @@ -128,6 +132,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); + deathcounter::disp(); ilbattle::disp(); cmseg::disp(); inputdisp::disp(); From af5318334f1ede4bbc2a189c29f9fadb7d69a3d0 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:13:26 -0500 Subject: [PATCH 42/90] import storytimer.cpp/h --- src/mods/storytimer.cpp | 362 ++++++++++++++++++++++++++++++++++++++++ src/mods/storytimer.h | 32 ++++ src/systems/main.cpp | 4 + 3 files changed, 398 insertions(+) create mode 100644 src/mods/storytimer.cpp create mode 100644 src/mods/storytimer.h diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp new file mode 100644 index 00000000..b2539c5e --- /dev/null +++ b/src/mods/storytimer.cpp @@ -0,0 +1,362 @@ +#include "storytimer.h" + +#include "../mkb/mkb.h" + +#include "../systems/goal.h" +#include "../systems/pad.h" // for testing +#include "../systems/pref.h" +#include "../systems/savest.h" // for testing +#include "../utils/draw.h" +#include "../utils/macro_utils.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/timerdisp.h" +#include "deathcounter.h" +#include "freecam.h" +#include "storyreset.h" + +namespace storytimer { + +constexpr u16 FULLGAME_TIMER_LOCATION_X = 18 + 24; +constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; +constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; +constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; +constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr u16 STARTING_ROW = 2; + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; +constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; + +static TimerGroup s_timer_group[WORLD_COUNT]; // timer info for each world + +// --- some getters that other files can use (if needed) --- + +TimerGroup get_world_timer_info(u16 world_idx) { + // clamp for safety so we don't access outside the bounds of the array + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); + return s_timer_group[clamped_idx]; +} + +// Used to calculate our split times for the breakdown screen; also gives us a +// convenient way to calculate fullgame loadless time +u32 get_split_timer_for_world(u16 world_idx) { + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); + u32 prev_world_sum = 0; + for (u16 k = 0; k < clamped_idx; k++) { + prev_world_sum += s_timer_group[k].full_world; + } + return prev_world_sum + s_timer_group[clamped_idx].segment; +} + +u32 get_loadless_time() { + return get_split_timer_for_world(WORLD_COUNT - 1); +} + +bool is_run_active() { + return get_loadless_time() != 0 && !goal::is_run_complete(); +} + +// --- main timer logic --- + +void reset_timer() { + if (get_loadless_time() != 0) { + for (u16 k = 0; k < WORLD_COUNT; k++) { + s_timer_group[k] = {}; + } + storyreset::reset_active_run_info(); + storyreset::display_reset_run_message(); + // mkb::OSReport("Reset timer \n"); + } +} + +// Importantly, we don't increment the timer on the 10 ball screen after selecting a stage +// since the time between doing so and entering the next stage can be highly variable +// To properly increment the timer on the 10 ball screen, we run this +// update function inside a hook for mkb::g_handle_storymode_stageselect_state +void update_timers_on_10_ball_screen(mkb::StoryModeStageSelectState state) { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { // World (k+1)'s timer + if (mode::is_on_10_ball_spin_in(state) || mode::is_idle_on_10_ball_screen(state) || + mode::has_selected_stage_on_10_ball_screen_init(state)) { + s_timer_group[k].full_world += 1; + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } +} + +// The above function does not get run when the game is paused, so we also need this +void update_timers_while_paused_on_10_ball_screen() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { + if (mode::is_on_10_ball_screen(mkb::sub_mode, mkb::scen_info) && mode::is_paused()) { + s_timer_group[k].full_world += 1; + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } +} + +// mode::is_on_stage_with_endpoints(mkb::sub_mode) +// Increment the timer on every submode on the stage (and exit game screen) +void update_timers_on_stage() { + for (u16 k = 0; k < WORLD_COUNT; k++) { + if (mkb::scen_info.world == k) { // World (k+1)'s timer + if (mode::is_on_stage(mkb::sub_mode) || mode::is_story_exit_game(mkb::sub_mode)) { + s_timer_group[k].full_world += 1; + + // Increment the segment timer until tape breek on the last stage of the world + if (!goal::is_between_worlds()) { + s_timer_group[k].segment = s_timer_group[k].full_world; + } + } + } + } +} + +// for if we fully exit game by accident +void update_timers_on_menus() { + if ((mode::is_sel_ngc(mkb::sub_mode) || mode::is_storymode_file_screen_main(mkb::scen_info)) && + is_run_active()) { + u8 active_world_idx = storyreset::get_active_world(); + s_timer_group[active_world_idx].full_world += 1; + s_timer_group[active_world_idx].segment = s_timer_group[active_world_idx].full_world; + } +} + +// So deathcounter.cpp can know when the run is active +void update_run_active_flag() { + storyreset::set_run_active_status(is_run_active()); +} + +void tick() { + if (storyreset::should_reset_run()) { + reset_timer(); + } + + if (mode::is_main_game_mode_story(mkb::main_game_mode)) { + update_timers_on_stage(); + update_timers_while_paused_on_10_ball_screen(); + } + + update_timers_on_menus(); + update_run_active_flag(); +} + +// --- display stuff --- + +// The run breakdown screen replaces the segment timer at the end of the run +// if the pref for it is on +bool should_display_timer(TimerType type) { + u8 pref; + if (type == TimerType::Fullgame) { + pref = pref::get(pref::Pref::FullgameTimerOptions); + } else { // Segment timer + pref = pref::get(pref::Pref::SegmentTimerOptions); + } + + using TimerOptions = storyreset::StoryDisplayOptions; + + switch (TimerOptions(pref)) { + case TimerOptions::AlwaysShow: + if (type == TimerType::Fullgame) { + return true; + } else if (pref::get(pref::Pref::ShowRunBreakdown)) { + // type is segment timer + show breakdown on + return !goal::is_run_complete(); + } else { // type is segment timer + show breakdown off + return true; + } + case TimerOptions::BetweenWorlds: + if (type == TimerType::Fullgame) { + return goal::is_between_worlds(); + } else if (pref::get(pref::Pref::ShowRunBreakdown)) { + return goal::is_between_worlds() && !goal::is_run_complete(); + } else { + return goal::is_between_worlds(); + } + case TimerOptions::EndOfRun: + if (type == TimerType::Fullgame) { + return goal::is_run_complete(); + } else { + // We don't have an end of run pref for the segment timer (since it doesn't really + // make sense to have one) + return false; + } + case TimerOptions::DontShow: + return false; + } + + // Unreachable + return false; +} + +// Doing this for now until a better display setup is figured out +u16 get_timer_y_pos(TimerType type) { // maybe rename to get_timer_row()? + u16 y_pos = STARTING_ROW; + bool is_displaying_death_counter = deathcounter::should_display_death_counter(); + + if (type == TimerType::Fullgame) { + if (is_displaying_death_counter) { + y_pos++; + } + } else { // Segment timer + if (should_display_timer(TimerType::Fullgame)) { + y_pos++; + } + if (is_displaying_death_counter) { + y_pos++; + } + } + + return y_pos; +} + +// Bundle up the info timerdisp::draw_timer uses into a struct for convenience +struct TimerDisplayInfo { + u16 pos_x; + u16 pos_y; + u16 text_offset; +}; + +TimerDisplayInfo get_timer_display_info(TimerType type) { + if (type == TimerType::Fullgame) { + return {FULLGAME_TIMER_LOCATION_X, get_timer_y_pos(type), FULLGAME_TIMER_TEXT_OFFSET}; + } else { + return {SEGMENT_TIMER_LOCATION_X, get_timer_y_pos(type), SEGMENT_TIMER_TEXT_OFFSET}; + } +} + +u16 get_current_segment_idx() { + // mkb::scen_info.world gets reset to 0 on the file screen, so the only case we ever need to + // worry about is if we fully exit game and are on the file screen with the timer still running + if (mode::is_storymode_file_screen_main(mkb::scen_info) && get_loadless_time() != 0) { + // The world we were on before exit-gaming + return storyreset::get_active_world(); + } else { + // if we're in a run (not on the menus), this is the index of the current world (between 0 + // and 9 inclusive) + return mkb::scen_info.world; + } +} + +void draw_timers() { + TimerDisplayInfo fullgame_info = get_timer_display_info(TimerType::Fullgame); + u32 loadless_time = get_loadless_time(); + + if (should_display_timer(TimerType::Fullgame)) { + timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.pos_y, fullgame_info.text_offset, + "Time:", loadless_time, false, draw::WHITE); + } + + u16 world_idx = + get_current_segment_idx(); // index of the current world (between 0 and 9 inclusive) + TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); + + if (should_display_timer(TimerType::Segment)) { + timerdisp::draw_timer(seg_info.pos_x, seg_info.pos_y, seg_info.text_offset, + "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); + } +} + +// We only use this function for 0 <= row <= 9 +Vec2d get_breakdown_row_position(u16 row) { + u16 starting_row = get_timer_y_pos(TimerType::Segment); + float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); + if (row < WORLD_COUNT - 1) { + return {BREAKDOWN_ROW_LOCATION_X, pos_y}; + } else { // "W10" takes up more space than "Wk" for "1 <= k <= 9" + return {SEGMENT_TIMER_LOCATION_X, pos_y}; + } +} + +void draw_breakdown_screen() { // TODO: totals row? + // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" + char split_buf[WORLD_COUNT][32] = {}; + char seg_buf[WORLD_COUNT][32] = {}; + char row_info_buf[WORLD_COUNT][32] = {}; + + for (u16 idx = 0; idx < WORLD_COUNT; idx++) { + Vec2d pos = get_breakdown_row_position(idx); + u32 world_deaths = deathcounter::get_world_death_count(idx); + + timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), + timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, + timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], + world_deaths); + + draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); + } +} + +bool should_not_display_timer_at_all() { + // The only time we can ever display the timer outside of story mode is + // when we fully exit game and the timer is still running + if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + return !is_run_active(); + } + // If in story, hide the timer if freecamming (if the pref is on) + return freecam::should_hide_hud(); +} + +void disp() { + if (should_not_display_timer_at_all()) { + // return; // testing, remember to uncomment + } + + draw_timers(); + + if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { + draw_breakdown_screen(); + } + + // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); + // u32 new_val = val & 0x0000ffff; + + u16 pos_y = get_timer_y_pos(TimerType::Segment); + + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, + "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, + "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, + "Cur:", 60 * static_cast(savest::get_history().curr_frame_action), + true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, + "Prv:", 60 * static_cast(savest::get_history().prev_frame_action), + true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, + "gp:", 60 * savest::state_changed_gameplay(), true, draw::WHITE); + timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, + "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); +} + +// for easier timer testing +// static patch::Tramp s_fade_screen_to_color_tramp; +/* static patch::Tramp + s_g_handle_storymode_stageselect_state_tramp; */ + +static patch::Tramp + s_g_handle_storymode_stageselect_state_tramp([]() { + s_g_handle_storymode_stageselect_state_tramp.chain(); + update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); + }); + +void init_main_game() { + /* patch::hook_function(s_fade_screen_to_color_tramp, mkb::fade_screen_to_color, + [](mkb::uint flags, u32 color, mkb::uint frames) { + // don't run for testing purposes + // s_fade_screen_to_color_tramp.dest(flags, color, frames); + }); */ + /* patch::hook_function(s_g_handle_storymode_stageselect_state_tramp, + mkb::g_handle_storymode_stageselect_state, []() { + s_g_handle_storymode_stageselect_state_tramp.dest(); + update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); + // update_run_status(); + }); */ + s_g_handle_storymode_stageselect_state_tramp.hook(); +} + +} // namespace storytimer \ No newline at end of file diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h new file mode 100644 index 00000000..0de3105c --- /dev/null +++ b/src/mods/storytimer.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace storytimer { + +/* enum class TimerOptions { + DontShow = 0, + AlwaysShow = 1, + BetweenWorlds = 2, + EndOfRun = 3, +}; */ + +enum class TimerType { + Fullgame, + Segment, +}; + +struct TimerGroup { + u32 segment; // the time taken to complete a world up until tape break on the last stage + u32 full_world; // the time taken to complete a world until the fade to white on the last stage +}; + +TimerGroup get_world_timer_info(u16 world_idx); +u32 get_split_timer_for_world(u16 world_idx); +u32 get_loadless_time(); + +void init_main_game(); +void tick(); +void disp(); + +} // namespace storytimer \ No newline at end of file diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 40c85345..284e28da 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -38,6 +38,7 @@ #include "mods/sfx.h" #include "mods/stage_edits.h" #include "mods/storyreset.h" +#include "mods/storytimer.h" #include "mods/timer.h" #include "mods/unlock.h" @@ -90,6 +91,7 @@ static patch::Tramp s_process_inputs_tramp([]() { unlock::tick(); iw::tick(); storyreset::tick(); + storytimer::tick(); deathcounter::tick(); savest::tick(); savest_ui::tick(); @@ -132,6 +134,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); + storytimer::disp(); deathcounter::disp(); ilbattle::disp(); cmseg::disp(); @@ -166,6 +169,7 @@ static patch::Tramp s_OSLink_tramp([](mkb::OSModuleHeader *rel_buff s_smd_game_ready_init_tramp.hook(); s_smd_game_play_tick_tramp.hook(); jump::patch_minimap(); + storytimer::init_main_game(); } // Sel_ngc init functions // else if (relutil::ModuleId(rel_buffer->info.id) == relutil::ModuleId::SelNgc) { From 3caac61b8eb6775ccaf34a9a55bf3ed63e1ecc35 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:15:05 -0500 Subject: [PATCH 43/90] import ilmark changes --- src/mods/ilmark.cpp | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index e5b73f5b..ce28ab57 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -3,18 +3,26 @@ #include "mkb/mkb.h" #include "mods/freecam.h" #include "mods/physics.h" +#include "mods/storyreset.h" #include "mods/validate.h" +#include "systems/goal.h" #include "systems/menu_impl.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/savest.h" #include "systems/version.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" -#include "systems/savest.h" namespace ilmark { +enum class StoryIlMarkSettings { + DontShow, + AlwaysInStory, + AtRunEnd, +}; + static bool s_valid_run = false; static bool s_is_romhack = false; @@ -44,23 +52,40 @@ void tick() { } } +bool show_in_story() { + u8 story_pref = pref::get(pref::Pref::IlMarkStory); + switch (StoryIlMarkSettings(story_pref)) { + case StoryIlMarkSettings::DontShow: + return false; + case StoryIlMarkSettings::AtRunEnd: + // show the mark if at least one timer related pref is on and if we've finished the run + return !storyreset::all_loadless_timer_prefs_off() && goal::is_run_complete(); + case StoryIlMarkSettings::AlwaysInStory: + return true; + } + + // Unreachable + return true; +} + bool is_ilmark_enabled() { if (mkb::main_mode != mkb::MD_GAME) return false; + // If in a romhack and the pref is off, don't show any validation marks whatsoever + if (s_is_romhack && !pref::get(pref::Pref::IlMarkRomhacks)) { + return false; + } + if (mkb::main_game_mode == mkb::PRACTICE_MODE) { if (!pref::get(pref::Pref::IlMarkPractice)) return false; } else if (mkb::main_game_mode == mkb::STORY_MODE) { - if (!pref::get(pref::Pref::IlMarkStory)) return false; + if (!show_in_story()) return false; } else if (mkb::main_game_mode == mkb::CHALLENGE_MODE) { if (!pref::get(pref::Pref::IlMarkChallenge)) return false; } else { return false; } - if (s_is_romhack && !pref::get(pref::Pref::IlMarkRomhacks)) { - return false; - } - return true; } From 3e266b4b545b7b578948e904913ab3cefeed4eea Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:16:08 -0500 Subject: [PATCH 44/90] update ilmark, validate to use the new func in savest --- src/mods/ilmark.cpp | 5 ++++- src/mods/validate.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index ce28ab57..d5acb3ff 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -47,7 +47,10 @@ void tick() { s_valid_run = false; } - if (savest::get_last_action() == savest::Action::Load) { + // savest::get_last_action() == savest::Action::Load + // savest::interacted_with_state() + if (savest::state_changed_gameplay()) { + // why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? s_valid_run = false; } } diff --git a/src/mods/validate.cpp b/src/mods/validate.cpp index e36343d9..80ba5ff0 100644 --- a/src/mods/validate.cpp +++ b/src/mods/validate.cpp @@ -4,10 +4,10 @@ #include "systems/menu_impl.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/savest.h" #include "utils/macro_utils.h" #include "utils/patch.h" #include "utils/relutil.h" -#include "systems/savest.h" namespace validate { @@ -54,7 +54,9 @@ void validate_run() { } // Track savestates - if (savest::get_last_action() == savest::Action::Load) s_loaded_savestate = true; + // savest::get_last_action() == savest::Action::Load + // savest::interacted_with_state() + if (savest::state_changed_gameplay()) s_loaded_savestate = true; // Using dpad controls is disallowed bool dpad_down = From d647e4a666572a93ad44db5e0ffc3c0a36aafc9f Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 03:25:53 -0500 Subject: [PATCH 45/90] import menu_accel.cpp/h --- src/mods/menu_accel.cpp | 47 +++++++++++++++++++++++++++++++++++++++++ src/mods/menu_accel.h | 15 +++++++++++++ src/systems/main.cpp | 2 ++ 3 files changed, 64 insertions(+) create mode 100644 src/mods/menu_accel.cpp create mode 100644 src/mods/menu_accel.h diff --git a/src/mods/menu_accel.cpp b/src/mods/menu_accel.cpp new file mode 100644 index 00000000..c1a0a12b --- /dev/null +++ b/src/mods/menu_accel.cpp @@ -0,0 +1,47 @@ +#include "menu_accel.h" + +#include "../systems/pref.h" +#include "../utils/mode.h" +#include "../utils/patch.h" +#include "../utils/ppcutil.h" +#include "../utils/relutil.h" + +namespace menu_accel { + +void accelerate_pause_menu() { + // nop the instructions that check for R held down to accelerate the menu + patch::write_nop(relutil::relocate_addr(0x80273478)); // for up inputs + patch::write_nop(relutil::relocate_addr(0x802735C8)); // for down inputs +} + +void apply_vanilla_menu_behavior() { + // Otherwise, recover the original vanilla instructions + patch::write_word(relutil::relocate_addr(0x80273478), + PPC_INSTR_CMPWI(PPC_R3, 0)); // for up inputs + patch::write_word(relutil::relocate_addr(0x802735C8), + PPC_INSTR_RLWINM(PPC_R3, PPC_R3, 0, 0x16, 0x16, 0)); // for down inputs +} + +void tick() { + u8 pref = pref::get(pref::Pref::MenuAcceleration); + switch (MenuAccelOptions(pref)) { + case MenuAccelOptions::AlwaysEnabled: { + accelerate_pause_menu(); + break; + } + case MenuAccelOptions::OnlyInStory: { + if (mode::is_main_game_mode_story(mkb::main_game_mode)) { + accelerate_pause_menu(); + } else { + apply_vanilla_menu_behavior(); + } + break; + } + case MenuAccelOptions::Disabled: { + apply_vanilla_menu_behavior(); + break; + } + } +} + +} // namespace menu_accel \ No newline at end of file diff --git a/src/mods/menu_accel.h b/src/mods/menu_accel.h new file mode 100644 index 00000000..d11d7d62 --- /dev/null +++ b/src/mods/menu_accel.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace menu_accel { + +enum class MenuAccelOptions { + Disabled, + AlwaysEnabled, + OnlyInStory, +}; + +void tick(); + +} // namespace menu_accel diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 284e28da..708c7b16 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -32,6 +32,7 @@ #include "mods/iw.h" #include "mods/jump.h" #include "mods/marathon.h" +#include "mods/menu_accel.h" #include "mods/physics.h" #include "mods/savest_ui.h" #include "mods/scratch.h" @@ -102,6 +103,7 @@ static patch::Tramp s_process_inputs_tramp([]() { inputdisp::tick(); gotostory::tick(); cmseg::tick(); + menu_accel::tick(); banans::tick(); marathon::tick(); ballcolor::tick(); From 1334284ec8abe11716fc8cf6a15e5ae041dc2525 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 17:00:35 -0500 Subject: [PATCH 46/90] fix edge case bug with deathcounter + some cleanup --- src/mods/deathcounter.cpp | 4 +++- src/mods/storyreset.cpp | 2 +- src/mods/storytimer.cpp | 17 +---------------- src/systems/goal.cpp | 33 --------------------------------- 4 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index e84bc9a6..d827d802 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -74,7 +74,9 @@ bool should_count_as_normal_death() { (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); bool left_stage_without_clearing = mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; - bool died = mode::is_death_init(mkb::sub_mode); + // Need to also check the flag for death init submodes in case we fall out and let the animation + // play out (ie we enter 2 "death init" submodes before resuming gameplay) + bool died = mode::is_death_init(mkb::sub_mode) && s_can_incr_death_counter; return retried_without_clearing || left_stage_without_clearing || died; } diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index 94e4520a..67ca1359 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -149,7 +149,7 @@ void display_reset_run_message() { if (pref::get(pref::Pref::HideRunResetMessage)) { return; } - + // TODO: is all this really necessary? // If the hide run reset message pref is off, we still want to be "minimal" with displaying it // So, we only display a reset message if: // (1) it's a less obvious reset trigger (eg not go to story) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index b2539c5e..bfc91c30 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -295,6 +295,7 @@ bool should_not_display_timer_at_all() { // The only time we can ever display the timer outside of story mode is // when we fully exit game and the timer is still running if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { + // return get_loadless_time() == 0; return !is_run_active(); } // If in story, hide the timer if freecamming (if the pref is on) @@ -333,11 +334,6 @@ void disp() { "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); } -// for easier timer testing -// static patch::Tramp s_fade_screen_to_color_tramp; -/* static patch::Tramp - s_g_handle_storymode_stageselect_state_tramp; */ - static patch::Tramp s_g_handle_storymode_stageselect_state_tramp([]() { s_g_handle_storymode_stageselect_state_tramp.chain(); @@ -345,17 +341,6 @@ static patch::Tramp }); void init_main_game() { - /* patch::hook_function(s_fade_screen_to_color_tramp, mkb::fade_screen_to_color, - [](mkb::uint flags, u32 color, mkb::uint frames) { - // don't run for testing purposes - // s_fade_screen_to_color_tramp.dest(flags, color, frames); - }); */ - /* patch::hook_function(s_g_handle_storymode_stageselect_state_tramp, - mkb::g_handle_storymode_stageselect_state, []() { - s_g_handle_storymode_stageselect_state_tramp.dest(); - update_timers_on_10_ball_screen(mkb::g_storymode_stageselect_state); - // update_run_status(); - }); */ s_g_handle_storymode_stageselect_state_tramp.hook(); } diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index 52c24a23..e7c69c7f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -25,9 +25,6 @@ static bool s_goal_flag_exit_game; // Unset outside of exit game and game sce static bool s_goal_flag_last_stage; // Unset on spin in, gameplay pre tape break, and game scenario // main -// static patch::Tramp s_goal_tramp; -// static patch::Tramp s_mode_tick_tramp; - u8 get_frames_until_goal_submode() { return s_frames_until_goal_submode; } @@ -85,25 +82,6 @@ static patch::Tramp s_goal_tramp( return orig_result; }); -// When entering the goal, set our flags to true and start the frames until goal submode timer -/* bool did_ball_enter_goal_hook(mkb::Ball *ball, - int *out_stage_goal_idx, - int *out_itemgroup_id, - mkb::byte *out_goal_flags) { - bool result = s_goal_tramp.dest(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); - - if (result) { - set_goal_flags(); - s_frames_until_goal_submode = TIME_BETWEEN_TAPE_BREAK_AND_GOAL_SUBMODE; - } - // We don't need to check if we're paused because this function doesn't - // get run while paused - if (s_frames_until_goal_submode != 0) { - s_frames_until_goal_submode -= 1; - } - return result; -} */ - // We also need to handle properly zeroing the timer and unsetting our flags in the appropriate // submodes @@ -147,18 +125,7 @@ static patch::Tramp s_mode_tick_tramp([]() { unset_goal_flags(); }); -/* void mode_tick_hook() { - s_mode_tick_tramp.dest(); - // Run this after the game updates the submode - // This ensures that our flags that should get unset during a submode switch don't get delayed - // by a frame - reset_tape_break_counter(); - unset_goal_flags(); -} */ - void init() { - // patch::hook_function(s_goal_tramp, &mkb::did_ball_enter_goal, did_ball_enter_goal_hook); - // patch::hook_function(s_mode_tick_tramp, mkb::mode_tick, mode_tick_hook); s_goal_tramp.hook(); s_mode_tick_tramp.hook(); } From 68626985f14f3050306247083c4254b03ff2cd06 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 20:40:20 -0500 Subject: [PATCH 47/90] add totals row to breakdown screen also renamed get_timer_y_pos() to get_timer_row() --- src/mods/storytimer.cpp | 71 +++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index bfc91c30..4ff13ecc 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -22,6 +22,7 @@ constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr u16 TOTALS_ROW_LOCATION_X = 18; constexpr u16 STARTING_ROW = 2; constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; @@ -185,51 +186,52 @@ bool should_display_timer(TimerType type) { } case TimerOptions::DontShow: return false; + default: + // Unreachable + return false; } - - // Unreachable - return false; } // Doing this for now until a better display setup is figured out -u16 get_timer_y_pos(TimerType type) { // maybe rename to get_timer_row()? - u16 y_pos = STARTING_ROW; +u16 get_timer_row(TimerType type) { + u16 row = STARTING_ROW; bool is_displaying_death_counter = deathcounter::should_display_death_counter(); if (type == TimerType::Fullgame) { if (is_displaying_death_counter) { - y_pos++; + row++; } } else { // Segment timer if (should_display_timer(TimerType::Fullgame)) { - y_pos++; + row++; } if (is_displaying_death_counter) { - y_pos++; + row++; } } - return y_pos; + return row; } // Bundle up the info timerdisp::draw_timer uses into a struct for convenience struct TimerDisplayInfo { u16 pos_x; - u16 pos_y; + u16 row; u16 text_offset; }; TimerDisplayInfo get_timer_display_info(TimerType type) { if (type == TimerType::Fullgame) { - return {FULLGAME_TIMER_LOCATION_X, get_timer_y_pos(type), FULLGAME_TIMER_TEXT_OFFSET}; + return {FULLGAME_TIMER_LOCATION_X, get_timer_row(type), FULLGAME_TIMER_TEXT_OFFSET}; } else { - return {SEGMENT_TIMER_LOCATION_X, get_timer_y_pos(type), SEGMENT_TIMER_TEXT_OFFSET}; + return {SEGMENT_TIMER_LOCATION_X, get_timer_row(type), SEGMENT_TIMER_TEXT_OFFSET}; } } u16 get_current_segment_idx() { - // mkb::scen_info.world gets reset to 0 on the file screen, so the only case we ever need to - // worry about is if we fully exit game and are on the file screen with the timer still running + // mkb::scen_info.world gets reset to 0 on the file screen, so if we accidentally exit game to + // the menus, the only case where we can't use mkb::scen_info.world is when we are on the file + // screen with the timer still running if (mode::is_storymode_file_screen_main(mkb::scen_info) && get_loadless_time() != 0) { // The world we were on before exit-gaming return storyreset::get_active_world(); @@ -245,32 +247,33 @@ void draw_timers() { u32 loadless_time = get_loadless_time(); if (should_display_timer(TimerType::Fullgame)) { - timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.pos_y, fullgame_info.text_offset, + timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, "Time:", loadless_time, false, draw::WHITE); } - u16 world_idx = - get_current_segment_idx(); // index of the current world (between 0 and 9 inclusive) + u16 world_idx = get_current_segment_idx(); // index of the current world TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); if (should_display_timer(TimerType::Segment)) { - timerdisp::draw_timer(seg_info.pos_x, seg_info.pos_y, seg_info.text_offset, + timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); } } -// We only use this function for 0 <= row <= 9 +// We only use this function for 0 <= row <= 10 Vec2d get_breakdown_row_position(u16 row) { - u16 starting_row = get_timer_y_pos(TimerType::Segment); + u16 starting_row = get_timer_row(TimerType::Segment); float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); if (row < WORLD_COUNT - 1) { return {BREAKDOWN_ROW_LOCATION_X, pos_y}; - } else { // "W10" takes up more space than "Wk" for "1 <= k <= 9" - return {SEGMENT_TIMER_LOCATION_X, pos_y}; + } else if (row == WORLD_COUNT - 1) { // "W10" takes up more space than "Wk" for "1 <= k <= 9" + return {BREAKDOWN_ROW_LOCATION_X - draw::DEBUG_CHAR_WIDTH, pos_y}; + } else { // For the totals row + return {TOTALS_ROW_LOCATION_X, pos_y}; } } -void draw_breakdown_screen() { // TODO: totals row? +void draw_breakdown_screen() { // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" char split_buf[WORLD_COUNT][32] = {}; char seg_buf[WORLD_COUNT][32] = {}; @@ -289,6 +292,14 @@ void draw_breakdown_screen() { // TODO: totals row? draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); } + + // For the totals row + char totals_row_buf[32] = {}; + Vec2d totals_row_pos = get_breakdown_row_position(WORLD_COUNT); + u32 total_deaths = deathcounter::get_total_death_count(); + + mkb::sprintf(totals_row_buf, "Totals:%s (%d)", split_buf[WORLD_COUNT - 1], total_deaths); + draw::debug_text(totals_row_pos.x, totals_row_pos.y, draw::WHITE, "%s", totals_row_buf); } bool should_not_display_timer_at_all() { @@ -304,7 +315,7 @@ bool should_not_display_timer_at_all() { void disp() { if (should_not_display_timer_at_all()) { - // return; // testing, remember to uncomment + return; // testing, remember to uncomment } draw_timers(); @@ -316,22 +327,20 @@ void disp() { // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); // u32 new_val = val & 0x0000ffff; - u16 pos_y = get_timer_y_pos(TimerType::Segment); + /* u16 pos_y = get_timer_row(TimerType::Segment); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, - "Cur:", 60 * static_cast(savest::get_history().curr_frame_action), - true, draw::WHITE); + "Btw:", 60 * goal::is_between_worlds(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, - "Prv:", 60 * static_cast(savest::get_history().prev_frame_action), - true, draw::WHITE); + "Sub:", 60 * mkb::sub_mode, true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, - "gp:", 60 * savest::state_changed_gameplay(), true, draw::WHITE); + "Ext:", 60 * goal::get_goal_flag_exit_game(), true, draw::WHITE); timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, - "Int:", 60 * savest::interacted_with_state(), true, draw::WHITE); + "Lst:", 60 * goal::get_goal_flag_last_stage(), true, draw::WHITE); */ } static patch::Tramp From e2da8eb849306589e2e0bc8bf6fb702477d9d130 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 16 Aug 2026 22:43:00 -0500 Subject: [PATCH 48/90] small cleanup --- src/mods/deathcounter.cpp | 16 ++++++---------- src/mods/ilmark.cpp | 8 ++++---- src/systems/goal.cpp | 5 +++-- src/utils/mode.cpp | 4 ++++ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index d827d802..df0dc91f 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -62,7 +62,7 @@ void reset_death_counters() { // When we're done holding the savestate button/when gameplay resumes void update_flag_on_state_release() { // goal::is_gameplay_exact() && !libsavest::state_loaded_this_frame() - if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // PLACEHOLDER + if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { // As soon as we're done holding the load state button (or just any time we're controlling // the monkey on the stage), we're allowed to die s_can_incr_death_counter = true; @@ -71,7 +71,7 @@ void update_flag_on_state_release() { bool should_count_as_normal_death() { bool retried_without_clearing = - (mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter); + mode::is_spin_in_init(mkb::sub_mode) && s_can_incr_death_counter; bool left_stage_without_clearing = mode::is_stage_exit_submode(mkb::sub_mode) && s_can_incr_death_counter; // Need to also check the flag for death init submodes in case we fall out and let the animation @@ -90,11 +90,7 @@ void count_deaths() { s_can_incr_death_counter = false; } - if (should_count_as_normal_death()) { - increment_world_death_counter(); - } - - if (should_count_as_savestate_death()) { + if (should_count_as_normal_death() || should_count_as_savestate_death()) { increment_world_death_counter(); } } @@ -127,10 +123,10 @@ bool should_display_death_counter() { return goal::is_run_complete(); case DeathCounterOptions::DontShow: return false; + default: + // Unreachable + return false; } - - // Unreachable, silence compiler warning? - return false; } bool should_not_display_counter_at_all() { diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index d5acb3ff..5a109e95 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -50,7 +50,7 @@ void tick() { // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() if (savest::state_changed_gameplay()) { - // why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? + // TODO: why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? s_valid_run = false; } } @@ -65,10 +65,10 @@ bool show_in_story() { return !storyreset::all_loadless_timer_prefs_off() && goal::is_run_complete(); case StoryIlMarkSettings::AlwaysInStory: return true; + default: + // Unreachable + return false; } - - // Unreachable - return true; } bool is_ilmark_enabled() { diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index e7c69c7f..ef22ad2f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -90,9 +90,10 @@ void reset_tape_break_counter() { // after breaking the tape // This gets run after mkb::mode_tick ie after the game uppdates the submode // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, - // which means that at the end of the frame that we load the state, s_frames_until_goal_submode + // which means that at the end of any frame where we load a state, s_frames_until_goal_submode // will be 0 - // libsavest::state_loaded_this_frame() + // libsavest::state_loaded_this_frame() // savest::interacted_with_state() + // savest::loaded_state() // TODO: test that updating this didn't break anything! if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp index 7e442a66..7c087b66 100644 --- a/src/utils/mode.cpp +++ b/src/utils/mode.cpp @@ -251,6 +251,10 @@ bool is_storymode_file_screen_main(mkb::ScenInfo scen_info) { return (scen_info.mode == mkb::DMD_SCEN_LOADGAME_MAIN); } +bool is_storymode_file_screen(mkb::ScenInfo scen_info) { + return is_storymode_file_screen_init(scen_info) || is_storymode_file_screen_main(scen_info); +} + bool is_storymode_name_entry_init(mkb::ScenInfo scen_info) { // For some reason causes crashes when you check this during the file screen init return scen_info.mode == mkb::DMD_SCEN_ENTRY_INIT; From 23df108770d403245870856908f7404d515c8b3e Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 00:25:01 -0500 Subject: [PATCH 49/90] update draw_timer to use format_time_to_buffer + other various cleanup in the file --- src/utils/timerdisp.cpp | 231 +++++++++++++--------------------------- src/utils/timerdisp.h | 17 ++- 2 files changed, 81 insertions(+), 167 deletions(-) diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 5ef6658b..70f9bd15 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -11,6 +11,64 @@ static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; static constexpr s32 X = 378; static constexpr s32 Y = 24; +TimeComp get_time_components(u32 frames) { + u32 time_hours = frames / HOUR_FRAMES; + u32 time_minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; + u32 time_seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; + u32 time_centiseconds = (frames % SECOND_FRAMES) * 100 / 60; + return {time_hours, time_minutes, time_seconds, time_centiseconds}; +} + +void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type) { + TimeComp time = get_time_components(frames); + + switch (format_type) { + case TimeFormatType::SecondsOnly: { + u32 total_seconds = frames / SECOND_FRAMES; + mkb::sprintf(buffer, "%d.%02d", total_seconds, time.centiseconds); + break; + } + case TimeFormatType::HoursAlways: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + break; + } + case TimeFormatType::MinutesAlwaysLeadingZero: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d:%02d.%02d", time.minutes, time.seconds, + time.centiseconds); + } + break; + } + case TimeFormatType::MinimalLeading: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else if (time.minutes > 0) { + mkb::sprintf(buffer, "%d:%02d.%02d", time.minutes, time.seconds, time.centiseconds); + } else { + mkb::sprintf(buffer, "%d.%02d", time.seconds, time.centiseconds); + } + break; + } + case TimeFormatType::AlwaysLeadNonHours: { + if (time.hours > 0) { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + } else if (time.minutes > 0) { + mkb::sprintf(buffer, "%02d:%02d.%02d", time.minutes, time.seconds, + time.centiseconds); + } else { + mkb::sprintf(buffer, "%02d.%02d", time.seconds, time.centiseconds); + } + break; + } + } +} + s32 row_number_to_vertical_pos(u32 row_num) { return Y + 16 * row_num; } @@ -27,28 +85,17 @@ void draw_timer(u32 pos_x, if (!positive) frames = -frames; const char *sign = positive ? "" : "-"; - u32 hours = frames / HOUR_FRAMES; - u32 minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; - u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; - u32 centiseconds = (frames % SECOND_FRAMES) * 100 / 60; - - s32 A = pos_x; - s32 a = A + text_offset; - s32 b = row_number_to_vertical_pos(row); - - if (hours > 0 && !show_seconds) { - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%d:%02d:%02d.%02d", sign, hours, minutes, seconds, - centiseconds); - } else if (minutes > 0 && !show_seconds) { - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%02d:%02d.%02d", sign, minutes, seconds, centiseconds); + s32 pos_y = row_number_to_vertical_pos(row); + + char buf[16] = {}; + if (!show_seconds) { + format_time_to_buffer(buf, frames, TimeFormatType::AlwaysLeadNonHours); } else { - u32 total_seconds = - seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; - draw::debug_text(A, b, color, prefix); - draw::debug_text(a, b, color, "%s%02d.%02d", sign, total_seconds, centiseconds); + format_time_to_buffer(buf, frames, TimeFormatType::SecondsOnly); } + + draw::debug_text(pos_x, pos_y, color, prefix); + draw::debug_text(pos_x + text_offset, pos_y, color, "%s%s", sign, buf); mkb::reset_ui_widescreen_scale_mtx(); } @@ -81,14 +128,16 @@ void draw_subtick_timer(s32 frames, u32 milliseconds = ((frames % SECOND_FRAMES) * 100 + framesave) / 6; // 3 digit u32 extra = (((frames % SECOND_FRAMES) * 100 + framesave) * 10) / 6; // 4 digit - s32 y = Y + row * 16; + s32 y = row_number_to_vertical_pos(row); u32 total_seconds = seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; draw::debug_text(X, y, color, prefix); if (extra_precision) { - draw::debug_text(X + 48, y, color, "%s%02d.%04d", sign, total_seconds, extra); + draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%04d", sign, + total_seconds, extra); } else { - draw::debug_text(X + 48, y, color, "%s%02d.%03d", sign, total_seconds, milliseconds); + draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%03d", sign, + total_seconds, milliseconds); } mkb::reset_ui_widescreen_scale_mtx(); @@ -97,145 +146,11 @@ void draw_subtick_timer(s32 frames, void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { mkb::set_ui_widescreen_scale_mtx(320); - s32 y = Y + row * 16; + s32 y = row_number_to_vertical_pos(row); draw::debug_text(X, y, color, prefix); draw::debug_text(X + 48, y, color, "%2d%", fsave); mkb::set_ui_widescreen_scale_mtx(0); } -TimeComp format_time(u32 time) { // input time in frames - u32 time_hours = time / HOUR_FRAMES; - u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; - u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; - u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; - return {time_hours, time_minutes, time_seconds, time_centiseconds}; -} - -void format_time_to_buffer(char *buffer, - u32 time, - TimeFormatType format_type) { // time is in frames - TimeComp formatted_time = format_time(time); - - switch (format_type) { - case TimeFormatType::SECONDS_ONLY: { - u32 seconds = time / SECOND_FRAMES; - u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; - mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); - break; - } - case TimeFormatType::HOURS_ALWAYS: { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - break; - } - case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } - break; - } - case TimeFormatType::MINIMAL_LEADING: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - } -} - -TimeComp format_time(u32 time) { // input time in frames - u32 time_hours = time / HOUR_FRAMES; - u32 time_minutes = time % HOUR_FRAMES / MINUTE_FRAMES; - u32 time_seconds = time % MINUTE_FRAMES / SECOND_FRAMES; - u32 time_centiseconds = (time % SECOND_FRAMES) * 100 / 60; - return {time_hours, time_minutes, time_seconds, time_centiseconds}; -} - -void format_time_to_buffer(char *buffer, - u32 time, - TimeFormatType format_type) { // time is in frames - TimeComp formatted_time = format_time(time); - - switch (format_type) { - case TimeFormatType::SECONDS_ONLY: { - u32 seconds = time / SECOND_FRAMES; - u32 centiseconds = (time % SECOND_FRAMES) * 100 / 60; - mkb::sprintf(buffer, "%d.%02d", seconds, centiseconds); - break; - } - case TimeFormatType::HOURS_ALWAYS: { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - break; - } - case TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } - break; - } - case TimeFormatType::MINIMAL_LEADING: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%d:%02d.%02d", formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - case TimeFormatType::ALWAYS_LEAD_NON_HOURS: { - if (formatted_time.hours > 0) { - mkb::sprintf(buffer, "%d:%02d:%02d.%02d", formatted_time.hours, - formatted_time.minutes, formatted_time.seconds, - formatted_time.centiseconds); - } else if (formatted_time.minutes > 0) { - mkb::sprintf(buffer, "%02d:%02d.%02d", formatted_time.minutes, - formatted_time.seconds, formatted_time.centiseconds); - } else { - mkb::sprintf(buffer, "%02d.%02d", formatted_time.seconds, - formatted_time.centiseconds); - } - break; - } - } -} - } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 7a885f4b..918780d9 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -13,14 +13,16 @@ struct TimeComp { }; enum class TimeFormatType { - SECONDS_ONLY, // ss.cc - HOURS_ALWAYS, // h:mm:ss.cc or 0:mm:ss.cc - MINUTES_ALWAYS_LEADING_ZERO, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise - MINIMAL_LEADING, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, - // h:mm:ss.cc otherwise - ALWAYS_LEAD_NON_HOURS // mm:ss.cc if < 1 hour, ss.cc if < 1 min + SecondsOnly, // ss.cc + HoursAlways, // h:mm:ss.cc or 0:mm:ss.cc + MinutesAlwaysLeadingZero, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise + MinimalLeading, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, + // h:mm:ss.cc otherwise + AlwaysLeadNonHours // mm:ss.cc if < 1 hour, ss.cc if < 1 min }; +void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type); + s32 row_number_to_vertical_pos(u32 row_num); void draw_timer(u32 pos_x, u32 row, @@ -43,7 +45,4 @@ void draw_subtick_timer(s32 frames, bool extra_precision); void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color); -TimeComp format_time(u32 time); -void format_time_to_buffer(char *buffer, u32 time, TimeFormatType format_type); - } // namespace timerdisp From fa276816b4a23da1f504d84e95c4fea4940570a6 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 00:27:08 -0500 Subject: [PATCH 50/90] update run order in main storyreset, storytimer, and deathcounter ticks run after menu_impl and savestate related things now --- src/mods/deathcounter.cpp | 1 - src/mods/deathcounter.h | 7 ------- src/mods/storytimer.cpp | 4 ++-- src/mods/storytimer.h | 7 ------- src/systems/goal.cpp | 3 --- src/systems/main.cpp | 8 ++++---- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index df0dc91f..465a584a 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -81,7 +81,6 @@ bool should_count_as_normal_death() { } bool should_count_as_savestate_death() { - // return libsavest::state_loaded_this_frame() && s_can_incr_death_counter; return savest::loaded_state() && s_can_incr_death_counter; } diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h index 0672b58c..a093e863 100644 --- a/src/mods/deathcounter.h +++ b/src/mods/deathcounter.h @@ -4,13 +4,6 @@ namespace deathcounter { -/* enum class DeathCounterOptions { - DontShow = 0, - AlwaysShow = 1, - BetweenWorlds = 2, - EndOfRun = 3, -}; */ - u32 get_total_death_count(); u32 get_world_death_count(u16 world_idx); bool should_display_death_counter(); diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 4ff13ecc..3f07873f 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -284,9 +284,9 @@ void draw_breakdown_screen() { u32 world_deaths = deathcounter::get_world_death_count(idx); timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), - timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, - timerdisp::TimeFormatType::MINUTES_ALWAYS_LEADING_ZERO); + timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index 0de3105c..dec3a7aa 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -4,13 +4,6 @@ namespace storytimer { -/* enum class TimerOptions { - DontShow = 0, - AlwaysShow = 1, - BetweenWorlds = 2, - EndOfRun = 3, -}; */ - enum class TimerType { Fullgame, Segment, diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index ef22ad2f..a314e30f 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -92,9 +92,6 @@ void reset_tape_break_counter() { // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, // which means that at the end of any frame where we load a state, s_frames_until_goal_submode // will be 0 - // libsavest::state_loaded_this_frame() // savest::interacted_with_state() - // savest::loaded_state() - // TODO: test that updating this didn't break anything! if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || mode::is_spin_in_init(mkb::sub_mode)) { s_frames_until_goal_submode = 0; diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 708c7b16..c12ece72 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -91,12 +91,12 @@ static patch::Tramp s_process_inputs_tramp([]() { cardio::tick(); unlock::tick(); iw::tick(); - storyreset::tick(); - storytimer::tick(); - deathcounter::tick(); savest::tick(); savest_ui::tick(); menu_impl::tick(); // anything checking for pref changes should run after menu_impl::tick() + storyreset::tick(); + deathcounter::tick(); + storytimer::tick(); fallout::tick(); jump::tick(); // (edits physics preset) physics::tick(); // anything editing physics presets must run before physics::tick() @@ -136,8 +136,8 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); timer::disp(); iw::disp(); - storytimer::disp(); deathcounter::disp(); + storytimer::disp(); ilbattle::disp(); cmseg::disp(); inputdisp::disp(); From b644714714c9148b4b2cad5cdee8c43515b74b2e Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 17:59:51 -0500 Subject: [PATCH 51/90] add hide reset message button to deathcounter menu + small things also rename timer_group -> world_timer and make draw_timer arg signed --- src/mods/storytimer.cpp | 42 +++++++++++++++++++-------------------- src/mods/storytimer.h | 4 ++-- src/systems/menu_defn.cpp | 10 ++++++++++ src/utils/timerdisp.cpp | 2 +- src/utils/timerdisp.h | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 3f07873f..a3490d19 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -17,25 +17,25 @@ namespace storytimer { -constexpr u16 FULLGAME_TIMER_LOCATION_X = 18 + 24; +constexpr s16 FULLGAME_TIMER_LOCATION_X = 18 + 24; constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; -constexpr u16 SEGMENT_TIMER_LOCATION_X = 30 + 24; +constexpr s16 SEGMENT_TIMER_LOCATION_X = 30 + 24; constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; -constexpr u16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; -constexpr u16 TOTALS_ROW_LOCATION_X = 18; +constexpr s16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; +constexpr s16 TOTALS_ROW_LOCATION_X = 18; constexpr u16 STARTING_ROW = 2; constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; -static TimerGroup s_timer_group[WORLD_COUNT]; // timer info for each world +static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world // --- some getters that other files can use (if needed) --- -TimerGroup get_world_timer_info(u16 world_idx) { +WorldTimer get_world_timer_info(u16 world_idx) { // clamp for safety so we don't access outside the bounds of the array u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); - return s_timer_group[clamped_idx]; + return s_world_timer[clamped_idx]; } // Used to calculate our split times for the breakdown screen; also gives us a @@ -44,9 +44,9 @@ u32 get_split_timer_for_world(u16 world_idx) { u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); u32 prev_world_sum = 0; for (u16 k = 0; k < clamped_idx; k++) { - prev_world_sum += s_timer_group[k].full_world; + prev_world_sum += s_world_timer[k].full_world; } - return prev_world_sum + s_timer_group[clamped_idx].segment; + return prev_world_sum + s_world_timer[clamped_idx].segment; } u32 get_loadless_time() { @@ -62,7 +62,7 @@ bool is_run_active() { void reset_timer() { if (get_loadless_time() != 0) { for (u16 k = 0; k < WORLD_COUNT; k++) { - s_timer_group[k] = {}; + s_world_timer[k] = {}; } storyreset::reset_active_run_info(); storyreset::display_reset_run_message(); @@ -79,8 +79,8 @@ void update_timers_on_10_ball_screen(mkb::StoryModeStageSelectState state) { if (mkb::scen_info.world == k) { // World (k+1)'s timer if (mode::is_on_10_ball_spin_in(state) || mode::is_idle_on_10_ball_screen(state) || mode::has_selected_stage_on_10_ball_screen_init(state)) { - s_timer_group[k].full_world += 1; - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } @@ -91,8 +91,8 @@ void update_timers_while_paused_on_10_ball_screen() { for (u16 k = 0; k < WORLD_COUNT; k++) { if (mkb::scen_info.world == k) { if (mode::is_on_10_ball_screen(mkb::sub_mode, mkb::scen_info) && mode::is_paused()) { - s_timer_group[k].full_world += 1; - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } @@ -104,24 +104,24 @@ void update_timers_on_stage() { for (u16 k = 0; k < WORLD_COUNT; k++) { if (mkb::scen_info.world == k) { // World (k+1)'s timer if (mode::is_on_stage(mkb::sub_mode) || mode::is_story_exit_game(mkb::sub_mode)) { - s_timer_group[k].full_world += 1; + s_world_timer[k].full_world += 1; // Increment the segment timer until tape breek on the last stage of the world if (!goal::is_between_worlds()) { - s_timer_group[k].segment = s_timer_group[k].full_world; + s_world_timer[k].segment = s_world_timer[k].full_world; } } } } } -// for if we fully exit game by accident +// For if we fully exit game by accident void update_timers_on_menus() { if ((mode::is_sel_ngc(mkb::sub_mode) || mode::is_storymode_file_screen_main(mkb::scen_info)) && is_run_active()) { u8 active_world_idx = storyreset::get_active_world(); - s_timer_group[active_world_idx].full_world += 1; - s_timer_group[active_world_idx].segment = s_timer_group[active_world_idx].full_world; + s_world_timer[active_world_idx].full_world += 1; + s_world_timer[active_world_idx].segment = s_world_timer[active_world_idx].full_world; } } @@ -256,7 +256,7 @@ void draw_timers() { if (should_display_timer(TimerType::Segment)) { timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, - "Seg:", s_timer_group[world_idx].segment, false, draw::WHITE); + "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); } } @@ -285,7 +285,7 @@ void draw_breakdown_screen() { timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); - timerdisp::format_time_to_buffer(seg_buf[idx], s_timer_group[idx].segment, + timerdisp::format_time_to_buffer(seg_buf[idx], s_world_timer[idx].segment, timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index dec3a7aa..c07413ef 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -9,12 +9,12 @@ enum class TimerType { Segment, }; -struct TimerGroup { +struct WorldTimer { u32 segment; // the time taken to complete a world up until tape break on the last stage u32 full_world; // the time taken to complete a world until the fade to white on the last stage }; -TimerGroup get_world_timer_info(u16 world_idx); +WorldTimer get_world_timer_info(u16 world_idx); u32 get_split_timer_for_world(u16 world_idx); u32 get_loadless_time(); diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index ce7cc4d7..c0f609d1 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1072,6 +1072,7 @@ static Widget s_loadless_timers_widgets[] = { .type = WidgetType::Checkbox, .checkbox = { + // Applies to both loadless timer and deathcounter (they share the same pref) .label = "Hide Reset Message", .pref = pref::Pref::HideRunResetMessage, }, @@ -1104,6 +1105,15 @@ static Widget s_deathcounter_widgets[] = { .pref = pref::Pref::CountFirstStageDeaths, }, }, + { + .type = WidgetType::Checkbox, + .checkbox = + { + // Applies to both loadless timer and deathcounter (they share the same pref) + .label = "Hide Reset Message", + .pref = pref::Pref::HideRunResetMessage, + }, + }, }; static Widget s_timers_widgets[] = { diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 70f9bd15..836e4105 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -73,7 +73,7 @@ s32 row_number_to_vertical_pos(u32 row_num) { return Y + 16 * row_num; } -void draw_timer(u32 pos_x, +void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 918780d9..98dc5fc4 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -24,7 +24,7 @@ enum class TimeFormatType { void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type); s32 row_number_to_vertical_pos(u32 row_num); -void draw_timer(u32 pos_x, +void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, From ed5aaaa88a8479127b5f6286184525d2d25a0253 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 19:17:51 -0500 Subject: [PATCH 52/90] cleanup storyreset some --- src/mods/storyreset.cpp | 34 +++++++++++----------------------- src/mods/storyreset.h | 11 ++++------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index 67ca1359..d77371a7 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -96,8 +96,6 @@ bool should_reset_run() { should_reset_completed_run(); } -// TODO: reset run if selecting active file and active file has 0 stages cleared? - void tick() { if (mode::is_main_game_mode_story(mkb::main_game_mode) && mode::is_story_exit_game_init(mkb::sub_mode)) { @@ -105,6 +103,8 @@ void tick() { } } +// --- stuff for handling displaying the run reset message --- + bool all_loadless_timer_prefs_off() { StoryDisplayOptions fullgame_pref = StoryDisplayOptions(pref::get(pref::Pref::FullgameTimerOptions)); @@ -136,12 +136,8 @@ bool displaying_on_menus_during_accidental_exit_game() { death_counter_pref == StoryDisplayOptions::AlwaysShow; } -bool run_active_but_not_displaying_on_menus() { - return !displaying_on_menus_during_accidental_exit_game() && - !(all_loadless_timer_prefs_off() && death_counter_pref_off()); -} - bool is_silent_reset_type() { + // ie, any reset that's not triggered by gotostory or on the menu after completing a run return should_reset_on_file_screen() || is_on_wrong_menu(); } @@ -149,15 +145,16 @@ void display_reset_run_message() { if (pref::get(pref::Pref::HideRunResetMessage)) { return; } - // TODO: is all this really necessary? + // If the hide run reset message pref is off, we still want to be "minimal" with displaying it // So, we only display a reset message if: - // (1) it's a less obvious reset trigger (eg not go to story) - // (2) at least one timer/death counter pref is on (don't bother displaying a reset message if - // everything is turned off) - // (3) the player has "always show" turned on for at least one of the timers/death counter. The - // idea here is if always show is turned on and if the timer gets zero-ed, it's obvious that the - // run was reset and no message needs to be displayed + // (1) it's a less obvious reset trigger (ie not go to story, or if we've already completed the + // run) + // (2) at least one timer/death counter pref is on (don't bother displaying a reset message + // if everything is turned off) + // (3) the player has "always show" turned on for at least one of + // the timers/death counter. The idea here is if always show is turned on and if the timer gets + // zero-ed, it's obvious that the run was reset and no message needs to be displayed if (!is_silent_reset_type()) { return; @@ -174,13 +171,4 @@ void display_reset_run_message() { draw::notify(draw::WHITE, "Run Was Reset"); } -/* if ((!is_silent_reset_type() || !run_active_but_not_displaying_on_menus())) { - return; - } */ - -// && death_counter_pref_off() -/* if (all_loadless_timer_prefs_off()) { - return; -} */ - } // namespace storyreset \ No newline at end of file diff --git a/src/mods/storyreset.h b/src/mods/storyreset.h index a73ea431..9239b48b 100644 --- a/src/mods/storyreset.h +++ b/src/mods/storyreset.h @@ -4,19 +4,16 @@ namespace storyreset { -// Does it make more sense to move this enum class to pref.h? -// We need this enum class in storyreset.cpp, but we also need it in both deathcounter.cpp and -// storytimer.cpp +// The fullgame loadless timer and death counter share the same display options. The loadless +// segment timer options are a subset of this enum class (which consist of everything here except an +// EndOfRun setting), and the segment timer options show up in the same order in the menu as the +// corresponding fullgame one, so we don't need to make a separate enum class for it enum class StoryDisplayOptions { DontShow, AlwaysShow, BetweenWorlds, EndOfRun, }; -// The fullgame loadless timer and death counter share the same display options. The loadless -// segment timer options are a subset of this enum class (everything except an EndOfRun setting), -// and the segment timer options show up in the same order in the menu as the corresponding fullgame -// one, so we don't need to make a separate enum class for it bool is_run_active(); void set_run_active_status(bool is_active); From 1a80d44d90f0072472ecb4b29056310aa5fa69c4 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 17 Aug 2026 19:18:46 -0500 Subject: [PATCH 53/90] revert to old validate/ilmark behavior on creating a state for 1f --- src/mods/ilmark.cpp | 5 ++--- src/mods/validate.cpp | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index 5a109e95..1c04738b 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -47,10 +47,9 @@ void tick() { s_valid_run = false; } - // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() - if (savest::state_changed_gameplay()) { - // TODO: why do we need this if validate_attempt() gets run in mkb::smd_game_play_tick? + // savest::state_changed_gameplay() + if (savest::interacted_with_state()) { s_valid_run = false; } } diff --git a/src/mods/validate.cpp b/src/mods/validate.cpp index 80ba5ff0..aaa250bb 100644 --- a/src/mods/validate.cpp +++ b/src/mods/validate.cpp @@ -54,9 +54,9 @@ void validate_run() { } // Track savestates - // savest::get_last_action() == savest::Action::Load // savest::interacted_with_state() - if (savest::state_changed_gameplay()) s_loaded_savestate = true; + // savest::state_changed_gameplay() + if (savest::interacted_with_state()) s_loaded_savestate = true; // Using dpad controls is disallowed bool dpad_down = From 46c8d170696901f6ac8b25328e4c55edb9a94cbc Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 18 Aug 2026 00:13:50 -0500 Subject: [PATCH 54/90] make split_buf, seg_buf arrays smaller + remove testing comments --- src/mods/deathcounter.cpp | 14 -------------- src/mods/storytimer.cpp | 26 +++----------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 465a584a..5b2f4a4b 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -9,7 +9,6 @@ #include "../utils/macro_utils.h" #include "../utils/mode.h" #include "../utils/patch.h" -#include "../utils/timerdisp.h" // for testing #include "freecam.h" #include "storyreset.h" @@ -147,19 +146,6 @@ void disp() { draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", get_total_death_count()); } - - /* u8 pos; - if (should_display_death_counter()) { - pos = 2; - } else { - pos = 0; - } - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 1 + pos, 44, - "Dbg:", 60 * storyreset::is_run_active(), true, draw::WHITE); - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 2 + pos, 44, "Sub:", 60 * mkb::sub_mode, true, - draw::WHITE); - timerdisp::draw_timer(COUNTER_DISPLAY_X_POS, 3 + pos, 44, - "Gol:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); */ } } // namespace deathcounter diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index a3490d19..5e99b9b1 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -3,9 +3,7 @@ #include "../mkb/mkb.h" #include "../systems/goal.h" -#include "../systems/pad.h" // for testing #include "../systems/pref.h" -#include "../systems/savest.h" // for testing #include "../utils/draw.h" #include "../utils/macro_utils.h" #include "../utils/mode.h" @@ -274,9 +272,9 @@ Vec2d get_breakdown_row_position(u16 row) { } void draw_breakdown_screen() { - // Format: "Wk: split k-1 time (segment k-1 time) (world k deaths)" - char split_buf[WORLD_COUNT][32] = {}; - char seg_buf[WORLD_COUNT][32] = {}; + // Format: "Wk: world k split time (world k segment time) (world k deaths)" + char split_buf[WORLD_COUNT][16] = {}; + char seg_buf[WORLD_COUNT][16] = {}; char row_info_buf[WORLD_COUNT][32] = {}; for (u16 idx = 0; idx < WORLD_COUNT; idx++) { @@ -323,24 +321,6 @@ void disp() { if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { draw_breakdown_screen(); } - - // u32 val = patch::view_word(reinterpret_cast(0x8090dbd0)); - // u32 new_val = val & 0x0000ffff; - - /* u16 pos_y = get_timer_row(TimerType::Segment); - - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 1, SEGMENT_TIMER_TEXT_OFFSET, - "Fra:", 60 * goal::get_frames_until_goal_submode(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 2, SEGMENT_TIMER_TEXT_OFFSET, - "Pxt:", 60 * goal::is_postgoal_exact(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 3, SEGMENT_TIMER_TEXT_OFFSET, - "Btw:", 60 * goal::is_between_worlds(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 4, SEGMENT_TIMER_TEXT_OFFSET, - "Sub:", 60 * mkb::sub_mode, true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 5, SEGMENT_TIMER_TEXT_OFFSET, - "Ext:", 60 * goal::get_goal_flag_exit_game(), true, draw::WHITE); - timerdisp::draw_timer(SEGMENT_TIMER_LOCATION_X, pos_y + 6, SEGMENT_TIMER_TEXT_OFFSET, - "Lst:", 60 * goal::get_goal_flag_last_stage(), true, draw::WHITE); */ } static patch::Tramp From 737c3e338fd1a8b8223493d09c2e090953109160 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Wed, 19 Aug 2026 04:36:59 -0500 Subject: [PATCH 55/90] super duper wip idea dump for display stuff --- src/systems/textinfo.cpp | 136 ++++++++++++++++++++++++++++++++++++ src/systems/textinfo.h | 39 +++++++++++ src/systems/textmanager.cpp | 12 ++++ src/systems/textmanager.h | 10 +++ 4 files changed, 197 insertions(+) create mode 100644 src/systems/textinfo.cpp create mode 100644 src/systems/textinfo.h create mode 100644 src/systems/textmanager.cpp create mode 100644 src/systems/textmanager.h diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp new file mode 100644 index 00000000..bd0d115c --- /dev/null +++ b/src/systems/textinfo.cpp @@ -0,0 +1,136 @@ +#include "textinfo.h" + +#include +#include "../mkb/mkb.h" +#include "../systems/pref.h" +#include "../utils/draw.h" +#include "../utils/macro_utils.h" +#include "../utils/timerdisp.h" + +namespace textinfo { + +// wip/messy/underdeveloped ideas etc +// textinfo will send its stuff to textmanager + +struct QueueObject { + DisplayGroup group; + char *text; +}; + +constexpr u16 MAX_PRIORITY = 5; +constexpr u16 MAX_QUEUE_LENGTH = 21; // 13 loadless timer // 1 death counter // 4 il battle // 2 + // rta/pause timer // 1 framesave // +constexpr DisplayGroup EMPTY_GROUP = {Slot::Unused, 0}; +constexpr QueueObject EMPTY_QUEUE_OBJECT = {EMPTY_GROUP, nullptr}; + +// static QueueObject s_text_queue[] = {[0 ... MAX_QUEUE_LENGTH] = EMPTY_QUEUE_OBJECT}; + +static QueueObject s_text_queue[MAX_QUEUE_LENGTH]; + +void initialize_queue() { // I guess + for (u8 k = 0; k < MAX_QUEUE_LENGTH; k++) { + s_text_queue[k] = EMPTY_QUEUE_OBJECT; + } +} + +QueueObject get_queue_obj(u16 idx) { + u16 clamped_idx = MIN(idx, MAX_QUEUE_LENGTH - 1); + return s_text_queue[clamped_idx]; +} + +DisplayGroup module_to_display_group(Module module) { + switch (module) { + case Module::IlBattle: + return {Slot::Left, 0}; + case Module::DeathCounter: + return {Slot::Left, 1}; + case Module::LoadlessTimer: + return {Slot::Left, 2}; + case Module::IwTimer: + return {Slot::Right, 0}; + case Module::CmSeg: + return {Slot::Right, 1}; + case Module::RtaTimer: + return {Slot::Right, 2}; + case Module::FrameSave: + return {Slot::Right, 3}; + default: + return {Slot::Unused, 0}; + } +} + +s32 get_slot_x_pos(Slot slot) { + switch (slot) { + case Slot::Left: + return 0; + case Slot::Right: + return 0; + default: + return 0; + } +} + +/* void reserve(Module module) { +} */ + +// char *format, ... +void queue(Module module, char *text) { + for (u16 k = 0; k < LEN(s_text_queue); k++) { + if (s_text_queue[k].text != nullptr) { + DisplayGroup group = module_to_display_group(module); + // char *text; + // mkb::sprintf(text, format, ...); + s_text_queue[k] = {group, text}; + } + } +} + +bool does_queue_obj_belong_to_display_group(DisplayGroup group, QueueObject obj) { + return obj.group.slot == group.slot && obj.group.priority == group.priority; +} + +// TODO: display ids attached to each object in a display group, so we can do hardcodes by id (like +// color, etc) + +// maybe? +struct DisplayObject { + DisplayGroup group; + u8 id; +}; + +struct TextInfo { + GXColor color; + char *buffer; +}; + +// Allows us to specify color and modify the text on a per-id basis +/* TextInfo get_display_object_text_info(DisplayObject obj) { +} */ + +void draw_displays() { + for (u8 k = 0; k < LEN(s_slot_list); k++) { // First iterate through each slot + u8 row = 0; + for (u8 j = 0; j < MAX_PRIORITY; j++) { // Iterate through each priority level + DisplayGroup group = {s_slot_list[k], j}; + for (u8 m = 0; m < MAX_QUEUE_LENGTH; m++) { + if (!does_queue_obj_belong_to_display_group(group, s_text_queue[m])) { + continue; + } + s32 x = get_slot_x_pos(s_slot_list[k]); + s32 y = timerdisp::row_number_to_vertical_pos(row); + + draw::debug_text(x, y, draw::WHITE, s_text_queue[m].text); + row++; + } + } + } +} + +void init() { +} +void tick() { +} +void disp() { +} + +} // namespace textinfo \ No newline at end of file diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h new file mode 100644 index 00000000..883f9665 --- /dev/null +++ b/src/systems/textinfo.h @@ -0,0 +1,39 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace textinfo { + +enum class Slot { + Unused, + Left, + Right, +}; + +static const Slot s_slot_list[]{ + Slot::Left, + Slot::Right, +}; + +struct DisplayGroup { + Slot slot; + u8 priority; +}; + +// maybe call it DisplayModule since a module can technically decide to draw to multiple slots, so +// we could have like ModuleALeft, ModuleARight, ModuleB, etc +enum class Module { + RtaTimer, + IwTimer, + CmSeg, + FrameSave, + IlBattle, + DeathCounter, + LoadlessTimer, +}; + +void init(); +void tick(); +void disp(); + +} // namespace textinfo diff --git a/src/systems/textmanager.cpp b/src/systems/textmanager.cpp new file mode 100644 index 00000000..4918b360 --- /dev/null +++ b/src/systems/textmanager.cpp @@ -0,0 +1,12 @@ +#include "textmanager.h" + +namespace textmanager { + +void init() { +} +void tick() { +} +void disp() { +} + +} // namespace textmanager \ No newline at end of file diff --git a/src/systems/textmanager.h b/src/systems/textmanager.h new file mode 100644 index 00000000..857d9ab9 --- /dev/null +++ b/src/systems/textmanager.h @@ -0,0 +1,10 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace textmanager { +void init(); +void tick(); +void disp(); + +} // namespace textmanager From fe291ae5f42b0d70044d32c26d95863e9ada1407 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Wed, 19 Aug 2026 21:25:16 -0500 Subject: [PATCH 56/90] fix includes and various small issues renamed format_time_to_buffer to _format_time(), renamed TimeFormatType to TimeFormat, moved some enum classes out of headers --- src/mods/deathcounter.cpp | 17 ++++++++--------- src/mods/deathcounter.h | 2 +- src/mods/menu_accel.cpp | 16 +++++++++++----- src/mods/menu_accel.h | 6 ------ src/mods/storyreset.cpp | 8 ++++---- src/mods/storyreset.h | 2 +- src/mods/storytimer.cpp | 26 +++++++++++++------------- src/mods/storytimer.h | 2 +- src/systems/goal.cpp | 8 ++++---- src/systems/goal.h | 8 ++------ src/systems/menu_defn.cpp | 26 ++++++++++++-------------- src/utils/mode.cpp | 2 +- src/utils/mode.h | 2 +- src/utils/timerdisp.cpp | 18 +++++++++--------- src/utils/timerdisp.h | 6 +++--- 15 files changed, 71 insertions(+), 78 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 5b2f4a4b..f8394761 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -1,16 +1,15 @@ #include "deathcounter.h" -#include "../mkb/mkb.h" - -#include "../systems/goal.h" -#include "../systems/pref.h" -#include "../systems/savest.h" -#include "../utils/draw.h" -#include "../utils/macro_utils.h" -#include "../utils/mode.h" -#include "../utils/patch.h" +#include "mkb/mkb.h" + #include "freecam.h" #include "storyreset.h" +#include "systems/goal.h" +#include "systems/pref.h" +#include "systems/savest.h" +#include "utils/draw.h" +#include "utils/macro_utils.h" +#include "utils/mode.h" namespace deathcounter { diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h index a093e863..1070cdb5 100644 --- a/src/mods/deathcounter.h +++ b/src/mods/deathcounter.h @@ -1,6 +1,6 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace deathcounter { diff --git a/src/mods/menu_accel.cpp b/src/mods/menu_accel.cpp index c1a0a12b..9841cfa8 100644 --- a/src/mods/menu_accel.cpp +++ b/src/mods/menu_accel.cpp @@ -1,13 +1,19 @@ #include "menu_accel.h" -#include "../systems/pref.h" -#include "../utils/mode.h" -#include "../utils/patch.h" -#include "../utils/ppcutil.h" -#include "../utils/relutil.h" +#include "systems/pref.h" +#include "utils/mode.h" +#include "utils/patch.h" +#include "utils/ppcutil.h" +#include "utils/relutil.h" namespace menu_accel { +enum class MenuAccelOptions { + Disabled, + AlwaysEnabled, + OnlyInStory, +}; + void accelerate_pause_menu() { // nop the instructions that check for R held down to accelerate the menu patch::write_nop(relutil::relocate_addr(0x80273478)); // for up inputs diff --git a/src/mods/menu_accel.h b/src/mods/menu_accel.h index d11d7d62..57876bd7 100644 --- a/src/mods/menu_accel.h +++ b/src/mods/menu_accel.h @@ -4,12 +4,6 @@ namespace menu_accel { -enum class MenuAccelOptions { - Disabled, - AlwaysEnabled, - OnlyInStory, -}; - void tick(); } // namespace menu_accel diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index d77371a7..361c9852 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -1,10 +1,10 @@ #include "storyreset.h" -#include "../systems/goal.h" -#include "../systems/pref.h" -#include "../utils/draw.h" -#include "../utils/mode.h" #include "gotostory.h" +#include "systems/goal.h" +#include "systems/pref.h" +#include "utils/draw.h" +#include "utils/mode.h" namespace storyreset { diff --git a/src/mods/storyreset.h b/src/mods/storyreset.h index 9239b48b..c6ec1106 100644 --- a/src/mods/storyreset.h +++ b/src/mods/storyreset.h @@ -1,6 +1,6 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace storyreset { diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 5e99b9b1..58a4c507 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -1,17 +1,17 @@ #include "storytimer.h" -#include "../mkb/mkb.h" - -#include "../systems/goal.h" -#include "../systems/pref.h" -#include "../utils/draw.h" -#include "../utils/macro_utils.h" -#include "../utils/mode.h" -#include "../utils/patch.h" -#include "../utils/timerdisp.h" +#include "mkb/mkb.h" + #include "deathcounter.h" #include "freecam.h" #include "storyreset.h" +#include "systems/goal.h" +#include "systems/pref.h" +#include "utils/draw.h" +#include "utils/macro_utils.h" +#include "utils/mode.h" +#include "utils/patch.h" +#include "utils/timerdisp.h" namespace storytimer { @@ -281,10 +281,10 @@ void draw_breakdown_screen() { Vec2d pos = get_breakdown_row_position(idx); u32 world_deaths = deathcounter::get_world_death_count(idx); - timerdisp::format_time_to_buffer(split_buf[idx], get_split_timer_for_world(idx), - timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); - timerdisp::format_time_to_buffer(seg_buf[idx], s_world_timer[idx].segment, - timerdisp::TimeFormatType::MinutesAlwaysLeadingZero); + timerdisp::format_time(split_buf[idx], get_split_timer_for_world(idx), + timerdisp::TimeFormat::MinutesAlwaysLeadingZero); + timerdisp::format_time(seg_buf[idx], s_world_timer[idx].segment, + timerdisp::TimeFormat::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index c07413ef..558480fe 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -1,6 +1,6 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace storytimer { diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index a314e30f..84d41318 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -1,9 +1,9 @@ #include "goal.h" -#include "../mkb/mkb.h" -#include "../systems/savest.h" -#include "../utils/mode.h" -#include "../utils/patch.h" +#include "mkb/mkb.h" +#include "systems/savest.h" +#include "utils/mode.h" +#include "utils/patch.h" namespace goal { diff --git a/src/systems/goal.h b/src/systems/goal.h index 223cedce..6eb1d1fb 100644 --- a/src/systems/goal.h +++ b/src/systems/goal.h @@ -1,16 +1,12 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace goal { -u8 get_frames_until_goal_submode(); -bool get_goal_flag_game_return(); -bool get_goal_flag_exit_game(); -bool get_goal_flag_last_stage(); - bool is_postgoal_exact(); bool is_gameplay_exact(); + bool is_between_worlds(); bool is_run_complete(); diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index c0f609d1..8af4347c 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1024,19 +1024,17 @@ static Widget s_cm_seg_widgets[] = { }, }; -// put my stuff here - static const char *FULLGAME_TIMER_OPTIONS[] = { - "Don't show", - "Always show", - "Between worlds", - "End of run", + "Don't Show", + "Always Show", + "Between Worlds", + "End of Run", }; static const char *SEGMENT_TIMER_OPTIONS[] = { - "Don't show", - "Always show", - "Between worlds", + "Don't Show", + "Always Show", + "Between Worlds", }; static Widget s_loadless_timers_widgets[] = { @@ -1080,10 +1078,10 @@ static Widget s_loadless_timers_widgets[] = { }; static const char *DEATH_COUNTER_OPTIONS[] = { - "Don't show", - "Always show", - "Between worlds", - "End of run", + "Don't Show", + "Always Show", + "Between Worlds", + "End of Run", }; static Widget s_deathcounter_widgets[] = { @@ -1485,7 +1483,7 @@ static Widget s_savestate_widgets[] = { }, }; -static const char *MENU_ACCEL_OPTIONS[] = {"Disabled", "Always enabled", "Only in story"}; +static const char *MENU_ACCEL_OPTIONS[] = {"Disabled", "Always Enabled", "Only in Story"}; static Widget s_tools_widgets[] = { { diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp index 7c087b66..94f39911 100644 --- a/src/utils/mode.cpp +++ b/src/utils/mode.cpp @@ -1,7 +1,7 @@ #include "mode.h" // #include "../internal/relutil.h" -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace mode { diff --git a/src/utils/mode.h b/src/utils/mode.h index c6ac5a60..702e51e4 100644 --- a/src/utils/mode.h +++ b/src/utils/mode.h @@ -1,6 +1,6 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace mode { diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 836e4105..fa73c271 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -1,6 +1,6 @@ #include "timerdisp.h" -#include "../utils/draw.h" +#include "utils/draw.h" namespace timerdisp { @@ -19,21 +19,21 @@ TimeComp get_time_components(u32 frames) { return {time_hours, time_minutes, time_seconds, time_centiseconds}; } -void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type) { +void format_time(char *buffer, u32 frames, TimeFormat format_type) { TimeComp time = get_time_components(frames); switch (format_type) { - case TimeFormatType::SecondsOnly: { + case TimeFormat::SecondsOnly: { u32 total_seconds = frames / SECOND_FRAMES; mkb::sprintf(buffer, "%d.%02d", total_seconds, time.centiseconds); break; } - case TimeFormatType::HoursAlways: { + case TimeFormat::HoursAlways: { mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, time.centiseconds); break; } - case TimeFormatType::MinutesAlwaysLeadingZero: { + case TimeFormat::MinutesAlwaysLeadingZero: { if (time.hours > 0) { mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, time.centiseconds); @@ -43,7 +43,7 @@ void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type) } break; } - case TimeFormatType::MinimalLeading: { + case TimeFormat::MinimalLeading: { if (time.hours > 0) { mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, time.centiseconds); @@ -54,7 +54,7 @@ void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type) } break; } - case TimeFormatType::AlwaysLeadNonHours: { + case TimeFormat::AlwaysLeadNonHours: { if (time.hours > 0) { mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, time.centiseconds); @@ -89,9 +89,9 @@ void draw_timer(s32 pos_x, char buf[16] = {}; if (!show_seconds) { - format_time_to_buffer(buf, frames, TimeFormatType::AlwaysLeadNonHours); + format_time(buf, frames, TimeFormat::AlwaysLeadNonHours); } else { - format_time_to_buffer(buf, frames, TimeFormatType::SecondsOnly); + format_time(buf, frames, TimeFormat::SecondsOnly); } draw::debug_text(pos_x, pos_y, color, prefix); diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 98dc5fc4..1f492432 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -1,6 +1,6 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" namespace timerdisp { @@ -12,7 +12,7 @@ struct TimeComp { u32 centiseconds; }; -enum class TimeFormatType { +enum class TimeFormat { SecondsOnly, // ss.cc HoursAlways, // h:mm:ss.cc or 0:mm:ss.cc MinutesAlwaysLeadingZero, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise @@ -21,7 +21,7 @@ enum class TimeFormatType { AlwaysLeadNonHours // mm:ss.cc if < 1 hour, ss.cc if < 1 min }; -void format_time_to_buffer(char *buffer, u32 frames, TimeFormatType format_type); +void format_time(char *buffer, u32 frames, TimeFormat format_type); s32 row_number_to_vertical_pos(u32 row_num); void draw_timer(s32 pos_x, From 58810afd13e46db588700f62cfe2713bc879d913 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Thu, 20 Aug 2026 14:24:24 -0500 Subject: [PATCH 57/90] savest: remove history struct --- src/systems/savest.cpp | 40 +++++++--------------------------------- src/systems/savest.h | 11 +---------- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index 91d71c37..aed4c3ae 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -32,7 +32,7 @@ struct SaveState { u32 s_timestamp; SaveState s_states[SLOT_COUNT]; -History s_history; +Action s_last_action; // // Hooks @@ -50,7 +50,7 @@ static patch::Tramp s_set_minimap_mode_tramp([](mkb::Mini // Prevent sound effects from playing while loading states static patch::Tramp s_soundreq_tramp([](u32 id) { - if (s_history.curr_frame_action != Action::Load) { + if (s_last_action != Action::Load) { s_soundreq_tramp.chain(id); } }); @@ -292,7 +292,7 @@ SaveResult save(u32 slot) { state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; - s_history.curr_frame_action = Action::Save; + s_last_action = Action::Save; return SaveResult::Ok; } @@ -351,7 +351,7 @@ LoadResult load(u32 slot) { } state->timestamp = s_timestamp; - s_history.curr_frame_action = Action::Load; + s_last_action = Action::Load; return LoadResult::Ok; } @@ -378,9 +378,7 @@ u32 get_timestamp(u32 slot) { } void tick() { - // Rotate history - s_history.prev_frame_action = s_history.curr_frame_action; - s_history.curr_frame_action = Action::None; + s_last_action = Action::None; s_timestamp++; @@ -395,32 +393,8 @@ bool is_enabled() { return pref::get(pref::Pref::Savestates) && !pref::get(pref::Pref::Freecam); } -History get_history() { - return s_history; -} - -Action get_curr_frame_action() { - return s_history.curr_frame_action; -} - -bool state_changed_gameplay() { - return s_history.curr_frame_action == Action::Load; -} - -// Note: checking s_history.curr_frame_action == Action::Load is different from -// interacted_with_state() -// With the former, pressing the create state button for 1 frame won't be detected - -bool interacted_with_state() { - // Returns true if we created a state this frame, or if we continue to hold the create state - // button, or if we load state - return s_history.curr_frame_action != Action::None; -} - -bool loaded_state() { - // Won't return true if we create a state, only if we load one - return s_history.curr_frame_action == Action::Load && - s_history.prev_frame_action == Action::None; +Action get_last_action() { + return s_last_action; } } // namespace savest diff --git a/src/systems/savest.h b/src/systems/savest.h index 8540faec..f8516335 100644 --- a/src/systems/savest.h +++ b/src/systems/savest.h @@ -36,11 +36,6 @@ enum class Action { Save, }; -struct History { - Action curr_frame_action; - Action prev_frame_action; -}; - void init(); void tick(); @@ -51,10 +46,6 @@ bool is_empty(u32 slot); u32 get_timestamp(u32 slot); bool is_enabled(); -History get_history(); -Action get_curr_frame_action(); -bool state_changed_gameplay(); -bool interacted_with_state(); -bool loaded_state(); +Action get_last_action(); } // namespace savest From 91b7be95e23a859d2641dc401fb9c49c7f4a8b0d Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Thu, 20 Aug 2026 14:32:17 -0500 Subject: [PATCH 58/90] deathcounter, goal, ilmark, validate use updated state api --- src/mods/deathcounter.cpp | 16 ++++++++++++---- src/mods/ilmark.cpp | 4 +--- src/mods/validate.cpp | 6 +++--- src/systems/goal.cpp | 4 ++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index f8394761..00f2ca4d 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -20,6 +20,7 @@ constexpr u16 COUNTER_NUMBER_X_POS = COUNTER_DISPLAY_X_POS + 7 * draw::DEBUG_CHA constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; static u32 s_world_death_count[WORLD_COUNT] = {}; +static savest::Action s_previous_frame_action = savest::Action::None; // Flag to determine when we should/shouldn't increment the death counter static bool s_can_incr_death_counter = false; @@ -57,10 +58,14 @@ void reset_death_counters() { reset_flag(); } +bool loaded_state() { + return savest::get_last_action() == savest::Action::Load && + s_previous_frame_action == savest::Action::None; +} + // When we're done holding the savestate button/when gameplay resumes void update_flag_on_state_release() { - // goal::is_gameplay_exact() && !libsavest::state_loaded_this_frame() - if (goal::is_gameplay_exact() && !savest::interacted_with_state()) { + if (goal::is_gameplay_exact() && savest::get_last_action() == savest::Action::None) { // As soon as we're done holding the load state button (or just any time we're controlling // the monkey on the stage), we're allowed to die s_can_incr_death_counter = true; @@ -79,7 +84,7 @@ bool should_count_as_normal_death() { } bool should_count_as_savestate_death() { - return savest::loaded_state() && s_can_incr_death_counter; + return loaded_state() && s_can_incr_death_counter; } void count_deaths() { @@ -102,8 +107,11 @@ void tick() { reset_flag(); } - count_deaths(); update_flag_on_state_release(); + count_deaths(); + + // Only after we're done doing death checks for this frame do we update s_previous_frame_action + s_previous_frame_action = savest::get_last_action(); } bool should_display_death_counter() { diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index 1c04738b..6a28c05b 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -47,9 +47,7 @@ void tick() { s_valid_run = false; } - // savest::interacted_with_state() - // savest::state_changed_gameplay() - if (savest::interacted_with_state()) { + if (savest::get_last_action() != savest::Action::None) { s_valid_run = false; } } diff --git a/src/mods/validate.cpp b/src/mods/validate.cpp index aaa250bb..12069cf5 100644 --- a/src/mods/validate.cpp +++ b/src/mods/validate.cpp @@ -54,9 +54,9 @@ void validate_run() { } // Track savestates - // savest::interacted_with_state() - // savest::state_changed_gameplay() - if (savest::interacted_with_state()) s_loaded_savestate = true; + // checking that the action is not None doesn't allow 1f tap state creations (as opposed to + // checking the action isn't Load) + if (savest::get_last_action() != savest::Action::None) s_loaded_savestate = true; // Using dpad controls is disallowed bool dpad_down = diff --git a/src/systems/goal.cpp b/src/systems/goal.cpp index 84d41318..1eb99750 100644 --- a/src/systems/goal.cpp +++ b/src/systems/goal.cpp @@ -92,8 +92,8 @@ void reset_tape_break_counter() { // Because of the run order notes mentioned earlier, this always runs after savest_ui's tick, // which means that at the end of any frame where we load a state, s_frames_until_goal_submode // will be 0 - if (mode::is_stage_exit_init(mkb::sub_mode) || savest::interacted_with_state() || - mode::is_spin_in_init(mkb::sub_mode)) { + if (mode::is_stage_exit_init(mkb::sub_mode) || + savest::get_last_action() != savest::Action::None || mode::is_spin_in_init(mkb::sub_mode)) { s_frames_until_goal_submode = 0; } } From 9ad68fd6bab2da29b3f62f5bcded4198cba89aa3 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Thu, 20 Aug 2026 15:26:01 -0500 Subject: [PATCH 59/90] fix state bug with gotostory and custom monkey --- src/mods/ballcolor.cpp | 14 +++++++++++--- src/systems/savest.cpp | 5 ++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mods/ballcolor.cpp b/src/mods/ballcolor.cpp index 97a60e1d..9ef3f9fb 100644 --- a/src/mods/ballcolor.cpp +++ b/src/mods/ballcolor.cpp @@ -2,6 +2,7 @@ #include "mkb/mkb.h" +#include "mkb/mkb2_ghidra.h" #include "systems/heap.h" #include "systems/pad.h" #include "systems/pref.h" @@ -56,6 +57,8 @@ static u8 convert_to_ape_color_id(u8 color_choice) { return color_choice - 1; } +// Set both active_monkey_id and selected_characters so savestates work when we use gotostory even +// if we haven't manually selected a monkey on the character select screen void switch_monkey() { switch (MonkeyType(pref::get(pref::Pref::MonkeyType))) { case MonkeyType::Default: { @@ -63,22 +66,28 @@ void switch_monkey() { } case MonkeyType::Aiai: { mkb::active_monkey_id[mkb::curr_player_idx] = 0; + mkb::selected_characters[mkb::curr_player_idx] = 0; break; } case MonkeyType::Meemee: { mkb::active_monkey_id[mkb::curr_player_idx] = 1; + mkb::selected_characters[mkb::curr_player_idx] = 1; break; } case MonkeyType::Baby: { mkb::active_monkey_id[mkb::curr_player_idx] = 2; + mkb::selected_characters[mkb::curr_player_idx] = 2; break; } case MonkeyType::Gongon: { mkb::active_monkey_id[mkb::curr_player_idx] = 3; + mkb::selected_characters[mkb::curr_player_idx] = 3; break; } case MonkeyType::Random: { - mkb::active_monkey_id[mkb::curr_player_idx] = mkb::rand() % 4; + u8 monkey_id = mkb::rand() % 4; + mkb::active_monkey_id[mkb::curr_player_idx] = monkey_id; + mkb::selected_characters[mkb::curr_player_idx] = monkey_id; break; } } @@ -150,8 +159,7 @@ void tick() { u32 blue = CLAMP(((mkb::rand() % 256) + bonus_brightness), 0, 0xff); s_current_color = {static_cast(red), static_cast(green), static_cast(blue), 0}; - *reinterpret_cast(relutil::relocate_addr(0x80472a34)) = - s_current_color; + *reinterpret_cast(relutil::relocate_addr(0x80472a34)) = s_current_color; } break; } diff --git a/src/systems/savest.cpp b/src/systems/savest.cpp index aed4c3ae..df382d60 100644 --- a/src/systems/savest.cpp +++ b/src/systems/savest.cpp @@ -24,7 +24,7 @@ struct SaveState { u32 flags; u32 timestamp; s32 stage_id; - u8 character; + u32 character; store::Store store; u8 pause_menu_sprite_status; mkb::Sprite pause_menu_sprite; @@ -287,8 +287,7 @@ SaveResult save(u32 slot) { pass_over_regions(&state->store, store::save); handle_pause_menu_save(state); state->stage_id = mkb::current_stage_id; - // using mkb::selected_characters instead of mkb::active_monkey_id works with gotostory - state->character = mkb::selected_characters[mkb::curr_player_idx]; + state->character = mkb::active_monkey_id[mkb::curr_player_idx]; state->flags |= Flag_IsPresent; state->timestamp = s_timestamp; From 861149691d486be378ac94f2a9198256206b6a01 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 01:38:22 -0500 Subject: [PATCH 60/90] more wip textinfo stuff, simpler setup still messy TODO: handle freecam --- src/systems/main.cpp | 2 + src/systems/textinfo.cpp | 80 +++++++++++++++++++++++++++++++++++++++- src/systems/textinfo.h | 9 ++++- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/systems/main.cpp b/src/systems/main.cpp index c12ece72..5c069a27 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -11,6 +11,7 @@ #include "systems/pad.h" #include "systems/pref.h" #include "systems/savest.h" +#include "systems/textinfo.h" #include "systems/version.h" #include "utils/draw.h" #include "utils/patch.h" @@ -146,6 +147,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { ilmark::disp(); physics::disp(); scratch::disp(); + textinfo::disp(); }); static patch::Tramp s_smd_game_ready_init_tramp([]() { diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index bd0d115c..9c0bcaf4 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -25,7 +25,9 @@ constexpr QueueObject EMPTY_QUEUE_OBJECT = {EMPTY_GROUP, nullptr}; // static QueueObject s_text_queue[] = {[0 ... MAX_QUEUE_LENGTH] = EMPTY_QUEUE_OBJECT}; +constexpr u16 STARTING_ROW = 2; static QueueObject s_text_queue[MAX_QUEUE_LENGTH]; +static u16 s_active_row[LEN(s_slot_list)] = {STARTING_ROW, STARTING_ROW}; void initialize_queue() { // I guess for (u8 k = 0; k < MAX_QUEUE_LENGTH; k++) { @@ -62,9 +64,21 @@ DisplayGroup module_to_display_group(Module module) { s32 get_slot_x_pos(Slot slot) { switch (slot) { case Slot::Left: - return 0; + return 18; case Slot::Right: + return 378; + default: return 0; + } +} + +// Where the numbers get lined up +s32 get_slot_timer_x_pos(Slot slot) { + switch (slot) { + case Slot::Left: + return 102; + case Slot::Right: + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; default: return 0; } @@ -126,11 +140,75 @@ void draw_displays() { } } +// --- new stuff --- + +// Slot slot, GXColor color, char *format, ... + +void draw(Slot slot, GXColor color, char *format, ...) { + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + s32 x = get_slot_x_pos(slot); + s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); + + draw::debug_text(x, y, color, format); + + s_active_row[k]++; + } + } +} + +// We use "Slot" to determine which set of timers/text rows to stack our current text row under +// Each mod might need to use a slightly different x position, though; see storytimer.cpp for +// instance +// The breakdown row x position depends on the row number since we want to align things based on the +// numbers and not the left hand side of the text +void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); + + draw::debug_text(pos_x, y, color, format); + + s_active_row[k]++; + } + } +} + +// aligns the time according to get_slot_timer_x_pos, the prefix gets automatically moved to the +// left depending on string length +void draw_timer(Slot slot, GXColor color, char *prefix, u32 frames, timerdisp::TimeFormat format) { + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + s32 x = get_slot_timer_x_pos(slot); + s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); + + char time_buf[16] = {}; + timerdisp::format_time(time_buf, frames, format); + + u32 prefix_len = mkb::strlen(prefix); + s32 text_x = x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + draw::debug_text(text_x, y, color, prefix); + draw::debug_text(x, y, color, time_buf); + + s_active_row[k]++; + } + } +} + +void reset_active_rows() { + for (u16 k = 0; k < LEN(s_slot_list); k++) { + s_active_row[k] = STARTING_ROW; + } +} + void init() { } void tick() { } void disp() { + // textinfo's disp() runs after all other disp() functions + reset_active_rows(); } } // namespace textinfo \ No newline at end of file diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 883f9665..f68d7760 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -1,6 +1,7 @@ #pragma once -#include "../mkb/mkb.h" +#include "mkb/mkb.h" +#include "utils/timerdisp.h" namespace textinfo { @@ -32,6 +33,12 @@ enum class Module { LoadlessTimer, }; +s32 get_slot_timer_x_pos(Slot slot); + +void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); +void draw(Slot slot, GXColor color, char *format, ...); +void draw_timer(Slot slot, GXColor color, char *prefix, u32 frames, timerdisp::TimeFormat format); + void init(); void tick(); void disp(); From 2705b5cdf11887da536bab72b9069e24722e7715 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 01:38:56 -0500 Subject: [PATCH 61/90] test textinfo functions on storytimer, deathcounter --- src/mods/deathcounter.cpp | 7 +++++-- src/mods/storytimer.cpp | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 00f2ca4d..82315944 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -7,6 +7,7 @@ #include "systems/goal.h" #include "systems/pref.h" #include "systems/savest.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/mode.h" @@ -149,9 +150,11 @@ void disp() { } if (should_display_death_counter()) { - draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); + /* draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", - get_total_death_count()); + get_total_death_count()); */ + textinfo::draw_main(textinfo::Slot::Left, COUNTER_DISPLAY_X_POS, draw::WHITE, "Deaths:%d", + get_total_death_count()); } } diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 58a4c507..903f8e0b 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -7,6 +7,7 @@ #include "storyreset.h" #include "systems/goal.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/mode.h" @@ -241,20 +242,28 @@ u16 get_current_segment_idx() { } void draw_timers() { - TimerDisplayInfo fullgame_info = get_timer_display_info(TimerType::Fullgame); - u32 loadless_time = get_loadless_time(); + // TimerDisplayInfo fullgame_info = get_timer_display_info(TimerType::Fullgame); + // u32 loadless_time = get_loadless_time(); if (should_display_timer(TimerType::Fullgame)) { - timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, - "Time:", loadless_time, false, draw::WHITE); + /* timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, + "Time:", loadless_time, false, draw::WHITE); */ + // textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Time: %d", get_loadless_time()); + textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, "Time:", get_loadless_time(), + timerdisp::TimeFormat::AlwaysLeadNonHours); } u16 world_idx = get_current_segment_idx(); // index of the current world - TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); + // TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); if (should_display_timer(TimerType::Segment)) { - timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, - "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); + /* timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, + "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); */ + /* textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Seg: %d", + s_world_timer[world_idx].segment); */ + textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, + "Seg:", s_world_timer[world_idx].segment, + timerdisp::TimeFormat::AlwaysLeadNonHours); } } @@ -288,7 +297,9 @@ void draw_breakdown_screen() { mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], world_deaths); - draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); + // draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); + // textinfo::draw(textinfo::Slot::Left, draw::WHITE, row_info_buf[idx]); + textinfo::draw_main(textinfo::Slot::Left, pos.x, draw::WHITE, row_info_buf[idx]); } // For the totals row From 10e1d1ab77edec4e80b87142da9ed9ffd88b7232 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 02:29:00 -0500 Subject: [PATCH 62/90] allow signed times + different starting rows per slot --- src/systems/textinfo.cpp | 28 +++++++++++++++++++++++----- src/systems/textinfo.h | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 9c0bcaf4..d1db6b3d 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -26,8 +26,10 @@ constexpr QueueObject EMPTY_QUEUE_OBJECT = {EMPTY_GROUP, nullptr}; // static QueueObject s_text_queue[] = {[0 ... MAX_QUEUE_LENGTH] = EMPTY_QUEUE_OBJECT}; constexpr u16 STARTING_ROW = 2; +// constexpr u16 STARTING_ROW_LIST[LEN(s_slot_list)] = {2, 0}; static QueueObject s_text_queue[MAX_QUEUE_LENGTH]; -static u16 s_active_row[LEN(s_slot_list)] = {STARTING_ROW, STARTING_ROW}; +// static u16 s_active_row[LEN(s_slot_list)] = {STARTING_ROW, STARTING_ROW}; +static u16 s_active_row[LEN(s_slot_list)] = {}; void initialize_queue() { // I guess for (u8 k = 0; k < MAX_QUEUE_LENGTH; k++) { @@ -174,11 +176,15 @@ void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { } } -// aligns the time according to get_slot_timer_x_pos, the prefix gets automatically moved to the +// aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the // left depending on string length -void draw_timer(Slot slot, GXColor color, char *prefix, u32 frames, timerdisp::TimeFormat format) { +void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { + bool positive = frames >= 0; + if (!positive) frames = -frames; + const char *sign = positive ? "" : "-"; + s32 x = get_slot_timer_x_pos(slot); s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); @@ -189,16 +195,28 @@ void draw_timer(Slot slot, GXColor color, char *prefix, u32 frames, timerdisp::T s32 text_x = x - prefix_len * draw::DEBUG_CHAR_WIDTH; draw::debug_text(text_x, y, color, prefix); - draw::debug_text(x, y, color, time_buf); + draw::debug_text(x, y, color, "%s%s", sign, time_buf); s_active_row[k]++; } } } +u16 get_slot_starting_row(Slot slot) { + switch (slot) { + case Slot::Left: + return 2; + case Slot::Right: + return 0; + default: + return 0; + } +} + void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { - s_active_row[k] = STARTING_ROW; + // s_active_row[k] = STARTING_ROW; + s_active_row[k] = get_slot_starting_row(s_slot_list[k]); } } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index f68d7760..80ad05cf 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -37,7 +37,7 @@ s32 get_slot_timer_x_pos(Slot slot); void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); void draw(Slot slot, GXColor color, char *format, ...); -void draw_timer(Slot slot, GXColor color, char *prefix, u32 frames, timerdisp::TimeFormat format); +void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); void init(); void tick(); From f1f8fb98b418f09f2d94a136486a3bbdc59f6aa5 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 02:30:11 -0500 Subject: [PATCH 63/90] update cmseg, iw to use textinfo, partially update timer.cpp updated run order of disp() functions in main to preserve old display stack order have not updated subtick/framesave stuff in timer.cpp yet --- src/mods/cmseg.cpp | 5 ++++- src/mods/iw.cpp | 5 ++++- src/mods/timer.cpp | 9 +++++++-- src/systems/main.cpp | 3 ++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index cd5b9df5..cbee602d 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -5,6 +5,7 @@ #include "mods/freecam.h" #include "systems/log.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" @@ -424,7 +425,9 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - timerdisp::draw_timer_right_side(static_cast(s_seg_time), "SEG:", 0, color, false); + // timerdisp::draw_timer_right_side(static_cast(s_seg_time), "SEG:", 0, color, false); + textinfo::draw_timer(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index 123f10ab..6d1a3692 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -6,6 +6,7 @@ #include "systems/assembly.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/patch.h" #include "utils/relutil.h" @@ -124,7 +125,9 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - timerdisp::draw_timer_right_side(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); + // timerdisp::draw_timer_right_side(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); + textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } // namespace iw diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index ece362c4..e87b0d14 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -4,6 +4,7 @@ #include "mods/freecam.h" #include "mods/validate.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/patch.h" #include "utils/timerdisp.h" @@ -59,11 +60,15 @@ void disp() { u32 row = 1; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); + // timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); + textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + timerdisp::TimeFormat::SecondsOnly); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - timerdisp::draw_timer_right_side(s_pause_timer, "PAU:", row++, draw::WHITE, true); + // timerdisp::draw_timer_right_side(s_pause_timer, "PAU:", row++, draw::WHITE, true); + textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "PAU:", s_pause_timer, + timerdisp::TimeFormat::SecondsOnly); } switch (mkb::sub_mode) { diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 5c069a27..4378952a 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -135,12 +135,13 @@ static patch::Tramp s_draw_debug_text_tramp([]() { } draw::predraw(); - timer::disp(); + // timer::disp(); iw::disp(); deathcounter::disp(); storytimer::disp(); ilbattle::disp(); cmseg::disp(); + timer::disp(); inputdisp::disp(); menu_impl::disp(); draw::disp(); From 1b1248cd0f77b5287fa70795efd76e7f0155e82d Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 04:27:47 -0500 Subject: [PATCH 64/90] add format_signed_time and format_subtick_time functions --- src/utils/timerdisp.cpp | 36 ++++++++++++++++++++++++++++++++++-- src/utils/timerdisp.h | 8 +++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index fa73c271..9994e19a 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -19,10 +19,10 @@ TimeComp get_time_components(u32 frames) { return {time_hours, time_minutes, time_seconds, time_centiseconds}; } -void format_time(char *buffer, u32 frames, TimeFormat format_type) { +void format_time(char *buffer, u32 frames, TimeFormat format) { TimeComp time = get_time_components(frames); - switch (format_type) { + switch (format) { case TimeFormat::SecondsOnly: { u32 total_seconds = frames / SECOND_FRAMES; mkb::sprintf(buffer, "%d.%02d", total_seconds, time.centiseconds); @@ -69,6 +69,17 @@ void format_time(char *buffer, u32 frames, TimeFormat format_type) { } } +void format_signed_time(char *buffer, s32 frames, TimeFormat format) { + bool positive = frames >= 0; + if (!positive) frames = -frames; + const char *sign = positive ? "" : "-"; + + char time_buf[16] = {}; + format_time(time_buf, frames, format); + + mkb::sprintf(buffer, "%s%s", sign, time_buf); +} + s32 row_number_to_vertical_pos(u32 row_num) { return Y + 16 * row_num; } @@ -109,6 +120,23 @@ void draw_timer_right_side(s32 frames, draw_timer(X, row, 4 * draw::DEBUG_CHAR_WIDTH, prefix, frames, show_seconds, color); } +void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_precision) { + bool positive = frames >= 0; + if (!positive) frames = -frames; + const char *sign = positive ? "" : "-"; + + TimeComp time = get_time_components(frames); + u32 total_seconds = frames / SECOND_FRAMES; + u32 milliseconds = time.centiseconds * 10 + framesave / 6; // 3 digit + u32 extra = time.centiseconds * 100 + framesave * 10 / 6; // 4 digit + + if (extra_precision) { + mkb::sprintf(buffer, "%s%02d.%04d", sign, total_seconds, extra); + } else { + mkb::sprintf(buffer, "%s%02d.%03d", sign, total_seconds, milliseconds); + } +} + void draw_subtick_timer(s32 frames, const char *prefix, u32 row, @@ -126,7 +154,11 @@ void draw_subtick_timer(s32 frames, u32 minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; u32 milliseconds = ((frames % SECOND_FRAMES) * 100 + framesave) / 6; // 3 digit + // (frames % SECOND_FRAMES) * 1000 / 60 + framesave / 6; + // centiseconds * 10 + framesave / 6; u32 extra = (((frames % SECOND_FRAMES) * 100 + framesave) * 10) / 6; // 4 digit + // (frames % SECOND_FRAMES) * 10000 / 60 + framesave * 10 / 6; + // centiseconds * 100 + framesave * 10 / 6; s32 y = row_number_to_vertical_pos(row); diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 1f492432..2ffaea50 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -1,9 +1,13 @@ #pragma once #include "mkb/mkb.h" +#include "utils/draw.h" namespace timerdisp { +// Where numbers on the right side get aligned (not their prefixes!) +constexpr s32 RIGHT_SIDE_TIMER_ALIGN_X = 378 + 4 * draw::DEBUG_CHAR_WIDTH; + // TimeComp = "Time Components" struct TimeComp { u32 hours; @@ -21,7 +25,9 @@ enum class TimeFormat { AlwaysLeadNonHours // mm:ss.cc if < 1 hour, ss.cc if < 1 min }; -void format_time(char *buffer, u32 frames, TimeFormat format_type); +void format_time(char *buffer, u32 frames, TimeFormat format); +void format_signed_time(char *buffer, s32 frames, TimeFormat format); +void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_precision); s32 row_number_to_vertical_pos(u32 row_num); void draw_timer(s32 pos_x, From 127d33d6a82861268981b32cde66928e8e9b8652 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 04:28:03 -0500 Subject: [PATCH 65/90] update draw_timer and add draw_subtick_timer in textinfo.cpp --- src/systems/textinfo.cpp | 28 ++++++++++++++++++++++++++-- src/systems/textinfo.h | 6 ++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index d1db6b3d..ee602a21 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -179,7 +179,14 @@ void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { // aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the // left depending on string length void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { - for (u16 k = 0; k < LEN(s_slot_list); k++) { + s32 num_x = get_slot_timer_x_pos(slot); + u32 prefix_len = mkb::strlen(prefix); + s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_signed_time(time_buf, frames, format); + draw_main(slot, x, color, "%s%s", prefix, time_buf); + /* for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { bool positive = frames >= 0; if (!positive) frames = -frames; @@ -199,7 +206,24 @@ void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::T s_active_row[k]++; } - } + } */ +} + +void draw_subtick_timer(Slot slot, + GXColor color, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision) { + s32 num_x = get_slot_timer_x_pos(slot); + u32 prefix_len = mkb::strlen(prefix); + s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); + + // draw_main(slot, x, color, "%s%s%s", prefix, sign, time_buf); // this crashes??? + draw_main(slot, x, color, "%s%s", prefix, time_buf); // but this doesn't??? } u16 get_slot_starting_row(Slot slot) { diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 80ad05cf..106eb531 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -38,6 +38,12 @@ s32 get_slot_timer_x_pos(Slot slot); void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); void draw(Slot slot, GXColor color, char *format, ...); void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); +void draw_subtick_timer(Slot slot, + GXColor color, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision); void init(); void tick(); From 5bd853c6d7c37a6e5e99b3f7a9a62bfee69f6b80 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 04:28:29 -0500 Subject: [PATCH 66/90] update subtick/framesave displays in timer.cpp --- src/mods/timer.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index e87b0d14..ea86c33e 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -57,7 +57,7 @@ void disp() { } } - u32 row = 1; + // u32 row = 1; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { // timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); @@ -84,12 +84,18 @@ void disp() { u32 framesave = validate::get_framesave(); if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { - timerdisp::draw_subtick_timer(mkb::mode_info.stage_time_frames_remaining, "SUB:", row++, - draw::WHITE, true, framesave, false); + /* timerdisp::draw_subtick_timer(mkb::mode_info.stage_time_frames_remaining, "SUB:", row++, + draw::WHITE, true, framesave, false); */ + textinfo::draw_subtick_timer(textinfo::Slot::Right, draw::WHITE, + "SUB:", mkb::mode_info.stage_time_frames_remaining, framesave, + false); } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { - timerdisp::draw_percentage(framesave, "FSV:", row++, draw::WHITE); + // timerdisp::draw_percentage(framesave, "FSV:", row++, draw::WHITE); + s32 num_x = textinfo::get_slot_timer_x_pos(textinfo::Slot::Right); + s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; + textinfo::draw_main(textinfo::Slot::Right, x, draw::WHITE, "FSV:%2d%", framesave); } } From 25877afd7dcdf6a6e61a60fc2598eb96ac635c2d Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 04:45:14 -0500 Subject: [PATCH 67/90] clean up textinfo some removed old initial code --- src/systems/textinfo.cpp | 170 ++------------------------------------- src/systems/textinfo.h | 25 ------ 2 files changed, 6 insertions(+), 189 deletions(-) diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index ee602a21..06fc5f9f 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -9,71 +9,13 @@ namespace textinfo { -// wip/messy/underdeveloped ideas etc -// textinfo will send its stuff to textmanager - -struct QueueObject { - DisplayGroup group; - char *text; +static const Slot s_slot_list[]{ + Slot::Left, + Slot::Right, }; -constexpr u16 MAX_PRIORITY = 5; -constexpr u16 MAX_QUEUE_LENGTH = 21; // 13 loadless timer // 1 death counter // 4 il battle // 2 - // rta/pause timer // 1 framesave // -constexpr DisplayGroup EMPTY_GROUP = {Slot::Unused, 0}; -constexpr QueueObject EMPTY_QUEUE_OBJECT = {EMPTY_GROUP, nullptr}; - -// static QueueObject s_text_queue[] = {[0 ... MAX_QUEUE_LENGTH] = EMPTY_QUEUE_OBJECT}; - -constexpr u16 STARTING_ROW = 2; -// constexpr u16 STARTING_ROW_LIST[LEN(s_slot_list)] = {2, 0}; -static QueueObject s_text_queue[MAX_QUEUE_LENGTH]; -// static u16 s_active_row[LEN(s_slot_list)] = {STARTING_ROW, STARTING_ROW}; static u16 s_active_row[LEN(s_slot_list)] = {}; -void initialize_queue() { // I guess - for (u8 k = 0; k < MAX_QUEUE_LENGTH; k++) { - s_text_queue[k] = EMPTY_QUEUE_OBJECT; - } -} - -QueueObject get_queue_obj(u16 idx) { - u16 clamped_idx = MIN(idx, MAX_QUEUE_LENGTH - 1); - return s_text_queue[clamped_idx]; -} - -DisplayGroup module_to_display_group(Module module) { - switch (module) { - case Module::IlBattle: - return {Slot::Left, 0}; - case Module::DeathCounter: - return {Slot::Left, 1}; - case Module::LoadlessTimer: - return {Slot::Left, 2}; - case Module::IwTimer: - return {Slot::Right, 0}; - case Module::CmSeg: - return {Slot::Right, 1}; - case Module::RtaTimer: - return {Slot::Right, 2}; - case Module::FrameSave: - return {Slot::Right, 3}; - default: - return {Slot::Unused, 0}; - } -} - -s32 get_slot_x_pos(Slot slot) { - switch (slot) { - case Slot::Left: - return 18; - case Slot::Right: - return 378; - default: - return 0; - } -} - // Where the numbers get lined up s32 get_slot_timer_x_pos(Slot slot) { switch (slot) { @@ -86,84 +28,12 @@ s32 get_slot_timer_x_pos(Slot slot) { } } -/* void reserve(Module module) { -} */ - -// char *format, ... -void queue(Module module, char *text) { - for (u16 k = 0; k < LEN(s_text_queue); k++) { - if (s_text_queue[k].text != nullptr) { - DisplayGroup group = module_to_display_group(module); - // char *text; - // mkb::sprintf(text, format, ...); - s_text_queue[k] = {group, text}; - } - } -} - -bool does_queue_obj_belong_to_display_group(DisplayGroup group, QueueObject obj) { - return obj.group.slot == group.slot && obj.group.priority == group.priority; -} - -// TODO: display ids attached to each object in a display group, so we can do hardcodes by id (like -// color, etc) - -// maybe? -struct DisplayObject { - DisplayGroup group; - u8 id; -}; - -struct TextInfo { - GXColor color; - char *buffer; -}; - -// Allows us to specify color and modify the text on a per-id basis -/* TextInfo get_display_object_text_info(DisplayObject obj) { -} */ - -void draw_displays() { - for (u8 k = 0; k < LEN(s_slot_list); k++) { // First iterate through each slot - u8 row = 0; - for (u8 j = 0; j < MAX_PRIORITY; j++) { // Iterate through each priority level - DisplayGroup group = {s_slot_list[k], j}; - for (u8 m = 0; m < MAX_QUEUE_LENGTH; m++) { - if (!does_queue_obj_belong_to_display_group(group, s_text_queue[m])) { - continue; - } - s32 x = get_slot_x_pos(s_slot_list[k]); - s32 y = timerdisp::row_number_to_vertical_pos(row); - - draw::debug_text(x, y, draw::WHITE, s_text_queue[m].text); - row++; - } - } - } -} - -// --- new stuff --- - -// Slot slot, GXColor color, char *format, ... - -void draw(Slot slot, GXColor color, char *format, ...) { - for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - s32 x = get_slot_x_pos(slot); - s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); - - draw::debug_text(x, y, color, format); - - s_active_row[k]++; - } - } -} - // We use "Slot" to determine which set of timers/text rows to stack our current text row under // Each mod might need to use a slightly different x position, though; see storytimer.cpp for // instance // The breakdown row x position depends on the row number since we want to align things based on the // numbers and not the left hand side of the text +// TODO: rename to just draw() void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { @@ -176,7 +46,7 @@ void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { } } -// aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the +// Aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the // left depending on string length void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { s32 num_x = get_slot_timer_x_pos(slot); @@ -186,27 +56,6 @@ void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::T char time_buf[16] = {}; timerdisp::format_signed_time(time_buf, frames, format); draw_main(slot, x, color, "%s%s", prefix, time_buf); - /* for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - bool positive = frames >= 0; - if (!positive) frames = -frames; - const char *sign = positive ? "" : "-"; - - s32 x = get_slot_timer_x_pos(slot); - s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); - - char time_buf[16] = {}; - timerdisp::format_time(time_buf, frames, format); - - u32 prefix_len = mkb::strlen(prefix); - s32 text_x = x - prefix_len * draw::DEBUG_CHAR_WIDTH; - - draw::debug_text(text_x, y, color, prefix); - draw::debug_text(x, y, color, "%s%s", sign, time_buf); - - s_active_row[k]++; - } - } */ } void draw_subtick_timer(Slot slot, @@ -221,9 +70,7 @@ void draw_subtick_timer(Slot slot, char time_buf[16] = {}; timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); - - // draw_main(slot, x, color, "%s%s%s", prefix, sign, time_buf); // this crashes??? - draw_main(slot, x, color, "%s%s", prefix, time_buf); // but this doesn't??? + draw_main(slot, x, color, "%s%s", prefix, time_buf); } u16 get_slot_starting_row(Slot slot) { @@ -239,15 +86,10 @@ u16 get_slot_starting_row(Slot slot) { void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { - // s_active_row[k] = STARTING_ROW; s_active_row[k] = get_slot_starting_row(s_slot_list[k]); } } -void init() { -} -void tick() { -} void disp() { // textinfo's disp() runs after all other disp() functions reset_active_rows(); diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 106eb531..0013a635 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -11,32 +11,9 @@ enum class Slot { Right, }; -static const Slot s_slot_list[]{ - Slot::Left, - Slot::Right, -}; - -struct DisplayGroup { - Slot slot; - u8 priority; -}; - -// maybe call it DisplayModule since a module can technically decide to draw to multiple slots, so -// we could have like ModuleALeft, ModuleARight, ModuleB, etc -enum class Module { - RtaTimer, - IwTimer, - CmSeg, - FrameSave, - IlBattle, - DeathCounter, - LoadlessTimer, -}; - s32 get_slot_timer_x_pos(Slot slot); void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); -void draw(Slot slot, GXColor color, char *format, ...); void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); void draw_subtick_timer(Slot slot, GXColor color, @@ -45,8 +22,6 @@ void draw_subtick_timer(Slot slot, u32 framesave, bool extra_precision); -void init(); -void tick(); void disp(); } // namespace textinfo From c90406375625eaee8740f1e6cb44a2e3b0e04f03 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 16:57:16 -0500 Subject: [PATCH 68/90] add debug_text_v to draw's header also made non static --- src/utils/draw.cpp | 2 +- src/utils/draw.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/draw.cpp b/src/utils/draw.cpp index 5463e248..24df763b 100644 --- a/src/utils/draw.cpp +++ b/src/utils/draw.cpp @@ -63,7 +63,7 @@ static void debug_text_buf(s32 x, s32 y, GXColor color, const char *buf) { mkb::textdraw_print((char *)buf); } -static void debug_text_v(s32 x, s32 y, GXColor color, const char *format, va_list args) { +void debug_text_v(s32 x, s32 y, GXColor color, const char *format, va_list args) { // Shouldn't be able to print a string to the screen longer than this // Be careful not to overflow! MKB2 doesn't have vsnprintf static char buf[80]; diff --git a/src/utils/draw.h b/src/utils/draw.h index 5693de3c..0a66ff2b 100644 --- a/src/utils/draw.h +++ b/src/utils/draw.h @@ -1,5 +1,6 @@ #pragma once +#include #include "mkb/mkb.h" namespace draw { @@ -37,6 +38,7 @@ void predraw(); void rect(float x1, float y1, float x2, float y2, GXColor color); void debug_text_palette(); +void debug_text_v(s32 x, s32 y, GXColor color, const char *format, va_list args); void debug_text(s32 x, s32 y, GXColor color, const char *format, ...); void heart(); void tm(); From 93f4b5e3fe0df2e44b4f75b94d9ed96e07036d16 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 16:57:42 -0500 Subject: [PATCH 69/90] add unformatted option to format_time --- src/utils/timerdisp.cpp | 3 +++ src/utils/timerdisp.h | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 9994e19a..36cc32d3 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -66,6 +66,9 @@ void format_time(char *buffer, u32 frames, TimeFormat format) { } break; } + case TimeFormat::Unformatted: { + mkb::sprintf(buffer, "%d", frames); + } } } diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 2ffaea50..4b249c74 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -22,7 +22,8 @@ enum class TimeFormat { MinutesAlwaysLeadingZero, // 0m:ss.cc or mm:ss.cc if <1 hour, h:mm:ss.cc otherwise MinimalLeading, // ss.cc if < 1 min, m:ss.cc if < 10 min, mm:ss.cc if > 10 min, < 1h, // h:mm:ss.cc otherwise - AlwaysLeadNonHours // mm:ss.cc if < 1 hour, ss.cc if < 1 min + AlwaysLeadNonHours, // mm:ss.cc if < 1 hour, ss.cc if < 1 min + Unformatted }; void format_time(char *buffer, u32 frames, TimeFormat format); From 6b9e0c0c10a19124f6926cc728ac33c3ee43d782 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 17:00:30 -0500 Subject: [PATCH 70/90] textinfo: api allows for mod specific input (wip) (1) modules can have a "min row" that they're allowed to draw to (2) add an option to not increment the row on the current draw() call, useful in ilbattle --- src/systems/textinfo.cpp | 180 +++++++++++++++++++++++++++++++++++++++ src/systems/textinfo.h | 21 +++++ 2 files changed, 201 insertions(+) diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 06fc5f9f..9476c032 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -28,6 +28,186 @@ s32 get_slot_timer_x_pos(Slot slot) { } } +/* Slot module_to_slot(Module module) { + switch (module) { + case Module::IlBattle: + return Slot::Left; + case Module::DeathCounter: + return Slot::Left; + case Module::LoadlessTimer: + return Slot::Left; + case Module::IwTimer: + return Slot::Right; + case Module::CmSeg: + return Slot::Right; + case Module::RtaTimer: + return Slot::Right; + case Module::FrameSave: + return Slot::Right; + default: + return Slot::Unused; + } +} + +u16 module_to_min_row(Module module) { + switch (module) { + case Module::RtaTimer: + return 2; + default: + return 0; + } +} */ + +/* void draw(Module module, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { + Slot slot = module_to_slot(module); + u16 min_row = module_to_min_row(module); + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + u16 row = MAX(min_row, s_active_row[k]); + s32 y = timerdisp::row_number_to_vertical_pos(row); + + draw::debug_text(pos_x, y, color, format); + + if (incr_row) { + s_active_row[k] = row + 1; + } + } + } +} */ + +// Per-slot minimum row number. Individual modules are allowed to have higher min rows, however +u16 get_slot_min_row(Slot slot) { + switch (slot) { + case Slot::Left: + return 2; + case Slot::Right: + return 0; + default: + return 0; + } +} + +// Modules are allowed to draw to multiple slots, and can have different min rows for each slot (we +// don't have any modules currently that need to do this though) +u16 module_and_slot_to_min_row(Module module, Slot slot) { + switch (module) { + case Module::IlBattle: + return get_slot_min_row(Slot::Left); + case Module::DeathCounter: + return get_slot_min_row(Slot::Left); + case Module::LoadlessTimer: + return get_slot_min_row(Slot::Left); + case Module::IwTimer: + return get_slot_min_row(Slot::Right); + case Module::CmSeg: + return get_slot_min_row(Slot::Right); + case Module::RtaTimer: + return 2; + case Module::FrameSave: + return 2; + default: + return 0; + } +} + +s32 module_and_slot_to_timer_x_pos(Module module, Slot slot) { + switch (module) { + case Module::IlBattle: + return 160 - 6; + case Module::DeathCounter: + return 102; + case Module::LoadlessTimer: + return 102; + case Module::IwTimer: + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + case Module::CmSeg: + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + case Module::RtaTimer: + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + case Module::FrameSave: + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + default: + return 0; + } +} + +void draw_v(Module module, + Slot slot, + s32 pos_x, + GXColor color, + bool incr_row, + char *format, + va_list args) { + u16 min_row = module_and_slot_to_min_row(module, slot); + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + u16 row = MAX(min_row, s_active_row[k]); + s32 y = timerdisp::row_number_to_vertical_pos(row); + + draw::debug_text_v(pos_x, y, color, format, args); + + if (incr_row) { + s_active_row[k] = row + 1; + } + } + } +} + +// We will almost always want to input true for the incr_row argument +// However, in cases where we want to draw two pieces of text in the same slot and in the same row +// with custom horizontal spacing, inputting false can be useful +// This is used in ilbattle.cpp (TODO) +void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { + va_list args; + va_start(args, format); + draw_v(module, slot, pos_x, color, incr_row, format, args); + va_end(args); + /* u16 min_row = module_and_slot_to_min_row(module, slot); + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + u16 row = MAX(min_row, s_active_row[k]); + s32 y = timerdisp::row_number_to_vertical_pos(row); + + va_list args; + va_start(args, format); + draw::debug_text_v(pos_x, y, color, format, args); + va_end(args); + + // draw::debug_text(pos_x, y, color, format); + + if (incr_row) { + s_active_row[k] = row + 1; + } + } + } */ +} + +// When we want to align according to module_and_slot_to_timer_x_pos() +void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...) { + va_list args; + va_start(args, format); + draw_v(module, slot, module_and_slot_to_timer_x_pos(module, slot), color, true, format, args); + va_end(args); +} + +void draw_timer_main(Module module, + Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format) { + // s32 num_x = get_slot_timer_x_pos(slot); + s32 num_x = module_and_slot_to_timer_x_pos(module, slot); + u32 prefix_len = mkb::strlen(prefix); + s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_signed_time(time_buf, frames, format); + draw(module, slot, x, color, true, "%s%s", prefix, time_buf); + // draw(module, slot, x, color, true, time_buf); + // draw(module, slot, x, color, true, "%s", prefix); +} + // We use "Slot" to determine which set of timers/text rows to stack our current text row under // Each mod might need to use a slightly different x position, though; see storytimer.cpp for // instance diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 0013a635..3dc19036 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -1,5 +1,6 @@ #pragma once +#include #include "mkb/mkb.h" #include "utils/timerdisp.h" @@ -11,7 +12,27 @@ enum class Slot { Right, }; +enum class Module { + RtaTimer, + IwTimer, + CmSeg, + FrameSave, + IlBattle, + DeathCounter, + LoadlessTimer, +}; + s32 get_slot_timer_x_pos(Slot slot); +s32 module_and_slot_to_timer_x_pos(Module module, Slot slot); + +void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); +void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); +void draw_timer_main(Module module, + Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format); void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); From 58c87ff26b624376d51b3f5261490b871b9d9204 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 17:00:59 -0500 Subject: [PATCH 71/90] start updating il battle display stuff --- src/mods/ilbattle.cpp | 121 ++++++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 29 deletions(-) diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 05c1573c..2761c281 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -5,8 +5,9 @@ #include "systems/binds.h" #include "systems/pad.h" #include "systems/pref.h" -#include "utils/draw.h" #include "systems/savest.h" +#include "systems/textinfo.h" +#include "utils/draw.h" #include "utils/patch.h" #include "utils/relutil.h" #include "utils/timerdisp.h" @@ -34,8 +35,9 @@ static IlBattleState s_state = IlBattleState::NotReady; // ui constants static constexpr s32 X = 160; static constexpr s32 Y = 48; -static constexpr u32 CWIDTH = 12; +static constexpr u32 CWIDTH = draw::DEBUG_CHAR_WIDTH; static constexpr u32 CHEIGHT = 16; +static constexpr s32 TEXT_POS_X = X - 12 * CWIDTH; // time constants static constexpr u32 SECOND_FRAMES = 60; // frames per second static constexpr u32 MINUTE_FRAMES = SECOND_FRAMES * 60; // frames per minute @@ -62,6 +64,14 @@ static u32 s_rainbow = 0; static bool s_time_buzzer = false; static bool s_score_buzzer = false; +using Mod = textinfo::Module; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + +void draw_buzzer_beater_row(GXColor color) { + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, color, true, "EPIC BUZZER BEATER B)"); +} + static void old_buzzer_display(u32 start_y) { s_buzzer_message_count = (s_buzzer_message_count + 1) % 30; if (s_buzzer_message_count >= 0) @@ -84,49 +94,85 @@ static void old_buzzer_display(u32 start_y) { "EPIC BUZZER BEATER B)"); } +void draw_battle_text(GXColor color, char *text) { + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, color, false, text); +} + +void draw_battle_breakdown_text(GXColor color, char *text) { + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X + 3 * CWIDTH, color, false, text); +} + +// When we only want to display a single time or number (like attempt count) +// Special formatting like time [bananas] is handled on a per-case basis, however +void draw_battle_num(GXColor color, u32 num, Format format) { + // No prefix specified since IL Battle uses custom spacing between its prefixes and times + textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, color, "", num, format); +} + +// pre cleanup static void battle_display(GXColor text_color) { - u32 battle_hours = s_battle_frames / HOUR_FRAMES; - u32 battle_minutes = s_battle_frames % HOUR_FRAMES / MINUTE_FRAMES; - u32 battle_seconds = s_battle_frames % MINUTE_FRAMES / SECOND_FRAMES; + // u32 battle_hours = s_battle_frames / HOUR_FRAMES; + // u32 battle_minutes = s_battle_frames % HOUR_FRAMES / MINUTE_FRAMES; + // u32 battle_seconds = s_battle_frames % MINUTE_FRAMES / SECOND_FRAMES; - u32 best_seconds = s_best_frames / SECOND_FRAMES; - u32 best_centiseconds = (s_best_frames % SECOND_FRAMES) * 100 / 60; + // u32 best_seconds = s_best_frames / SECOND_FRAMES; + // u32 best_centiseconds = (s_best_frames % SECOND_FRAMES) * 100 / 60; - u32 best_score_seconds = s_best_score_frames / SECOND_FRAMES; - u32 best_score_centiseconds = (s_best_score_frames % SECOND_FRAMES) * 100 / 60; + // u32 best_score_seconds = s_best_score_frames / SECOND_FRAMES; + // u32 best_score_centiseconds = (s_best_score_frames % SECOND_FRAMES) * 100 / 60; u32 current_y = Y; GXColor time_color = s_time_buzzer ? draw::num_to_rainbow(s_rainbow) : text_color; GXColor score_color = s_score_buzzer ? draw::num_to_rainbow(s_rainbow) : text_color; - draw::debug_text(X - 12 * CWIDTH, Y, text_color, "ELAPSED:"); - if (battle_hours > 0) { + // draw::debug_text(X - 12 * CWIDTH, Y, text_color, "ELAPSED:"); + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "ELAPSED:"); + /* if (battle_hours > 0) { draw::debug_text(X - 6, Y, text_color, "%d:%02d:%02d", battle_hours, battle_minutes, battle_seconds); } else { draw::debug_text(X - 6, Y, text_color, "%02d:%02d", battle_minutes, battle_seconds); - } + } */ + textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, text_color, "", s_battle_frames, + Format::MinutesAlwaysLeadingZero); + /* textinfo::draw(Mod::IlBattle, Slot::Left, textinfo::get_slot_timer_x_pos(Slot::Left), + text_color, true, "%d", s_battle_frames); */ if (pref::get(pref::Pref::IlBattleShowTime)) { current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST TIME:"); + // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST TIME:"); + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "BEST TIME:"); if (pref::get(pref::Pref::IlBattleTieCount) && s_best_frames_ties > 0) { - draw::debug_text(X - 6, current_y, time_color, "%d.%02d (%d)", best_seconds, - best_centiseconds, s_best_frames_ties + 1); + /* draw::debug_text(X - 6, current_y, time_color, "%d.%02d (%d)", best_seconds, + best_centiseconds, s_best_frames_ties + 1); */ + char time_buf[16] = {}; + timerdisp::format_time(time_buf, s_best_frames, Format::SecondsOnly); + /* textinfo::draw(Mod::IlBattle, Slot::Left, + textinfo::module_and_slot_to_timer_x_pos(Mod::IlBattle, Slot::Left), + text_color, true, "%s (%d)", time_buf, s_best_frames_ties + 1); */ + textinfo::draw_aligned(Mod::IlBattle, Slot::Left, time_color, "%s (%d)", time_buf, + s_best_frames_ties + 1); } else { - draw::debug_text(X - 6, current_y, time_color, "%d.%02d", best_seconds, - best_centiseconds); + /* draw::debug_text(X - 6, current_y, time_color, "%d.%02d", best_seconds, + best_centiseconds); */ + textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, time_color, "", s_best_frames, + Format::SecondsOnly); } } if (pref::get(pref::Pref::IlBattleShowScore)) { current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST SCORE:"); + // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST SCORE:"); + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "BEST SCORE:"); if (pref::get(pref::Pref::IlBattleTieCount) && s_best_score_ties > 0) { - draw::debug_text(X - 6, current_y, score_color, "%d (%d)", s_best_score, - s_best_score_ties + 1); + /* draw::debug_text(X - 6, current_y, score_color, "%d (%d)", s_best_score, + s_best_score_ties + 1); */ + textinfo::draw_aligned(Mod::IlBattle, Slot::Left, score_color, "%d (%d)", s_best_score, + s_best_score_ties + 1); } else { - draw::debug_text(X - 6, current_y, score_color, "%d", s_best_score); + // draw::debug_text(X - 6, current_y, score_color, "%d", s_best_score); + textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, score_color, "", s_best_score, + Format::Unformatted); } // breakdown @@ -134,24 +180,41 @@ static void battle_display(GXColor text_color) { if (breakdown_value == 1) { // minimal current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BREAKDOWN:"); + /* draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BREAKDOWN:"); draw::debug_text(X - 6, current_y, text_color, "%d.%02d [%d]", best_score_seconds, - best_score_centiseconds, s_best_score_bananas); + best_score_centiseconds, s_best_score_bananas); */ + + draw_battle_text(text_color, "BREAKDOWN:"); + char time_buf[16] = {}; + timerdisp::format_time(time_buf, s_best_score_frames, Format::SecondsOnly); + textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%s [%d]", time_buf, + s_best_score_bananas); } else if (breakdown_value == 2) { // full current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " BANANAS:"); - draw::debug_text(X - 6, current_y, text_color, "%d", s_best_score_bananas); + // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " BANANAS:"); + // draw::debug_text(X - 6, current_y, text_color, "%d", s_best_score_bananas); + draw_battle_text(text_color, " BANANAS:"); + textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", + s_best_score_bananas); current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " TIMER:"); + /* draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " TIMER:"); draw::debug_text(X - 6, current_y, text_color, "%d.%02d", best_score_seconds, - best_score_centiseconds); + best_score_centiseconds); */ + + draw_battle_text(text_color, " TIMER:"); + textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, text_color, "", + s_best_score_frames, Format::SecondsOnly); } } if (pref::get(pref::Pref::IlBattleAttemptCount)) { current_y += CHEIGHT; - draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "ATTEMPTS:"); - draw::debug_text(X - 6, current_y, text_color, "%d", s_attempts); + // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "ATTEMPTS:"); + // draw::debug_text(X - 6, current_y, text_color, "%d", s_attempts); + + draw_battle_text(text_color, "ATTEMPTS:"); + textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", s_attempts); + // textinfo::draw(Mod::IlBattle, Slot::Left, ) } if (pref::get(pref::Pref::IlBattleBuzzerOld) && From 45c7a9406fb6656b69574d28c3137baf47177f44 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sat, 22 Aug 2026 23:08:20 -0500 Subject: [PATCH 72/90] finish up updating il battle display --- src/mods/ilbattle.cpp | 136 ++++++++++++------------------------------ 1 file changed, 39 insertions(+), 97 deletions(-) diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 2761c281..3e65d2bb 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -8,6 +8,7 @@ #include "systems/savest.h" #include "systems/textinfo.h" #include "utils/draw.h" +#include "utils/macro_utils.h" #include "utils/patch.h" #include "utils/relutil.h" #include "utils/timerdisp.h" @@ -38,10 +39,10 @@ static constexpr s32 Y = 48; static constexpr u32 CWIDTH = draw::DEBUG_CHAR_WIDTH; static constexpr u32 CHEIGHT = 16; static constexpr s32 TEXT_POS_X = X - 12 * CWIDTH; -// time constants -static constexpr u32 SECOND_FRAMES = 60; // frames per second -static constexpr u32 MINUTE_FRAMES = SECOND_FRAMES * 60; // frames per minute -static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; // frames per hour +// buzzer beater constants +static constexpr u16 BUZZER_BEATER_FRAMES_PER_COLOR = 5; +static const GXColor s_buzzer_beater_color_list[] = { + draw::RED, draw::ORANGE, draw::GOLD, draw::GREEN, draw::BLUE, draw::BRIGHT_PURPLE}; // battle trackers static u32 s_battle_frames = 0; static u32 s_battle_length = 0; @@ -68,42 +69,36 @@ using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; +void draw_battle_text_main(s32 pos_x, GXColor color, bool incr_row, char *text) { + textinfo::draw(Mod::IlBattle, Slot::Left, pos_x, color, incr_row, text); +} + void draw_buzzer_beater_row(GXColor color) { - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, color, true, "EPIC BUZZER BEATER B)"); + draw_battle_text_main(TEXT_POS_X, color, true, "EPIC BUZZER BEATER B)"); } static void old_buzzer_display(u32 start_y) { - s_buzzer_message_count = (s_buzzer_message_count + 1) % 30; - if (s_buzzer_message_count >= 0) - draw::debug_text(X - 12 * CWIDTH, start_y + 1 * CHEIGHT, draw::RED, - "EPIC BUZZER BEATER B)"); - if (s_buzzer_message_count >= 5) - draw::debug_text(X - 12 * CWIDTH, start_y + 2 * CHEIGHT, draw::ORANGE, - "EPIC BUZZER BEATER B)"); - if (s_buzzer_message_count >= 10) - draw::debug_text(X - 12 * CWIDTH, start_y + 3 * CHEIGHT, draw::GOLD, - "EPIC BUZZER BEATER B)"); - if (s_buzzer_message_count >= 15) - draw::debug_text(X - 12 * CWIDTH, start_y + 4 * CHEIGHT, draw::GREEN, - "EPIC BUZZER BEATER B)"); - if (s_buzzer_message_count >= 20) - draw::debug_text(X - 12 * CWIDTH, start_y + 5 * CHEIGHT, draw::BLUE, - "EPIC BUZZER BEATER B)"); - if (s_buzzer_message_count >= 25) - draw::debug_text(X - 12 * CWIDTH, start_y + 6 * CHEIGHT, draw::BRIGHT_PURPLE, - "EPIC BUZZER BEATER B)"); + constexpr u16 COLOR_COUNT = LEN(s_buzzer_beater_color_list); + constexpr u16 TOTAL_FRAMES = COLOR_COUNT * BUZZER_BEATER_FRAMES_PER_COLOR; + + s_buzzer_message_count = (s_buzzer_message_count + 1) % TOTAL_FRAMES; + for (u16 col = 0; col < COLOR_COUNT; col++) { + if (s_buzzer_message_count >= col * BUZZER_BEATER_FRAMES_PER_COLOR) { + draw_buzzer_beater_row(s_buzzer_beater_color_list[col]); + } + } } void draw_battle_text(GXColor color, char *text) { - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, color, false, text); + draw_battle_text_main(TEXT_POS_X, color, false, text); } void draw_battle_breakdown_text(GXColor color, char *text) { - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X + 3 * CWIDTH, color, false, text); + draw_battle_text_main(TEXT_POS_X + 3 * CWIDTH, color, false, text); } // When we only want to display a single time or number (like attempt count) -// Special formatting like time [bananas] is handled on a per-case basis, however +// Special formatting like "time [bananas]"" is handled on a per-case basis, however void draw_battle_num(GXColor color, u32 num, Format format) { // No prefix specified since IL Battle uses custom spacing between its prefixes and times textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, color, "", num, format); @@ -111,79 +106,39 @@ void draw_battle_num(GXColor color, u32 num, Format format) { // pre cleanup static void battle_display(GXColor text_color) { - // u32 battle_hours = s_battle_frames / HOUR_FRAMES; - // u32 battle_minutes = s_battle_frames % HOUR_FRAMES / MINUTE_FRAMES; - // u32 battle_seconds = s_battle_frames % MINUTE_FRAMES / SECOND_FRAMES; - - // u32 best_seconds = s_best_frames / SECOND_FRAMES; - // u32 best_centiseconds = (s_best_frames % SECOND_FRAMES) * 100 / 60; - - // u32 best_score_seconds = s_best_score_frames / SECOND_FRAMES; - // u32 best_score_centiseconds = (s_best_score_frames % SECOND_FRAMES) * 100 / 60; - u32 current_y = Y; GXColor time_color = s_time_buzzer ? draw::num_to_rainbow(s_rainbow) : text_color; GXColor score_color = s_score_buzzer ? draw::num_to_rainbow(s_rainbow) : text_color; - // draw::debug_text(X - 12 * CWIDTH, Y, text_color, "ELAPSED:"); - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "ELAPSED:"); - /* if (battle_hours > 0) { - draw::debug_text(X - 6, Y, text_color, "%d:%02d:%02d", battle_hours, battle_minutes, - battle_seconds); - } else { - draw::debug_text(X - 6, Y, text_color, "%02d:%02d", battle_minutes, battle_seconds); - } */ - textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, text_color, "", s_battle_frames, - Format::MinutesAlwaysLeadingZero); - /* textinfo::draw(Mod::IlBattle, Slot::Left, textinfo::get_slot_timer_x_pos(Slot::Left), - text_color, true, "%d", s_battle_frames); */ + draw_battle_text(text_color, "ELAPSED:"); + draw_battle_num(text_color, s_battle_frames, Format::MinutesAlwaysLeadingZero); if (pref::get(pref::Pref::IlBattleShowTime)) { - current_y += CHEIGHT; - // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST TIME:"); - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "BEST TIME:"); + draw_battle_text(text_color, "BEST TIME:"); + if (pref::get(pref::Pref::IlBattleTieCount) && s_best_frames_ties > 0) { - /* draw::debug_text(X - 6, current_y, time_color, "%d.%02d (%d)", best_seconds, - best_centiseconds, s_best_frames_ties + 1); */ char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_frames, Format::SecondsOnly); - /* textinfo::draw(Mod::IlBattle, Slot::Left, - textinfo::module_and_slot_to_timer_x_pos(Mod::IlBattle, Slot::Left), - text_color, true, "%s (%d)", time_buf, s_best_frames_ties + 1); */ textinfo::draw_aligned(Mod::IlBattle, Slot::Left, time_color, "%s (%d)", time_buf, s_best_frames_ties + 1); } else { - /* draw::debug_text(X - 6, current_y, time_color, "%d.%02d", best_seconds, - best_centiseconds); */ - textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, time_color, "", s_best_frames, - Format::SecondsOnly); + draw_battle_num(text_color, s_best_frames, Format::SecondsOnly); } } if (pref::get(pref::Pref::IlBattleShowScore)) { - current_y += CHEIGHT; - // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BEST SCORE:"); - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, text_color, false, "BEST SCORE:"); + draw_battle_text(text_color, "BEST SCORE:"); if (pref::get(pref::Pref::IlBattleTieCount) && s_best_score_ties > 0) { - /* draw::debug_text(X - 6, current_y, score_color, "%d (%d)", s_best_score, - s_best_score_ties + 1); */ textinfo::draw_aligned(Mod::IlBattle, Slot::Left, score_color, "%d (%d)", s_best_score, s_best_score_ties + 1); } else { - // draw::debug_text(X - 6, current_y, score_color, "%d", s_best_score); - textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, score_color, "", s_best_score, - Format::Unformatted); + draw_battle_num(score_color, s_best_score, Format::Unformatted); } // breakdown u8 breakdown_value = pref::get(pref::Pref::IlBattleBreakdown); if (breakdown_value == 1) { // minimal - current_y += CHEIGHT; - /* draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "BREAKDOWN:"); - draw::debug_text(X - 6, current_y, text_color, "%d.%02d [%d]", best_score_seconds, - best_score_centiseconds, s_best_score_bananas); */ - draw_battle_text(text_color, "BREAKDOWN:"); char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_score_frames, Format::SecondsOnly); @@ -191,30 +146,16 @@ static void battle_display(GXColor text_color) { s_best_score_bananas); } else if (breakdown_value == 2) { // full - current_y += CHEIGHT; - // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " BANANAS:"); - // draw::debug_text(X - 6, current_y, text_color, "%d", s_best_score_bananas); - draw_battle_text(text_color, " BANANAS:"); + draw_battle_breakdown_text(text_color, "BANANAS:"); textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", s_best_score_bananas); - current_y += CHEIGHT; - /* draw::debug_text(X - 12 * CWIDTH, current_y, text_color, " TIMER:"); - draw::debug_text(X - 6, current_y, text_color, "%d.%02d", best_score_seconds, - best_score_centiseconds); */ - - draw_battle_text(text_color, " TIMER:"); - textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, text_color, "", - s_best_score_frames, Format::SecondsOnly); + draw_battle_breakdown_text(text_color, "TIMER:"); + draw_battle_num(text_color, s_best_score_frames, Format::SecondsOnly); } } if (pref::get(pref::Pref::IlBattleAttemptCount)) { - current_y += CHEIGHT; - // draw::debug_text(X - 12 * CWIDTH, current_y, text_color, "ATTEMPTS:"); - // draw::debug_text(X - 6, current_y, text_color, "%d", s_attempts); - draw_battle_text(text_color, "ATTEMPTS:"); - textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", s_attempts); - // textinfo::draw(Mod::IlBattle, Slot::Left, ) + draw_battle_num(text_color, s_attempts, Format::Unformatted); } if (pref::get(pref::Pref::IlBattleBuzzerOld) && @@ -448,14 +389,15 @@ void disp() { u8 input = pref::get(pref::Pref::IlBattleReadyBind); char buf[25]; binds::get_bind_str(input, buf); - draw::debug_text(X - 12 * CWIDTH, Y, draw::LIGHT_PURPLE, "NOT READY"); - draw::debug_text(X - 12 * CWIDTH, Y + CHEIGHT, draw::LIGHT_PURPLE, "%s to ready", buf); + draw_battle_text_main(TEXT_POS_X, draw::LIGHT_PURPLE, true, "NOT READY"); + textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, + "%s to ready", buf); break; } case IlBattleState::WaitForFirstRetry: { if (mkb::main_mode == mkb::MD_GAME) { - draw::debug_text(X - 12 * CWIDTH, Y, draw::GOLD, "READY"); - draw::debug_text(X - 12 * CWIDTH, Y + CHEIGHT, draw::GOLD, "Retry to begin"); + draw_battle_text_main(TEXT_POS_X, draw::GOLD, true, "READY"); + draw_battle_text_main(TEXT_POS_X, draw::GOLD, true, "Retry to begin"); } break; } @@ -463,7 +405,7 @@ void disp() { case IlBattleState::BuzzerBeater: { if (s_main_mode_play_timer > 0 && s_battle_stage_id != mkb::current_stage_id && mkb::main_mode == mkb::MD_GAME) { - draw::debug_text(X - 12 * CWIDTH, Y, draw::RED, "WRONG STAGE"); + draw_battle_text_main(TEXT_POS_X, draw::RED, true, "WRONG STAGE"); } else { battle_display(draw::LIGHT_GREEN); } From bd92011ccb409b5bbd96c48eddcf878d068ba949 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 23 Aug 2026 00:06:52 -0500 Subject: [PATCH 73/90] update names in textinfo and func calls --- src/mods/cmseg.cpp | 4 +- src/mods/deathcounter.cpp | 12 ++- src/mods/ilbattle.cpp | 2 +- src/mods/iw.cpp | 4 +- src/mods/storytimer.cpp | 22 ++++- src/mods/timer.cpp | 25 +++-- src/systems/textinfo.cpp | 189 ++++++++++++++------------------------ src/systems/textinfo.h | 19 ++-- 8 files changed, 123 insertions(+), 154 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index cbee602d..08e99be9 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -425,8 +425,8 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - // timerdisp::draw_timer_right_side(static_cast(s_seg_time), "SEG:", 0, color, false); - textinfo::draw_timer(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), + textinfo::draw_timer(textinfo::Module::CmSeg, textinfo::Slot::Right, color, + "SEG:", static_cast(s_seg_time), timerdisp::TimeFormat::AlwaysLeadNonHours); } } diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 82315944..aea1e72d 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -25,6 +25,10 @@ static savest::Action s_previous_frame_action = savest::Action::None; // Flag to determine when we should/shouldn't increment the death counter static bool s_can_incr_death_counter = false; +using Mod = textinfo::Module; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + u32 get_total_death_count() { u32 total = 0; for (u16 k = 0; k < WORLD_COUNT; k++) { @@ -153,8 +157,12 @@ void disp() { /* draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", get_total_death_count()); */ - textinfo::draw_main(textinfo::Slot::Left, COUNTER_DISPLAY_X_POS, draw::WHITE, "Deaths:%d", - get_total_death_count()); + /* textinfo::draw_main(textinfo::Slot::Left, COUNTER_DISPLAY_X_POS, draw::WHITE, + "Deaths:%d", get_total_death_count()); */ + // Technically not a timer, but we can still use this function without specifying any + // special formatting + textinfo::draw_timer(Mod::DeathCounter, Slot::Left, draw::WHITE, + "Deaths:", get_total_death_count(), Format::Unformatted); } } diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 3e65d2bb..b204eb0f 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -101,7 +101,7 @@ void draw_battle_breakdown_text(GXColor color, char *text) { // Special formatting like "time [bananas]"" is handled on a per-case basis, however void draw_battle_num(GXColor color, u32 num, Format format) { // No prefix specified since IL Battle uses custom spacing between its prefixes and times - textinfo::draw_timer_main(Mod::IlBattle, Slot::Left, color, "", num, format); + textinfo::draw_timer(Mod::IlBattle, Slot::Left, color, "", num, format); } // pre cleanup diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index 6d1a3692..39883d7f 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -125,8 +125,8 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - // timerdisp::draw_timer_right_side(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); - textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + textinfo::draw_timer(textinfo::Module::IwTimer, textinfo::Slot::Right, draw::WHITE, + "IW:", static_cast(s_iw_time), timerdisp::TimeFormat::AlwaysLeadNonHours); } diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 903f8e0b..1cd02a15 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -29,6 +29,10 @@ constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world +using Mod = textinfo::Module; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + // --- some getters that other files can use (if needed) --- WorldTimer get_world_timer_info(u16 world_idx) { @@ -145,6 +149,11 @@ void tick() { // --- display stuff --- +// Special case of textinfo::draw_timer() useful for us +void draw_timer(char *prefix, u32 frames, Format format) { + textinfo::draw_timer(Mod::LoadlessTimer, Slot::Left, draw::WHITE, prefix, frames, format); +} + // The run breakdown screen replaces the segment timer at the end of the run // if the pref for it is on bool should_display_timer(TimerType type) { @@ -249,8 +258,9 @@ void draw_timers() { /* timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, "Time:", loadless_time, false, draw::WHITE); */ // textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Time: %d", get_loadless_time()); - textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, "Time:", get_loadless_time(), - timerdisp::TimeFormat::AlwaysLeadNonHours); + /* textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, "Time:", get_loadless_time(), + timerdisp::TimeFormat::AlwaysLeadNonHours); */ + draw_timer("Time:", get_loadless_time(), Format::AlwaysLeadNonHours); } u16 world_idx = get_current_segment_idx(); // index of the current world @@ -261,9 +271,10 @@ void draw_timers() { "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); */ /* textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Seg: %d", s_world_timer[world_idx].segment); */ - textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, + /* textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, "Seg:", s_world_timer[world_idx].segment, - timerdisp::TimeFormat::AlwaysLeadNonHours); + timerdisp::TimeFormat::AlwaysLeadNonHours); */ + draw_timer("Seg:", s_world_timer[world_idx].segment, Format::AlwaysLeadNonHours); } } @@ -299,7 +310,8 @@ void draw_breakdown_screen() { // draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); // textinfo::draw(textinfo::Slot::Left, draw::WHITE, row_info_buf[idx]); - textinfo::draw_main(textinfo::Slot::Left, pos.x, draw::WHITE, row_info_buf[idx]); + // textinfo::draw_main(textinfo::Slot::Left, pos.x, draw::WHITE, row_info_buf[idx]); + textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos.x, draw::WHITE, true, row_info_buf[idx]); } // For the totals row diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index ea86c33e..e05840c2 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -58,17 +58,18 @@ void disp() { } // u32 row = 1; + using Mod = textinfo::Module; + using Slot = textinfo::Slot; + using Format = timerdisp::TimeFormat; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - // timerdisp::draw_timer_right_side(s_rta_timer, "RTA:", row++, draw::WHITE, true); - textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "RTA:", s_rta_timer, - timerdisp::TimeFormat::SecondsOnly); + textinfo::draw_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + Format::SecondsOnly); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - // timerdisp::draw_timer_right_side(s_pause_timer, "PAU:", row++, draw::WHITE, true); - textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "PAU:", s_pause_timer, - timerdisp::TimeFormat::SecondsOnly); + textinfo::draw_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, + Format::SecondsOnly); } switch (mkb::sub_mode) { @@ -84,18 +85,14 @@ void disp() { u32 framesave = validate::get_framesave(); if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { - /* timerdisp::draw_subtick_timer(mkb::mode_info.stage_time_frames_remaining, "SUB:", row++, - draw::WHITE, true, framesave, false); */ - textinfo::draw_subtick_timer(textinfo::Slot::Right, draw::WHITE, - "SUB:", mkb::mode_info.stage_time_frames_remaining, framesave, - false); + textinfo::draw_subtick_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + framesave, false); } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { - // timerdisp::draw_percentage(framesave, "FSV:", row++, draw::WHITE); - s32 num_x = textinfo::get_slot_timer_x_pos(textinfo::Slot::Right); + s32 num_x = textinfo::get_slot_timer_x_pos(Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; - textinfo::draw_main(textinfo::Slot::Right, x, draw::WHITE, "FSV:%2d%", framesave); + textinfo::draw(Mod::RtaTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); } } diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 9476c032..87b6bf6c 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -16,7 +16,12 @@ static const Slot s_slot_list[]{ static u16 s_active_row[LEN(s_slot_list)] = {}; -// Where the numbers get lined up +// In addition to the generic text drawing functions, it's useful to be able to line up our displays +// by numbers +// Each module may have slightly different requirements for this alignment + +// Where the numbers get lined up (this acts as the "default" number alignment per slot, but this +// can be overridden on a per-mod basis) s32 get_slot_timer_x_pos(Slot slot) { switch (slot) { case Slot::Left: @@ -28,52 +33,27 @@ s32 get_slot_timer_x_pos(Slot slot) { } } -/* Slot module_to_slot(Module module) { +s32 module_and_slot_to_timer_x_pos(Module module, Slot slot) { switch (module) { case Module::IlBattle: - return Slot::Left; + // IL Battle needs different number alignment from loadless timer/death counter + return 160 - 6; case Module::DeathCounter: - return Slot::Left; + return get_slot_timer_x_pos(Slot::Left); case Module::LoadlessTimer: - return Slot::Left; + return get_slot_timer_x_pos(Slot::Left); case Module::IwTimer: - return Slot::Right; + return get_slot_timer_x_pos(Slot::Right); case Module::CmSeg: - return Slot::Right; + return get_slot_timer_x_pos(Slot::Right); case Module::RtaTimer: - return Slot::Right; + return get_slot_timer_x_pos(Slot::Right); case Module::FrameSave: - return Slot::Right; - default: - return Slot::Unused; - } -} - -u16 module_to_min_row(Module module) { - switch (module) { - case Module::RtaTimer: - return 2; + return get_slot_timer_x_pos(Slot::Right); default: return 0; } -} */ - -/* void draw(Module module, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { - Slot slot = module_to_slot(module); - u16 min_row = module_to_min_row(module); - for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - u16 row = MAX(min_row, s_active_row[k]); - s32 y = timerdisp::row_number_to_vertical_pos(row); - - draw::debug_text(pos_x, y, color, format); - - if (incr_row) { - s_active_row[k] = row + 1; - } - } - } -} */ +} // Per-slot minimum row number. Individual modules are allowed to have higher min rows, however u16 get_slot_min_row(Slot slot) { @@ -110,41 +90,24 @@ u16 module_and_slot_to_min_row(Module module, Slot slot) { } } -s32 module_and_slot_to_timer_x_pos(Module module, Slot slot) { - switch (module) { - case Module::IlBattle: - return 160 - 6; - case Module::DeathCounter: - return 102; - case Module::LoadlessTimer: - return 102; - case Module::IwTimer: - return 378 + 4 * draw::DEBUG_CHAR_WIDTH; - case Module::CmSeg: - return 378 + 4 * draw::DEBUG_CHAR_WIDTH; - case Module::RtaTimer: - return 378 + 4 * draw::DEBUG_CHAR_WIDTH; - case Module::FrameSave: - return 378 + 4 * draw::DEBUG_CHAR_WIDTH; - default: - return 0; - } -} - -void draw_v(Module module, - Slot slot, - s32 pos_x, - GXColor color, - bool incr_row, - char *format, - va_list args) { - u16 min_row = module_and_slot_to_min_row(module, slot); +// The main text drawing function +// We will almost always want to input true for the incr_row argument +// However, in cases where we want to draw two (or more) pieces of text in the same slot and in +// the same row with custom horizontal spacing, inputting false can be useful +// This is used in ilbattle.cpp +void draw_main_v(s32 min_row, + Slot slot, + s32 pos_x, + GXColor color, + bool incr_row, + char *format, + va_list args) { for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { u16 row = MAX(min_row, s_active_row[k]); - s32 y = timerdisp::row_number_to_vertical_pos(row); + s32 pos_y = timerdisp::row_number_to_vertical_pos(row); - draw::debug_text_v(pos_x, y, color, format, args); + draw::debug_text_v(pos_x, pos_y, color, format, args); if (incr_row) { s_active_row[k] = row + 1; @@ -153,33 +116,22 @@ void draw_v(Module module, } } -// We will almost always want to input true for the incr_row argument -// However, in cases where we want to draw two pieces of text in the same slot and in the same row -// with custom horizontal spacing, inputting false can be useful -// This is used in ilbattle.cpp (TODO) +void draw_v(Module module, + Slot slot, + s32 pos_x, + GXColor color, + bool incr_row, + char *format, + va_list args) { + draw_main_v(module_and_slot_to_min_row(module, slot), slot, pos_x, color, incr_row, format, + args); +} + void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { va_list args; va_start(args, format); draw_v(module, slot, pos_x, color, incr_row, format, args); va_end(args); - /* u16 min_row = module_and_slot_to_min_row(module, slot); - for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - u16 row = MAX(min_row, s_active_row[k]); - s32 y = timerdisp::row_number_to_vertical_pos(row); - - va_list args; - va_start(args, format); - draw::debug_text_v(pos_x, y, color, format, args); - va_end(args); - - // draw::debug_text(pos_x, y, color, format); - - if (incr_row) { - s_active_row[k] = row + 1; - } - } - } */ } // When we want to align according to module_and_slot_to_timer_x_pos() @@ -190,13 +142,12 @@ void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...) { va_end(args); } -void draw_timer_main(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format) { - // s32 num_x = get_slot_timer_x_pos(slot); +void draw_timer(Module module, + Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format) { s32 num_x = module_and_slot_to_timer_x_pos(module, slot); u32 prefix_len = mkb::strlen(prefix); s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; @@ -204,8 +155,6 @@ void draw_timer_main(Module module, char time_buf[16] = {}; timerdisp::format_signed_time(time_buf, frames, format); draw(module, slot, x, color, true, "%s%s", prefix, time_buf); - // draw(module, slot, x, color, true, time_buf); - // draw(module, slot, x, color, true, "%s", prefix); } // We use "Slot" to determine which set of timers/text rows to stack our current text row under @@ -214,7 +163,7 @@ void draw_timer_main(Module module, // The breakdown row x position depends on the row number since we want to align things based on the // numbers and not the left hand side of the text // TODO: rename to just draw() -void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { +/* void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); @@ -224,21 +173,20 @@ void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { s_active_row[k]++; } } -} +} */ // Aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the // left depending on string length -void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { - s32 num_x = get_slot_timer_x_pos(slot); - u32 prefix_len = mkb::strlen(prefix); - s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; +/* void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) +{ s32 num_x = get_slot_timer_x_pos(slot); u32 prefix_len = mkb::strlen(prefix); s32 x = num_x - +prefix_len * draw::DEBUG_CHAR_WIDTH; char time_buf[16] = {}; timerdisp::format_signed_time(time_buf, frames, format); draw_main(slot, x, color, "%s%s", prefix, time_buf); -} +} */ -void draw_subtick_timer(Slot slot, +/* void draw_subtick_timer(Slot slot, GXColor color, char *prefix, s32 frames, @@ -250,23 +198,28 @@ void draw_subtick_timer(Slot slot, char time_buf[16] = {}; timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); - draw_main(slot, x, color, "%s%s", prefix, time_buf); -} + // draw_main(slot, x, color, "%s%s", prefix, time_buf); +} */ -u16 get_slot_starting_row(Slot slot) { - switch (slot) { - case Slot::Left: - return 2; - case Slot::Right: - return 0; - default: - return 0; - } +void draw_subtick_timer(Module module, + Slot slot, + GXColor color, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision) { + s32 num_x = get_slot_timer_x_pos(slot); + u32 prefix_len = mkb::strlen(prefix); + s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); + draw(module, slot, x, color, true, "%s%s", prefix, time_buf); } void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { - s_active_row[k] = get_slot_starting_row(s_slot_list[k]); + s_active_row[k] = get_slot_min_row(s_slot_list[k]); } } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 3dc19036..be183ee7 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -27,16 +27,15 @@ s32 module_and_slot_to_timer_x_pos(Module module, Slot slot); void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); -void draw_timer_main(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format); - -void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...); -void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); -void draw_subtick_timer(Slot slot, + +void draw_timer(Module module, + Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format); +void draw_subtick_timer(Module module, + Slot slot, GXColor color, char *prefix, s32 frames, From 5486527fd48d27994c25c4606e026e9d82b7c4ee Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 23 Aug 2026 00:59:12 -0500 Subject: [PATCH 74/90] renames --- src/mods/timer.cpp | 2 +- src/systems/textinfo.cpp | 74 +++++++++------------------------------- src/systems/textinfo.h | 4 +-- 3 files changed, 20 insertions(+), 60 deletions(-) diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index e05840c2..181c20ba 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -90,7 +90,7 @@ void disp() { } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { - s32 num_x = textinfo::get_slot_timer_x_pos(Slot::Right); + s32 num_x = textinfo::module_and_slot_to_x_alignment(Mod::RtaTimer, Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; textinfo::draw(Mod::RtaTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); } diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 87b6bf6c..144099b1 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -16,13 +16,15 @@ static const Slot s_slot_list[]{ static u16 s_active_row[LEN(s_slot_list)] = {}; +// "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get +// stacked on top of each other // In addition to the generic text drawing functions, it's useful to be able to line up our displays // by numbers // Each module may have slightly different requirements for this alignment // Where the numbers get lined up (this acts as the "default" number alignment per slot, but this // can be overridden on a per-mod basis) -s32 get_slot_timer_x_pos(Slot slot) { +s32 get_slot_x_alignment(Slot slot) { switch (slot) { case Slot::Left: return 102; @@ -33,23 +35,23 @@ s32 get_slot_timer_x_pos(Slot slot) { } } -s32 module_and_slot_to_timer_x_pos(Module module, Slot slot) { +s32 module_and_slot_to_x_alignment(Module module, Slot slot) { switch (module) { case Module::IlBattle: // IL Battle needs different number alignment from loadless timer/death counter return 160 - 6; case Module::DeathCounter: - return get_slot_timer_x_pos(Slot::Left); + return get_slot_x_alignment(Slot::Left); case Module::LoadlessTimer: - return get_slot_timer_x_pos(Slot::Left); + return get_slot_x_alignment(Slot::Left); case Module::IwTimer: - return get_slot_timer_x_pos(Slot::Right); + return get_slot_x_alignment(Slot::Right); case Module::CmSeg: - return get_slot_timer_x_pos(Slot::Right); + return get_slot_x_alignment(Slot::Right); case Module::RtaTimer: - return get_slot_timer_x_pos(Slot::Right); + return get_slot_x_alignment(Slot::Right); case Module::FrameSave: - return get_slot_timer_x_pos(Slot::Right); + return get_slot_x_alignment(Slot::Right); default: return 0; } @@ -61,7 +63,7 @@ u16 get_slot_min_row(Slot slot) { case Slot::Left: return 2; case Slot::Right: - return 0; + return 1; default: return 0; } @@ -134,21 +136,23 @@ void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, cha va_end(args); } -// When we want to align according to module_and_slot_to_timer_x_pos() +// When we want to align according to module_and_slot_to_x_alignment() void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...) { va_list args; va_start(args, format); - draw_v(module, slot, module_and_slot_to_timer_x_pos(module, slot), color, true, format, args); + draw_v(module, slot, module_and_slot_to_x_alignment(module, slot), color, true, format, args); va_end(args); } +// Aligns the time according to get_slot_x_alignment; the prefix gets automatically moved to the +// left depending on string length void draw_timer(Module module, Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { - s32 num_x = module_and_slot_to_timer_x_pos(module, slot); + s32 num_x = module_and_slot_to_x_alignment(module, slot); u32 prefix_len = mkb::strlen(prefix); s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; @@ -157,50 +161,6 @@ void draw_timer(Module module, draw(module, slot, x, color, true, "%s%s", prefix, time_buf); } -// We use "Slot" to determine which set of timers/text rows to stack our current text row under -// Each mod might need to use a slightly different x position, though; see storytimer.cpp for -// instance -// The breakdown row x position depends on the row number since we want to align things based on the -// numbers and not the left hand side of the text -// TODO: rename to just draw() -/* void draw_main(Slot slot, s32 pos_x, GXColor color, char *format, ...) { - for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - s32 y = timerdisp::row_number_to_vertical_pos(s_active_row[k]); - - draw::debug_text(pos_x, y, color, format); - - s_active_row[k]++; - } - } -} */ - -// Aligns the time according to get_slot_timer_x_pos; the prefix gets automatically moved to the -// left depending on string length -/* void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) -{ s32 num_x = get_slot_timer_x_pos(slot); u32 prefix_len = mkb::strlen(prefix); s32 x = num_x - -prefix_len * draw::DEBUG_CHAR_WIDTH; - - char time_buf[16] = {}; - timerdisp::format_signed_time(time_buf, frames, format); - draw_main(slot, x, color, "%s%s", prefix, time_buf); -} */ - -/* void draw_subtick_timer(Slot slot, - GXColor color, - char *prefix, - s32 frames, - u32 framesave, - bool extra_precision) { - s32 num_x = get_slot_timer_x_pos(slot); - u32 prefix_len = mkb::strlen(prefix); - s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; - - char time_buf[16] = {}; - timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); - // draw_main(slot, x, color, "%s%s", prefix, time_buf); -} */ - void draw_subtick_timer(Module module, Slot slot, GXColor color, @@ -208,7 +168,7 @@ void draw_subtick_timer(Module module, s32 frames, u32 framesave, bool extra_precision) { - s32 num_x = get_slot_timer_x_pos(slot); + s32 num_x = module_and_slot_to_x_alignment(module, slot); u32 prefix_len = mkb::strlen(prefix); s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index be183ee7..2ed28339 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -22,8 +22,8 @@ enum class Module { LoadlessTimer, }; -s32 get_slot_timer_x_pos(Slot slot); -s32 module_and_slot_to_timer_x_pos(Module module, Slot slot); +s32 get_slot_x_alignment(Slot slot); +s32 module_and_slot_to_x_alignment(Module module, Slot slot); void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); From 92a054dfaba972980642ff16204f9ef8e487c938 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 23 Aug 2026 00:59:24 -0500 Subject: [PATCH 75/90] update the rest of storytimer --- src/mods/storytimer.cpp | 60 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 1cd02a15..feb92aeb 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -201,7 +201,7 @@ bool should_display_timer(TimerType type) { } // Doing this for now until a better display setup is figured out -u16 get_timer_row(TimerType type) { +/* u16 get_timer_row(TimerType type) { u16 row = STARTING_ROW; bool is_displaying_death_counter = deathcounter::should_display_death_counter(); @@ -234,7 +234,7 @@ TimerDisplayInfo get_timer_display_info(TimerType type) { } else { return {SEGMENT_TIMER_LOCATION_X, get_timer_row(type), SEGMENT_TIMER_TEXT_OFFSET}; } -} +} */ u16 get_current_segment_idx() { // mkb::scen_info.world gets reset to 0 on the file screen, so if we accidentally exit game to @@ -279,8 +279,9 @@ void draw_timers() { } // We only use this function for 0 <= row <= 10 -Vec2d get_breakdown_row_position(u16 row) { - u16 starting_row = get_timer_row(TimerType::Segment); +/* Vec2d get_breakdown_row_position(u16 row) { + // u16 starting_row = get_timer_row(TimerType::Segment); + u16 starting_row = 0; // placeholder float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); if (row < WORLD_COUNT - 1) { return {BREAKDOWN_ROW_LOCATION_X, pos_y}; @@ -289,38 +290,49 @@ Vec2d get_breakdown_row_position(u16 row) { } else { // For the totals row return {TOTALS_ROW_LOCATION_X, pos_y}; } +} */ + +// We only use this function for 0 <= row <= 10 +s32 get_breakdown_row_x_pos(u16 row) { + s32 num_x_pos = textinfo::module_and_slot_to_x_alignment(Mod::LoadlessTimer, Slot::Left); + if (row < WORLD_COUNT - 1) { // "Wk:" is 3 characters long if 1 <= k <= 9 + return num_x_pos - 3 * draw::DEBUG_CHAR_WIDTH; + } else if (row == WORLD_COUNT - 1) { // "W10:" is 4 characters long + return num_x_pos - 4 * draw::DEBUG_CHAR_WIDTH; + } else { // "Totals:" is 7 characters long + return num_x_pos - 7 * draw::DEBUG_CHAR_WIDTH; + } } void draw_breakdown_screen() { // Format: "Wk: world k split time (world k segment time) (world k deaths)" - char split_buf[WORLD_COUNT][16] = {}; - char seg_buf[WORLD_COUNT][16] = {}; - char row_info_buf[WORLD_COUNT][32] = {}; + char split_buf[16] = {}; + char seg_buf[16] = {}; + char row_info_buf[32] = {}; for (u16 idx = 0; idx < WORLD_COUNT; idx++) { - Vec2d pos = get_breakdown_row_position(idx); + // Vec2d pos = get_breakdown_row_position(idx); + s32 pos_x = get_breakdown_row_x_pos(idx); u32 world_deaths = deathcounter::get_world_death_count(idx); - timerdisp::format_time(split_buf[idx], get_split_timer_for_world(idx), - timerdisp::TimeFormat::MinutesAlwaysLeadingZero); - timerdisp::format_time(seg_buf[idx], s_world_timer[idx].segment, - timerdisp::TimeFormat::MinutesAlwaysLeadingZero); - mkb::sprintf(row_info_buf[idx], "W%d:%s (%s) (%d)", idx + 1, split_buf[idx], seg_buf[idx], - world_deaths); - - // draw::debug_text(pos.x, pos.y, draw::WHITE, "%s", row_info_buf[idx]); - // textinfo::draw(textinfo::Slot::Left, draw::WHITE, row_info_buf[idx]); - // textinfo::draw_main(textinfo::Slot::Left, pos.x, draw::WHITE, row_info_buf[idx]); - textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos.x, draw::WHITE, true, row_info_buf[idx]); + timerdisp::format_time(split_buf, get_split_timer_for_world(idx), + Format::MinutesAlwaysLeadingZero); + timerdisp::format_time(seg_buf, s_world_timer[idx].segment, + Format::MinutesAlwaysLeadingZero); + mkb::sprintf(row_info_buf, "W%d:%s (%s) (%d)", idx + 1, split_buf, seg_buf, world_deaths); + + textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos_x, draw::WHITE, true, row_info_buf); } // For the totals row - char totals_row_buf[32] = {}; - Vec2d totals_row_pos = get_breakdown_row_position(WORLD_COUNT); + // Vec2d totals_row_pos = get_breakdown_row_position(WORLD_COUNT); + s32 totals_x_pos = get_breakdown_row_x_pos(WORLD_COUNT); u32 total_deaths = deathcounter::get_total_death_count(); - mkb::sprintf(totals_row_buf, "Totals:%s (%d)", split_buf[WORLD_COUNT - 1], total_deaths); - draw::debug_text(totals_row_pos.x, totals_row_pos.y, draw::WHITE, "%s", totals_row_buf); + char total_time_buf[16] = {}; + timerdisp::format_time(total_time_buf, get_loadless_time(), Format::MinutesAlwaysLeadingZero); + textinfo::draw(Mod::LoadlessTimer, Slot::Left, totals_x_pos, draw::WHITE, true, + "Totals:%s (%d)", total_time_buf, total_deaths); } bool should_not_display_timer_at_all() { @@ -336,7 +348,7 @@ bool should_not_display_timer_at_all() { void disp() { if (should_not_display_timer_at_all()) { - return; // testing, remember to uncomment + return; } draw_timers(); From 8de98f4c1851a0deb007c379e56f83697171bb84 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 23 Aug 2026 01:20:00 -0500 Subject: [PATCH 76/90] cleanup --- src/mods/deathcounter.cpp | 9 ----- src/mods/storytimer.cpp | 69 --------------------------------------- src/systems/textinfo.cpp | 2 +- src/utils/timerdisp.cpp | 27 +++++++-------- src/utils/timerdisp.h | 9 ++--- 5 files changed, 20 insertions(+), 96 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index aea1e72d..d7e0454f 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -14,10 +14,6 @@ namespace deathcounter { -constexpr u16 COUNTER_DISPLAY_Y_POS = 56; -constexpr u16 COUNTER_DISPLAY_X_POS = 18; -constexpr u16 COUNTER_NUMBER_X_POS = COUNTER_DISPLAY_X_POS + 7 * draw::DEBUG_CHAR_WIDTH; - constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; static u32 s_world_death_count[WORLD_COUNT] = {}; @@ -154,11 +150,6 @@ void disp() { } if (should_display_death_counter()) { - /* draw::debug_text(COUNTER_DISPLAY_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "Deaths:"); - draw::debug_text(COUNTER_NUMBER_X_POS, COUNTER_DISPLAY_Y_POS, draw::WHITE, "%d", - get_total_death_count()); */ - /* textinfo::draw_main(textinfo::Slot::Left, COUNTER_DISPLAY_X_POS, draw::WHITE, - "Deaths:%d", get_total_death_count()); */ // Technically not a timer, but we can still use this function without specifying any // special formatting textinfo::draw_timer(Mod::DeathCounter, Slot::Left, draw::WHITE, diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index feb92aeb..4de4949e 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -200,42 +200,6 @@ bool should_display_timer(TimerType type) { } } -// Doing this for now until a better display setup is figured out -/* u16 get_timer_row(TimerType type) { - u16 row = STARTING_ROW; - bool is_displaying_death_counter = deathcounter::should_display_death_counter(); - - if (type == TimerType::Fullgame) { - if (is_displaying_death_counter) { - row++; - } - } else { // Segment timer - if (should_display_timer(TimerType::Fullgame)) { - row++; - } - if (is_displaying_death_counter) { - row++; - } - } - - return row; -} - -// Bundle up the info timerdisp::draw_timer uses into a struct for convenience -struct TimerDisplayInfo { - u16 pos_x; - u16 row; - u16 text_offset; -}; - -TimerDisplayInfo get_timer_display_info(TimerType type) { - if (type == TimerType::Fullgame) { - return {FULLGAME_TIMER_LOCATION_X, get_timer_row(type), FULLGAME_TIMER_TEXT_OFFSET}; - } else { - return {SEGMENT_TIMER_LOCATION_X, get_timer_row(type), SEGMENT_TIMER_TEXT_OFFSET}; - } -} */ - u16 get_current_segment_idx() { // mkb::scen_info.world gets reset to 0 on the file screen, so if we accidentally exit game to // the menus, the only case where we can't use mkb::scen_info.world is when we are on the file @@ -251,47 +215,16 @@ u16 get_current_segment_idx() { } void draw_timers() { - // TimerDisplayInfo fullgame_info = get_timer_display_info(TimerType::Fullgame); - // u32 loadless_time = get_loadless_time(); - if (should_display_timer(TimerType::Fullgame)) { - /* timerdisp::draw_timer(fullgame_info.pos_x, fullgame_info.row, fullgame_info.text_offset, - "Time:", loadless_time, false, draw::WHITE); */ - // textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Time: %d", get_loadless_time()); - /* textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, "Time:", get_loadless_time(), - timerdisp::TimeFormat::AlwaysLeadNonHours); */ draw_timer("Time:", get_loadless_time(), Format::AlwaysLeadNonHours); } u16 world_idx = get_current_segment_idx(); // index of the current world - // TimerDisplayInfo seg_info = get_timer_display_info(TimerType::Segment); - if (should_display_timer(TimerType::Segment)) { - /* timerdisp::draw_timer(seg_info.pos_x, seg_info.row, seg_info.text_offset, - "Seg:", s_world_timer[world_idx].segment, false, draw::WHITE); */ - /* textinfo::draw(textinfo::Slot::Left, draw::WHITE, "Seg: %d", - s_world_timer[world_idx].segment); */ - /* textinfo::draw_timer(textinfo::Slot::Left, draw::WHITE, - "Seg:", s_world_timer[world_idx].segment, - timerdisp::TimeFormat::AlwaysLeadNonHours); */ draw_timer("Seg:", s_world_timer[world_idx].segment, Format::AlwaysLeadNonHours); } } -// We only use this function for 0 <= row <= 10 -/* Vec2d get_breakdown_row_position(u16 row) { - // u16 starting_row = get_timer_row(TimerType::Segment); - u16 starting_row = 0; // placeholder - float pos_y = timerdisp::row_number_to_vertical_pos(starting_row + row); - if (row < WORLD_COUNT - 1) { - return {BREAKDOWN_ROW_LOCATION_X, pos_y}; - } else if (row == WORLD_COUNT - 1) { // "W10" takes up more space than "Wk" for "1 <= k <= 9" - return {BREAKDOWN_ROW_LOCATION_X - draw::DEBUG_CHAR_WIDTH, pos_y}; - } else { // For the totals row - return {TOTALS_ROW_LOCATION_X, pos_y}; - } -} */ - // We only use this function for 0 <= row <= 10 s32 get_breakdown_row_x_pos(u16 row) { s32 num_x_pos = textinfo::module_and_slot_to_x_alignment(Mod::LoadlessTimer, Slot::Left); @@ -311,7 +244,6 @@ void draw_breakdown_screen() { char row_info_buf[32] = {}; for (u16 idx = 0; idx < WORLD_COUNT; idx++) { - // Vec2d pos = get_breakdown_row_position(idx); s32 pos_x = get_breakdown_row_x_pos(idx); u32 world_deaths = deathcounter::get_world_death_count(idx); @@ -325,7 +257,6 @@ void draw_breakdown_screen() { } // For the totals row - // Vec2d totals_row_pos = get_breakdown_row_position(WORLD_COUNT); s32 totals_x_pos = get_breakdown_row_x_pos(WORLD_COUNT); u32 total_deaths = deathcounter::get_total_death_count(); diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 144099b1..2c810e4e 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -19,7 +19,7 @@ static u16 s_active_row[LEN(s_slot_list)] = {}; // "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get // stacked on top of each other // In addition to the generic text drawing functions, it's useful to be able to line up our displays -// by numbers +// by numbers (this situation covers a toooooon of special cases) // Each module may have slightly different requirements for this alignment // Where the numbers get lined up (this acts as the "default" number alignment per slot, but this diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 36cc32d3..5284a7b6 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -8,8 +8,13 @@ static constexpr u32 SECOND_FRAMES = 60; static constexpr u32 MINUTE_FRAMES = SECOND_FRAMES * 60; static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; -static constexpr s32 X = 378; +// static constexpr s32 X = 378; static constexpr s32 Y = 24; +static constexpr s32 ROW_HEIGHT = 16; + +s32 row_number_to_vertical_pos(s32 row_num) { + return Y + ROW_HEIGHT * row_num; +} TimeComp get_time_components(u32 frames) { u32 time_hours = frames / HOUR_FRAMES; @@ -83,11 +88,7 @@ void format_signed_time(char *buffer, s32 frames, TimeFormat format) { mkb::sprintf(buffer, "%s%s", sign, time_buf); } -s32 row_number_to_vertical_pos(u32 row_num) { - return Y + 16 * row_num; -} - -void draw_timer(s32 pos_x, +/* void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, @@ -111,17 +112,17 @@ void draw_timer(s32 pos_x, draw::debug_text(pos_x, pos_y, color, prefix); draw::debug_text(pos_x + text_offset, pos_y, color, "%s%s", sign, buf); mkb::reset_ui_widescreen_scale_mtx(); -} +} */ // Special case of the above function with a hardcoded x-pos and text offset that covers a lot of // use cases, used for the RTA/pause timer, challenge mode segment timer, and IW timer -void draw_timer_right_side(s32 frames, +/* void draw_timer_right_side(s32 frames, const char *prefix, u32 row, mkb::GXColor color, bool show_seconds) { draw_timer(X, row, 4 * draw::DEBUG_CHAR_WIDTH, prefix, frames, show_seconds, color); -} +} */ void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_precision) { bool positive = frames >= 0; @@ -140,7 +141,7 @@ void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_pre } } -void draw_subtick_timer(s32 frames, +/* void draw_subtick_timer(s32 frames, const char *prefix, u32 row, GXColor color, @@ -176,9 +177,9 @@ void draw_subtick_timer(s32 frames, } mkb::reset_ui_widescreen_scale_mtx(); -} +} */ -void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { +/* void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { mkb::set_ui_widescreen_scale_mtx(320); s32 y = row_number_to_vertical_pos(row); @@ -186,6 +187,6 @@ void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { draw::debug_text(X + 48, y, color, "%2d%", fsave); mkb::set_ui_widescreen_scale_mtx(0); -} +} */ } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 4b249c74..28e03fe5 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -6,7 +6,7 @@ namespace timerdisp { // Where numbers on the right side get aligned (not their prefixes!) -constexpr s32 RIGHT_SIDE_TIMER_ALIGN_X = 378 + 4 * draw::DEBUG_CHAR_WIDTH; +// constexpr s32 RIGHT_SIDE_TIMER_ALIGN_X = 378 + 4 * draw::DEBUG_CHAR_WIDTH; // TimeComp = "Time Components" struct TimeComp { @@ -30,8 +30,9 @@ void format_time(char *buffer, u32 frames, TimeFormat format); void format_signed_time(char *buffer, s32 frames, TimeFormat format); void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_precision); -s32 row_number_to_vertical_pos(u32 row_num); -void draw_timer(s32 pos_x, +s32 row_number_to_vertical_pos(s32 row_num); + +/* void draw_timer(s32 pos_x, u32 row, u32 text_offset, const char *prefix, @@ -50,6 +51,6 @@ void draw_subtick_timer(s32 frames, bool show_minutes, u32 framesave, bool extra_precision); -void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color); +void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color); */ } // namespace timerdisp From 30cea1064ed61754e80100da731648f36b810254 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Sun, 23 Aug 2026 02:02:47 -0500 Subject: [PATCH 77/90] more cleanup --- src/mods/storytimer.cpp | 8 ---- src/mods/timer.cpp | 10 ++--- src/systems/textinfo.cpp | 30 ++------------ src/systems/textinfo.h | 3 +- src/utils/timerdisp.cpp | 87 ---------------------------------------- src/utils/timerdisp.h | 25 ------------ 6 files changed, 9 insertions(+), 154 deletions(-) diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 4de4949e..6ffc2111 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -16,14 +16,6 @@ namespace storytimer { -constexpr s16 FULLGAME_TIMER_LOCATION_X = 18 + 24; -constexpr u16 FULLGAME_TIMER_TEXT_OFFSET = 5 * draw::DEBUG_CHAR_WIDTH; -constexpr s16 SEGMENT_TIMER_LOCATION_X = 30 + 24; -constexpr u16 SEGMENT_TIMER_TEXT_OFFSET = 4 * draw::DEBUG_CHAR_WIDTH; -constexpr s16 BREAKDOWN_ROW_LOCATION_X = 42 + 24; -constexpr s16 TOTALS_ROW_LOCATION_X = 18; -constexpr u16 STARTING_ROW = 2; - constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index 181c20ba..a79d6bdc 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -63,12 +63,12 @@ void disp() { using Format = timerdisp::TimeFormat; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - textinfo::draw_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, Format::SecondsOnly); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - textinfo::draw_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, + textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, Format::SecondsOnly); } @@ -85,14 +85,14 @@ void disp() { u32 framesave = validate::get_framesave(); if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { - textinfo::draw_subtick_timer(Mod::RtaTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + textinfo::draw_subtick_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "SUB:", s_rta_timer, framesave, false); } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { - s32 num_x = textinfo::module_and_slot_to_x_alignment(Mod::RtaTimer, Slot::Right); + s32 num_x = textinfo::module_and_slot_to_x_alignment(Mod::StageTimer, Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; - textinfo::draw(Mod::RtaTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); + textinfo::draw(Mod::StageTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); } } diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 2c810e4e..78d177cd 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -40,20 +40,8 @@ s32 module_and_slot_to_x_alignment(Module module, Slot slot) { case Module::IlBattle: // IL Battle needs different number alignment from loadless timer/death counter return 160 - 6; - case Module::DeathCounter: - return get_slot_x_alignment(Slot::Left); - case Module::LoadlessTimer: - return get_slot_x_alignment(Slot::Left); - case Module::IwTimer: - return get_slot_x_alignment(Slot::Right); - case Module::CmSeg: - return get_slot_x_alignment(Slot::Right); - case Module::RtaTimer: - return get_slot_x_alignment(Slot::Right); - case Module::FrameSave: - return get_slot_x_alignment(Slot::Right); default: - return 0; + return get_slot_x_alignment(slot); } } @@ -73,22 +61,10 @@ u16 get_slot_min_row(Slot slot) { // don't have any modules currently that need to do this though) u16 module_and_slot_to_min_row(Module module, Slot slot) { switch (module) { - case Module::IlBattle: - return get_slot_min_row(Slot::Left); - case Module::DeathCounter: - return get_slot_min_row(Slot::Left); - case Module::LoadlessTimer: - return get_slot_min_row(Slot::Left); - case Module::IwTimer: - return get_slot_min_row(Slot::Right); - case Module::CmSeg: - return get_slot_min_row(Slot::Right); - case Module::RtaTimer: - return 2; - case Module::FrameSave: + case Module::StageTimer: return 2; default: - return 0; + return get_slot_min_row(slot); } } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 2ed28339..37398bc9 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -13,10 +13,9 @@ enum class Slot { }; enum class Module { - RtaTimer, + StageTimer, // Includes RTA/Pause timer + framesave stuff IwTimer, CmSeg, - FrameSave, IlBattle, DeathCounter, LoadlessTimer, diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index 5284a7b6..4b804f87 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -1,14 +1,11 @@ #include "timerdisp.h" -#include "utils/draw.h" - namespace timerdisp { static constexpr u32 SECOND_FRAMES = 60; static constexpr u32 MINUTE_FRAMES = SECOND_FRAMES * 60; static constexpr u32 HOUR_FRAMES = MINUTE_FRAMES * 60; -// static constexpr s32 X = 378; static constexpr s32 Y = 24; static constexpr s32 ROW_HEIGHT = 16; @@ -88,42 +85,6 @@ void format_signed_time(char *buffer, s32 frames, TimeFormat format) { mkb::sprintf(buffer, "%s%s", sign, time_buf); } -/* void draw_timer(s32 pos_x, - u32 row, - u32 text_offset, - const char *prefix, - s32 frames, - bool show_seconds, - GXColor color) { - mkb::set_ui_widescreen_scale_mtx(320); - bool positive = frames >= 0; - if (!positive) frames = -frames; - const char *sign = positive ? "" : "-"; - - s32 pos_y = row_number_to_vertical_pos(row); - - char buf[16] = {}; - if (!show_seconds) { - format_time(buf, frames, TimeFormat::AlwaysLeadNonHours); - } else { - format_time(buf, frames, TimeFormat::SecondsOnly); - } - - draw::debug_text(pos_x, pos_y, color, prefix); - draw::debug_text(pos_x + text_offset, pos_y, color, "%s%s", sign, buf); - mkb::reset_ui_widescreen_scale_mtx(); -} */ - -// Special case of the above function with a hardcoded x-pos and text offset that covers a lot of -// use cases, used for the RTA/pause timer, challenge mode segment timer, and IW timer -/* void draw_timer_right_side(s32 frames, - const char *prefix, - u32 row, - mkb::GXColor color, - bool show_seconds) { - draw_timer(X, row, 4 * draw::DEBUG_CHAR_WIDTH, prefix, frames, show_seconds, color); -} */ - void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_precision) { bool positive = frames >= 0; if (!positive) frames = -frames; @@ -141,52 +102,4 @@ void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_pre } } -/* void draw_subtick_timer(s32 frames, - const char *prefix, - u32 row, - GXColor color, - bool show_minutes, - u32 framesave, - bool extra_precision) { - mkb::set_ui_widescreen_scale_mtx(320); - - bool positive = frames >= 0; - if (!positive) frames = -frames; - const char *sign = positive ? "" : "-"; - - u32 hours = frames / HOUR_FRAMES; - u32 minutes = frames % HOUR_FRAMES / MINUTE_FRAMES; - u32 seconds = frames % MINUTE_FRAMES / SECOND_FRAMES; - u32 milliseconds = ((frames % SECOND_FRAMES) * 100 + framesave) / 6; // 3 digit - // (frames % SECOND_FRAMES) * 1000 / 60 + framesave / 6; - // centiseconds * 10 + framesave / 6; - u32 extra = (((frames % SECOND_FRAMES) * 100 + framesave) * 10) / 6; // 4 digit - // (frames % SECOND_FRAMES) * 10000 / 60 + framesave * 10 / 6; - // centiseconds * 100 + framesave * 10 / 6; - - s32 y = row_number_to_vertical_pos(row); - - u32 total_seconds = seconds + (minutes * MINUTE_FRAMES + hours * HOUR_FRAMES) / SECOND_FRAMES; - draw::debug_text(X, y, color, prefix); - if (extra_precision) { - draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%04d", sign, - total_seconds, extra); - } else { - draw::debug_text(X + 4 * draw::DEBUG_CHAR_WIDTH, y, color, "%s%02d.%03d", sign, - total_seconds, milliseconds); - } - - mkb::reset_ui_widescreen_scale_mtx(); -} */ - -/* void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color) { - mkb::set_ui_widescreen_scale_mtx(320); - - s32 y = row_number_to_vertical_pos(row); - draw::debug_text(X, y, color, prefix); - draw::debug_text(X + 48, y, color, "%2d%", fsave); - - mkb::set_ui_widescreen_scale_mtx(0); -} */ - } // namespace timerdisp diff --git a/src/utils/timerdisp.h b/src/utils/timerdisp.h index 28e03fe5..e3d5a5c4 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -1,13 +1,9 @@ #pragma once #include "mkb/mkb.h" -#include "utils/draw.h" namespace timerdisp { -// Where numbers on the right side get aligned (not their prefixes!) -// constexpr s32 RIGHT_SIDE_TIMER_ALIGN_X = 378 + 4 * draw::DEBUG_CHAR_WIDTH; - // TimeComp = "Time Components" struct TimeComp { u32 hours; @@ -32,25 +28,4 @@ void format_subtick_time(char *buffer, s32 frames, u32 framesave, bool extra_pre s32 row_number_to_vertical_pos(s32 row_num); -/* void draw_timer(s32 pos_x, - u32 row, - u32 text_offset, - const char *prefix, - s32 frames, - bool show_seconds, - GXColor color); -void draw_timer_right_side(s32 frames, - const char *prefix, - u32 row, - GXColor color, - bool show_seconds); -void draw_subtick_timer(s32 frames, - const char *prefix, - u32 row, - GXColor color, - bool show_minutes, - u32 framesave, - bool extra_precision); -void draw_percentage(s32 fsave, const char *prefix, u32 row, GXColor color); */ - } // namespace timerdisp From a9a2d589ea23f12b15698c61a76a86ed8f5227b2 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 05:57:11 -0500 Subject: [PATCH 78/90] start working on new textinfo update slots that need to get moved now get stored to a buffer --- src/mods/timer.cpp | 23 +++-- src/systems/main.cpp | 1 + src/systems/textinfo.cpp | 188 ++++++++++++++++++++++++++++++++++++++- src/systems/textinfo.h | 17 ++++ 4 files changed, 219 insertions(+), 10 deletions(-) diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index a79d6bdc..f58ac089 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -57,19 +57,22 @@ void disp() { } } - // u32 row = 1; using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, - Format::SecondsOnly); + /* textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + Format::SecondsOnly); */ + textinfo::draw_timer_new(Slot::Right, draw::WHITE, "RTA:", s_rta_timer, + Format::SecondsOnly); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, - Format::SecondsOnly); + /* textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, + Format::SecondsOnly); */ + textinfo::draw_timer_new(Slot::Right, draw::WHITE, "PAU:", s_pause_timer, + Format::SecondsOnly); } switch (mkb::sub_mode) { @@ -85,14 +88,16 @@ void disp() { u32 framesave = validate::get_framesave(); if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { - textinfo::draw_subtick_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "SUB:", s_rta_timer, - framesave, false); + /* textinfo::draw_subtick_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "SUB:", + s_rta_timer, framesave, false); */ } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { s32 num_x = textinfo::module_and_slot_to_x_alignment(Mod::StageTimer, Slot::Right); - s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; - textinfo::draw(Mod::StageTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); + s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; // "FSV:" is 4 characters long + // textinfo::draw(Mod::StageTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", + // framesave); + textinfo::draw_new(Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); } } diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 4378952a..a665d55f 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -195,6 +195,7 @@ void init() { pref::init(); unlock::init(); draw::init(); + textinfo::init(); physics::init(); iw::init(); savest::init(); diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 78d177cd..34e4a127 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -6,9 +6,20 @@ #include "../utils/draw.h" #include "../utils/macro_utils.h" #include "../utils/timerdisp.h" +#include "systems/pad.h" namespace textinfo { +// We can get away with making this buffer relatively small since (currently) only text that gets +// drawn to the right side of the screen needs to be stored to the buffer and potentially moved +// up/down +constexpr u16 SLOT_BUF_COUNT = 1; // number of slots whose draw calls get stored in a buffer +constexpr u16 MAX_ROWS = 6; +static char s_row_buf[SLOT_BUF_COUNT][MAX_ROWS][16] = {}; +static u16 s_slot_display_count[SLOT_BUF_COUNT] = {}; + +static bool s_enable_drawing = true; + static const Slot s_slot_list[]{ Slot::Left, Slot::Right, @@ -16,6 +27,52 @@ static const Slot s_slot_list[]{ static u16 s_active_row[LEN(s_slot_list)] = {}; +struct TextData { + s32 pos_x; + u8 row; + GXColor color; + char *text; +}; + +static TextData s_text_data[SLOT_BUF_COUNT][MAX_ROWS]; + +void init_text_data() { + for (u16 j = 0; j < SLOT_BUF_COUNT; j++) { + for (u16 k = 0; k < MAX_ROWS; k++) { + s_text_data[j][k].pos_x = 0; + s_text_data[j][k].row = 0; + s_text_data[j][k].color = draw::WHITE; + s_text_data[j][k].text = s_row_buf[j][k]; + } + } +} + +// For now this works if we want other mods to have the ability to globally stop text drawing from +// happening +void set_drawing_state(bool draw_elements) { + s_enable_drawing = draw_elements; +} + +// We only need to potentially center text that gets drawn on the right side of the screen, so there +// is no need to store things in other slots to the buffer +bool store_slot_to_buf(Slot slot) { + switch (slot) { + case Slot::Right: + return true; + default: + return false; + } +} + +s16 slot_to_buffer_idx(Slot slot) { + switch (slot) { + case Slot::Right: + return 0; + default: + return -1; + } +} + // "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get // stacked on top of each other // In addition to the generic text drawing functions, it's useful to be able to line up our displays @@ -51,7 +108,7 @@ u16 get_slot_min_row(Slot slot) { case Slot::Left: return 2; case Slot::Right: - return 1; + return 0; // 1 default: return 0; } @@ -68,6 +125,127 @@ u16 module_and_slot_to_min_row(Module module, Slot slot) { } } +s32 get_modified_slot_y_pos(Slot slot, u16 row) { + if (store_slot_to_buf(slot) && slot == Slot::Right) { + u16 idx = slot_to_buffer_idx(slot); + if (s_slot_display_count[idx] < 3) { + return timerdisp::row_number_to_vertical_pos(row + 2); + } + } + return timerdisp::row_number_to_vertical_pos(row); +} + +// Option to not incr row is only supported for slots that don't store stuff to a buffer +void draw_main_v_new(Slot slot, + s32 pos_x, + GXColor color, + bool incr_row, + char *format, + va_list args) { + if (!s_enable_drawing) { + return; + } + + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + u16 row = s_active_row[k]; + if (store_slot_to_buf(slot)) { + // Store to buffer so we can draw and move the text later + u16 idx = slot_to_buffer_idx(slot); + // u16 row = s_slot_display_count[idx]; + u16 text_idx = s_slot_display_count[idx]; + + mkb::vsprintf(s_row_buf[idx][row], format, args); + s_text_data[idx][text_idx].pos_x = pos_x; + s_text_data[idx][text_idx].row = row; + s_text_data[idx][text_idx].color = color; + + s_slot_display_count[idx] += 1; + + if (incr_row) { + s_active_row[k] += 1; + } + + if (pad::button_pressed(mkb::PAD_BUTTON_B)) { + mkb::OSReport("Stored to buf. Idx: %d Row: %d \n", idx, row); + mkb::OSReport(s_row_buf[idx][row]); + } + } else { + // If not storing this slot's draw calls to the buffer, draw instantly + // u16 row = s_active_row[k]; + s32 pos_y = timerdisp::row_number_to_vertical_pos(row); + draw::debug_text_v(pos_x, pos_y, color, format, args); + if (incr_row) { + s_active_row[k] += 1; + } + } + } + } +} + +// Responsible for drawing everything that got stored to a buffer (instead of the text rows that got +// drawn instantly) +void draw_from_buf() { + if (!s_enable_drawing) { + return; + } + + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (store_slot_to_buf(s_slot_list[k])) { + u16 idx = slot_to_buffer_idx(s_slot_list[k]); + for (u16 j = 0; j < s_slot_display_count[idx]; j++) { + // u16 row = s_active_row[k]; + // u16 row = j; + u16 row = s_text_data[idx][j].row; + s32 pos_y = get_modified_slot_y_pos(s_slot_list[k], row); + draw::debug_text(s_text_data[idx][row].pos_x, pos_y, s_text_data[idx][row].color, + s_text_data[idx][row].text); + // s_active_row[k] += 1; + if (pad::button_pressed(mkb::PAD_BUTTON_A)) { + mkb::OSReport("Drawing. Active Row: %d Count: %d \n", row, + s_slot_display_count[idx]); + } + } + } + } +} + +void draw_new(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { + va_list args; + va_start(args, format); + draw_main_v_new(slot, pos_x, color, incr_row, format, args); + va_end(args); +} + +void draw_aligned_new(Slot slot, GXColor color, char *format, ...) { + va_list args; + va_start(args, format); + draw_main_v_new(slot, get_slot_x_alignment(slot), color, true, format, args); + va_end(args); +} + +void draw_timer_main_new(Slot slot, + GXColor color, + s32 pos_x, + char *prefix, + s32 frames, + timerdisp::TimeFormat format) { + u32 prefix_len = mkb::strlen(prefix); + s32 x = pos_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_signed_time(time_buf, frames, format); + draw_new(slot, x, color, true, "%s%s", prefix, time_buf); +} + +void draw_timer_new(Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format) { + draw_timer_main_new(slot, color, get_slot_x_alignment(slot), prefix, frames, format); +} + // The main text drawing function // We will almost always want to input true for the incr_row argument // However, in cases where we want to draw two (or more) pieces of text in the same slot and in @@ -157,10 +335,18 @@ void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { s_active_row[k] = get_slot_min_row(s_slot_list[k]); } + for (u16 k = 0; k < SLOT_BUF_COUNT; k++) { + s_slot_display_count[k] = 0; + } +} + +void init() { + init_text_data(); } void disp() { // textinfo's disp() runs after all other disp() functions + draw_from_buf(); reset_active_rows(); } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 37398bc9..2964388e 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -41,6 +41,23 @@ void draw_subtick_timer(Module module, u32 framesave, bool extra_precision); +// new stuff +void draw_new(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); +void draw_aligned_new(Slot slot, GXColor color, char *format, ...); + +void draw_timer_main_new(Slot slot, + GXColor color, + s32 pos_x, + char *prefix, + s32 frames, + timerdisp::TimeFormat format); +void draw_timer_new(Slot slot, + GXColor color, + char *prefix, + s32 frames, + timerdisp::TimeFormat format); + +void init(); void disp(); } // namespace textinfo From ab4c84ad5aeeafa7fd109d85ccbf21c40b308d34 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 05:57:58 -0500 Subject: [PATCH 79/90] simplify AtRunEnd setting for story IL mark --- src/mods/ilmark.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index 6a28c05b..30b0c9d5 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -3,7 +3,6 @@ #include "mkb/mkb.h" #include "mods/freecam.h" #include "mods/physics.h" -#include "mods/storyreset.h" #include "mods/validate.h" #include "systems/goal.h" #include "systems/menu_impl.h" @@ -58,8 +57,8 @@ bool show_in_story() { case StoryIlMarkSettings::DontShow: return false; case StoryIlMarkSettings::AtRunEnd: - // show the mark if at least one timer related pref is on and if we've finished the run - return !storyreset::all_loadless_timer_prefs_off() && goal::is_run_complete(); + // show the mark if we've finished the run + return goal::is_run_complete(); case StoryIlMarkSettings::AlwaysInStory: return true; default: From cfcfdb845cc3531d104fccf5c80f4ea907998fa8 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 06:54:14 -0500 Subject: [PATCH 80/90] update textinfo draw calls --- src/mods/cmseg.cpp | 6 +++-- src/mods/deathcounter.cpp | 8 +++--- src/mods/ilbattle.cpp | 51 ++++++++++++++++++++++++++++---------- src/mods/iw.cpp | 6 +++-- src/mods/storytimer.cpp | 22 ++++++++++++----- src/mods/storytimer.h | 5 ---- src/mods/timer.cpp | 6 +++-- src/systems/textinfo.cpp | 52 ++++++++++++++++++++++++++++++--------- src/systems/textinfo.h | 26 ++++++++++++++------ 9 files changed, 130 insertions(+), 52 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index 08e99be9..fd612c1d 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -425,9 +425,11 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - textinfo::draw_timer(textinfo::Module::CmSeg, textinfo::Slot::Right, color, + /* textinfo::draw_timer(textinfo::Module::CmSeg, textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + timerdisp::TimeFormat::AlwaysLeadNonHours); */ + textinfo::draw_timer_new(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index d7e0454f..a24e5b65 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -21,7 +21,7 @@ static savest::Action s_previous_frame_action = savest::Action::None; // Flag to determine when we should/shouldn't increment the death counter static bool s_can_incr_death_counter = false; -using Mod = textinfo::Module; +// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; @@ -152,8 +152,10 @@ void disp() { if (should_display_death_counter()) { // Technically not a timer, but we can still use this function without specifying any // special formatting - textinfo::draw_timer(Mod::DeathCounter, Slot::Left, draw::WHITE, - "Deaths:", get_total_death_count(), Format::Unformatted); + /* textinfo::draw_timer(Mod::DeathCounter, Slot::Left, draw::WHITE, + "Deaths:", get_total_death_count(), Format::Unformatted); */ + textinfo::draw_timer_new(Slot::Left, draw::WHITE, "Deaths:", get_total_death_count(), + Format::Unformatted); } } diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index b204eb0f..2197e582 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -39,6 +39,7 @@ static constexpr s32 Y = 48; static constexpr u32 CWIDTH = draw::DEBUG_CHAR_WIDTH; static constexpr u32 CHEIGHT = 16; static constexpr s32 TEXT_POS_X = X - 12 * CWIDTH; +static constexpr s32 NUM_POS_X = X - 6; // buzzer beater constants static constexpr u16 BUZZER_BEATER_FRAMES_PER_COLOR = 5; static const GXColor s_buzzer_beater_color_list[] = { @@ -65,12 +66,13 @@ static u32 s_rainbow = 0; static bool s_time_buzzer = false; static bool s_score_buzzer = false; -using Mod = textinfo::Module; +// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; void draw_battle_text_main(s32 pos_x, GXColor color, bool incr_row, char *text) { - textinfo::draw(Mod::IlBattle, Slot::Left, pos_x, color, incr_row, text); + // textinfo::draw(Mod::IlBattle, Slot::Left, pos_x, color, incr_row, text); + textinfo::draw_new(Slot::Left, pos_x, color, incr_row, text); } void draw_buzzer_beater_row(GXColor color) { @@ -101,7 +103,16 @@ void draw_battle_breakdown_text(GXColor color, char *text) { // Special formatting like "time [bananas]"" is handled on a per-case basis, however void draw_battle_num(GXColor color, u32 num, Format format) { // No prefix specified since IL Battle uses custom spacing between its prefixes and times - textinfo::draw_timer(Mod::IlBattle, Slot::Left, color, "", num, format); + // textinfo::draw_timer(Mod::IlBattle, Slot::Left, color, "", num, format); + // textinfo::draw_timer_new(Slot::Left, color, "", num, format); + textinfo::draw_timer_main_new(Slot::Left, color, NUM_POS_X, "", num, format); +} + +void draw_battle_text_right_aligned(GXColor color, char *format, ...) { + va_list args; + va_start(args, format); + textinfo::draw_main_v_new(Slot::Left, NUM_POS_X, color, true, format, args); + va_end(args); } // pre cleanup @@ -120,8 +131,11 @@ static void battle_display(GXColor text_color) { if (pref::get(pref::Pref::IlBattleTieCount) && s_best_frames_ties > 0) { char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_frames, Format::SecondsOnly); - textinfo::draw_aligned(Mod::IlBattle, Slot::Left, time_color, "%s (%d)", time_buf, - s_best_frames_ties + 1); + /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, time_color, "%s (%d)", time_buf, + s_best_frames_ties + 1); */ + /* textinfo::draw_aligned_new(Slot::Left, time_color, "%s (%d)", time_buf, + s_best_frames_ties + 1); */ + draw_battle_text_right_aligned(time_color, "%s (%d)", time_buf, s_best_frames_ties + 1); } else { draw_battle_num(text_color, s_best_frames, Format::SecondsOnly); } @@ -129,8 +143,12 @@ static void battle_display(GXColor text_color) { if (pref::get(pref::Pref::IlBattleShowScore)) { draw_battle_text(text_color, "BEST SCORE:"); if (pref::get(pref::Pref::IlBattleTieCount) && s_best_score_ties > 0) { - textinfo::draw_aligned(Mod::IlBattle, Slot::Left, score_color, "%d (%d)", s_best_score, - s_best_score_ties + 1); + /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, score_color, "%d (%d)", + s_best_score, s_best_score_ties + 1); */ + /* textinfo::draw_aligned_new(Slot::Left, score_color, "%d (%d)", s_best_score, + s_best_score_ties + 1); */ + draw_battle_text_right_aligned(score_color, "%d (%d)", s_best_score, + s_best_score_ties + 1); } else { draw_battle_num(score_color, s_best_score, Format::Unformatted); } @@ -142,13 +160,18 @@ static void battle_display(GXColor text_color) { draw_battle_text(text_color, "BREAKDOWN:"); char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_score_frames, Format::SecondsOnly); - textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%s [%d]", time_buf, - s_best_score_bananas); + /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%s [%d]", time_buf, + s_best_score_bananas); */ + /* textinfo::draw_aligned_new(Slot::Left, text_color, "%s [%d]", time_buf, + s_best_score_bananas); */ + draw_battle_text_right_aligned(text_color, "%s [%d]", time_buf, s_best_score_bananas); } else if (breakdown_value == 2) { // full draw_battle_breakdown_text(text_color, "BANANAS:"); - textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", - s_best_score_bananas); + /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", + s_best_score_bananas); */ + // textinfo::draw_aligned_new(Slot::Left, text_color, "%d", s_best_score_bananas); + draw_battle_text_right_aligned(text_color, "%d", s_best_score_bananas); draw_battle_breakdown_text(text_color, "TIMER:"); draw_battle_num(text_color, s_best_score_frames, Format::SecondsOnly); } @@ -390,8 +413,10 @@ void disp() { char buf[25]; binds::get_bind_str(input, buf); draw_battle_text_main(TEXT_POS_X, draw::LIGHT_PURPLE, true, "NOT READY"); - textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, - "%s to ready", buf); + /* textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, + "%s to ready", buf); */ + textinfo::draw_new(Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, "%s to ready", + buf); break; } case IlBattleState::WaitForFirstRetry: { diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index 39883d7f..ef37c9c1 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -125,9 +125,11 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - textinfo::draw_timer(textinfo::Module::IwTimer, textinfo::Slot::Right, draw::WHITE, + /* textinfo::draw_timer(textinfo::Module::IwTimer, textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + timerdisp::TimeFormat::AlwaysLeadNonHours); */ + textinfo::draw_timer_new(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } // namespace iw diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 6ffc2111..f51acf15 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -16,12 +16,17 @@ namespace storytimer { +enum class TimerType { + Fullgame, + Segment, +}; + constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world -using Mod = textinfo::Module; +// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; @@ -143,7 +148,8 @@ void tick() { // Special case of textinfo::draw_timer() useful for us void draw_timer(char *prefix, u32 frames, Format format) { - textinfo::draw_timer(Mod::LoadlessTimer, Slot::Left, draw::WHITE, prefix, frames, format); + // textinfo::draw_timer(Mod::LoadlessTimer, Slot::Left, draw::WHITE, prefix, frames, format); + textinfo::draw_timer_new(Slot::Left, draw::WHITE, prefix, frames, format); } // The run breakdown screen replaces the segment timer at the end of the run @@ -219,7 +225,8 @@ void draw_timers() { // We only use this function for 0 <= row <= 10 s32 get_breakdown_row_x_pos(u16 row) { - s32 num_x_pos = textinfo::module_and_slot_to_x_alignment(Mod::LoadlessTimer, Slot::Left); + // s32 num_x_pos = textinfo::module_and_slot_to_x_alignment(Mod::LoadlessTimer, Slot::Left); + s32 num_x_pos = textinfo::get_slot_x_alignment(Slot::Left); if (row < WORLD_COUNT - 1) { // "Wk:" is 3 characters long if 1 <= k <= 9 return num_x_pos - 3 * draw::DEBUG_CHAR_WIDTH; } else if (row == WORLD_COUNT - 1) { // "W10:" is 4 characters long @@ -245,7 +252,8 @@ void draw_breakdown_screen() { Format::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf, "W%d:%s (%s) (%d)", idx + 1, split_buf, seg_buf, world_deaths); - textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos_x, draw::WHITE, true, row_info_buf); + // textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos_x, draw::WHITE, true, row_info_buf); + textinfo::draw_new(Slot::Left, pos_x, draw::WHITE, true, row_info_buf); } // For the totals row @@ -254,8 +262,10 @@ void draw_breakdown_screen() { char total_time_buf[16] = {}; timerdisp::format_time(total_time_buf, get_loadless_time(), Format::MinutesAlwaysLeadingZero); - textinfo::draw(Mod::LoadlessTimer, Slot::Left, totals_x_pos, draw::WHITE, true, - "Totals:%s (%d)", total_time_buf, total_deaths); + /* textinfo::draw(Mod::LoadlessTimer, Slot::Left, totals_x_pos, draw::WHITE, true, + "Totals:%s (%d)", total_time_buf, total_deaths); */ + textinfo::draw_new(Slot::Left, totals_x_pos, draw::WHITE, true, "Totals:%s (%d)", + total_time_buf, total_deaths); } bool should_not_display_timer_at_all() { diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index 558480fe..c4dff0f0 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -4,11 +4,6 @@ namespace storytimer { -enum class TimerType { - Fullgame, - Segment, -}; - struct WorldTimer { u32 segment; // the time taken to complete a world up until tape break on the last stage u32 full_world; // the time taken to complete a world until the fade to white on the last stage diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index f58ac089..40b748be 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -57,7 +57,7 @@ void disp() { } } - using Mod = textinfo::Module; + // using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; @@ -90,10 +90,12 @@ void disp() { if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { /* textinfo::draw_subtick_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "SUB:", s_rta_timer, framesave, false); */ + textinfo::draw_subtick_timer(Slot::Right, draw::WHITE, "SUB:", s_rta_timer, framesave, + false); } if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { - s32 num_x = textinfo::module_and_slot_to_x_alignment(Mod::StageTimer, Slot::Right); + s32 num_x = textinfo::get_slot_x_alignment(Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; // "FSV:" is 4 characters long // textinfo::draw(Mod::StageTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", // framesave); diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 34e4a127..54905ad4 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -14,9 +14,9 @@ namespace textinfo { // drawn to the right side of the screen needs to be stored to the buffer and potentially moved // up/down constexpr u16 SLOT_BUF_COUNT = 1; // number of slots whose draw calls get stored in a buffer -constexpr u16 MAX_ROWS = 6; +constexpr u16 MAX_ROWS = 6; // max number of text displays that can be stored to a buffer static char s_row_buf[SLOT_BUF_COUNT][MAX_ROWS][16] = {}; -static u16 s_slot_display_count[SLOT_BUF_COUNT] = {}; +static u16 s_slot_display_count[SLOT_BUF_COUNT] = {}; // maybe call it s_buffer_slot_display_count static bool s_enable_drawing = true; @@ -92,7 +92,7 @@ s32 get_slot_x_alignment(Slot slot) { } } -s32 module_and_slot_to_x_alignment(Module module, Slot slot) { +/* s32 module_and_slot_to_x_alignment(Module module, Slot slot) { switch (module) { case Module::IlBattle: // IL Battle needs different number alignment from loadless timer/death counter @@ -100,7 +100,7 @@ s32 module_and_slot_to_x_alignment(Module module, Slot slot) { default: return get_slot_x_alignment(slot); } -} +} */ // Per-slot minimum row number. Individual modules are allowed to have higher min rows, however u16 get_slot_min_row(Slot slot) { @@ -116,14 +116,14 @@ u16 get_slot_min_row(Slot slot) { // Modules are allowed to draw to multiple slots, and can have different min rows for each slot (we // don't have any modules currently that need to do this though) -u16 module_and_slot_to_min_row(Module module, Slot slot) { +/* u16 module_and_slot_to_min_row(Module module, Slot slot) { switch (module) { case Module::StageTimer: return 2; default: return get_slot_min_row(slot); } -} +} */ s32 get_modified_slot_y_pos(Slot slot, u16 row) { if (store_slot_to_buf(slot) && slot == Slot::Right) { @@ -151,6 +151,7 @@ void draw_main_v_new(Slot slot, u16 row = s_active_row[k]; if (store_slot_to_buf(slot)) { // Store to buffer so we can draw and move the text later + // idx = buffer slot index u16 idx = slot_to_buffer_idx(slot); // u16 row = s_slot_display_count[idx]; u16 text_idx = s_slot_display_count[idx]; @@ -246,12 +247,39 @@ void draw_timer_new(Slot slot, draw_timer_main_new(slot, color, get_slot_x_alignment(slot), prefix, frames, format); } +void draw_subtick_timer_main(Slot slot, + GXColor color, + s32 pos_x, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision) { + u32 prefix_len = mkb::strlen(prefix); + s32 x = pos_x - prefix_len * draw::DEBUG_CHAR_WIDTH; + + char time_buf[16] = {}; + timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); + draw_new(slot, x, color, true, "%s%s", prefix, time_buf); +} + +void draw_subtick_timer(Slot slot, + GXColor color, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision) { + draw_subtick_timer_main(slot, color, get_slot_x_alignment(slot), prefix, frames, framesave, + extra_precision); +} + +// --- + // The main text drawing function // We will almost always want to input true for the incr_row argument // However, in cases where we want to draw two (or more) pieces of text in the same slot and in // the same row with custom horizontal spacing, inputting false can be useful // This is used in ilbattle.cpp -void draw_main_v(s32 min_row, +/* void draw_main_v(s32 min_row, Slot slot, s32 pos_x, GXColor color, @@ -270,9 +298,9 @@ void draw_main_v(s32 min_row, } } } -} +} */ -void draw_v(Module module, +/* void draw_v(Module module, Slot slot, s32 pos_x, GXColor color, @@ -296,11 +324,11 @@ void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...) { va_start(args, format); draw_v(module, slot, module_and_slot_to_x_alignment(module, slot), color, true, format, args); va_end(args); -} +} */ // Aligns the time according to get_slot_x_alignment; the prefix gets automatically moved to the // left depending on string length -void draw_timer(Module module, +/* void draw_timer(Module module, Slot slot, GXColor color, char *prefix, @@ -329,7 +357,7 @@ void draw_subtick_timer(Module module, char time_buf[16] = {}; timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); draw(module, slot, x, color, true, "%s%s", prefix, time_buf); -} +} */ void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 2964388e..5ab8a63f 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -22,26 +22,32 @@ enum class Module { }; s32 get_slot_x_alignment(Slot slot); -s32 module_and_slot_to_x_alignment(Module module, Slot slot); +// s32 module_and_slot_to_x_alignment(Module module, Slot slot); -void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); -void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); +// void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); +// void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); -void draw_timer(Module module, +/* void draw_timer(Module module, Slot slot, GXColor color, char *prefix, s32 frames, - timerdisp::TimeFormat format); -void draw_subtick_timer(Module module, + timerdisp::TimeFormat format); */ +/* void draw_subtick_timer(Module module, Slot slot, GXColor color, char *prefix, s32 frames, u32 framesave, - bool extra_precision); + bool extra_precision); */ // new stuff +void draw_main_v_new(Slot slot, + s32 pos_x, + GXColor color, + bool incr_row, + char *format, + va_list args); void draw_new(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); void draw_aligned_new(Slot slot, GXColor color, char *format, ...); @@ -56,6 +62,12 @@ void draw_timer_new(Slot slot, char *prefix, s32 frames, timerdisp::TimeFormat format); +void draw_subtick_timer(Slot slot, + GXColor color, + char *prefix, + s32 frames, + u32 framesave, + bool extra_precision); void init(); void disp(); From 0d6ea352f8fcb89cd60a2db8b16621f54328cc28 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 10:28:37 -0500 Subject: [PATCH 81/90] various cleanup removed the _new suffixes from the stand-in names + renamed some stuff in textinfo for clarity. also fixed a bug in draw_v() and draw_from_buf() with using the wrong indices --- src/mods/cmseg.cpp | 7 +- src/mods/deathcounter.cpp | 7 +- src/mods/ilbattle.cpp | 34 +---- src/mods/iw.cpp | 7 +- src/mods/storytimer.cpp | 14 +- src/mods/timer.cpp | 17 +-- src/systems/textinfo.cpp | 275 +++++++++++--------------------------- src/systems/textinfo.h | 55 ++------ 8 files changed, 105 insertions(+), 311 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index fd612c1d..af7deb99 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -425,11 +425,8 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - /* textinfo::draw_timer(textinfo::Module::CmSeg, textinfo::Slot::Right, color, - "SEG:", static_cast(s_seg_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); */ - textinfo::draw_timer_new(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + textinfo::draw_timer(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index a24e5b65..182e390c 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -21,7 +21,6 @@ static savest::Action s_previous_frame_action = savest::Action::None; // Flag to determine when we should/shouldn't increment the death counter static bool s_can_incr_death_counter = false; -// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; @@ -152,10 +151,8 @@ void disp() { if (should_display_death_counter()) { // Technically not a timer, but we can still use this function without specifying any // special formatting - /* textinfo::draw_timer(Mod::DeathCounter, Slot::Left, draw::WHITE, - "Deaths:", get_total_death_count(), Format::Unformatted); */ - textinfo::draw_timer_new(Slot::Left, draw::WHITE, "Deaths:", get_total_death_count(), - Format::Unformatted); + textinfo::draw_timer(Slot::Left, draw::WHITE, "Deaths:", get_total_death_count(), + Format::Unformatted); } } diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 2197e582..5390b7aa 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -66,13 +66,11 @@ static u32 s_rainbow = 0; static bool s_time_buzzer = false; static bool s_score_buzzer = false; -// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; void draw_battle_text_main(s32 pos_x, GXColor color, bool incr_row, char *text) { - // textinfo::draw(Mod::IlBattle, Slot::Left, pos_x, color, incr_row, text); - textinfo::draw_new(Slot::Left, pos_x, color, incr_row, text); + textinfo::draw(Slot::Left, pos_x, color, incr_row, text); } void draw_buzzer_beater_row(GXColor color) { @@ -100,22 +98,20 @@ void draw_battle_breakdown_text(GXColor color, char *text) { } // When we only want to display a single time or number (like attempt count) -// Special formatting like "time [bananas]"" is handled on a per-case basis, however +// Special formatting like "time [bananas]" is handled on a per-case basis +// using the next function, however void draw_battle_num(GXColor color, u32 num, Format format) { // No prefix specified since IL Battle uses custom spacing between its prefixes and times - // textinfo::draw_timer(Mod::IlBattle, Slot::Left, color, "", num, format); - // textinfo::draw_timer_new(Slot::Left, color, "", num, format); - textinfo::draw_timer_main_new(Slot::Left, color, NUM_POS_X, "", num, format); + textinfo::draw_timer_main(Slot::Left, color, NUM_POS_X, "", num, format); } void draw_battle_text_right_aligned(GXColor color, char *format, ...) { va_list args; va_start(args, format); - textinfo::draw_main_v_new(Slot::Left, NUM_POS_X, color, true, format, args); + textinfo::draw_v(Slot::Left, NUM_POS_X, color, true, format, args); va_end(args); } -// pre cleanup static void battle_display(GXColor text_color) { u32 current_y = Y; @@ -131,10 +127,6 @@ static void battle_display(GXColor text_color) { if (pref::get(pref::Pref::IlBattleTieCount) && s_best_frames_ties > 0) { char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_frames, Format::SecondsOnly); - /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, time_color, "%s (%d)", time_buf, - s_best_frames_ties + 1); */ - /* textinfo::draw_aligned_new(Slot::Left, time_color, "%s (%d)", time_buf, - s_best_frames_ties + 1); */ draw_battle_text_right_aligned(time_color, "%s (%d)", time_buf, s_best_frames_ties + 1); } else { draw_battle_num(text_color, s_best_frames, Format::SecondsOnly); @@ -143,10 +135,6 @@ static void battle_display(GXColor text_color) { if (pref::get(pref::Pref::IlBattleShowScore)) { draw_battle_text(text_color, "BEST SCORE:"); if (pref::get(pref::Pref::IlBattleTieCount) && s_best_score_ties > 0) { - /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, score_color, "%d (%d)", - s_best_score, s_best_score_ties + 1); */ - /* textinfo::draw_aligned_new(Slot::Left, score_color, "%d (%d)", s_best_score, - s_best_score_ties + 1); */ draw_battle_text_right_aligned(score_color, "%d (%d)", s_best_score, s_best_score_ties + 1); } else { @@ -160,17 +148,10 @@ static void battle_display(GXColor text_color) { draw_battle_text(text_color, "BREAKDOWN:"); char time_buf[16] = {}; timerdisp::format_time(time_buf, s_best_score_frames, Format::SecondsOnly); - /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%s [%d]", time_buf, - s_best_score_bananas); */ - /* textinfo::draw_aligned_new(Slot::Left, text_color, "%s [%d]", time_buf, - s_best_score_bananas); */ draw_battle_text_right_aligned(text_color, "%s [%d]", time_buf, s_best_score_bananas); } else if (breakdown_value == 2) { // full draw_battle_breakdown_text(text_color, "BANANAS:"); - /* textinfo::draw_aligned(Mod::IlBattle, Slot::Left, text_color, "%d", - s_best_score_bananas); */ - // textinfo::draw_aligned_new(Slot::Left, text_color, "%d", s_best_score_bananas); draw_battle_text_right_aligned(text_color, "%d", s_best_score_bananas); draw_battle_breakdown_text(text_color, "TIMER:"); draw_battle_num(text_color, s_best_score_frames, Format::SecondsOnly); @@ -413,10 +394,7 @@ void disp() { char buf[25]; binds::get_bind_str(input, buf); draw_battle_text_main(TEXT_POS_X, draw::LIGHT_PURPLE, true, "NOT READY"); - /* textinfo::draw(Mod::IlBattle, Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, - "%s to ready", buf); */ - textinfo::draw_new(Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, "%s to ready", - buf); + textinfo::draw(Slot::Left, TEXT_POS_X, draw::LIGHT_PURPLE, true, "%s to ready", buf); break; } case IlBattleState::WaitForFirstRetry: { diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index ef37c9c1..44468cb7 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -125,11 +125,8 @@ void disp() { mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || freecam::should_hide_hud()) return; - /* textinfo::draw_timer(textinfo::Module::IwTimer, textinfo::Slot::Right, draw::WHITE, - "IW:", static_cast(s_iw_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); */ - textinfo::draw_timer_new(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + timerdisp::TimeFormat::AlwaysLeadNonHours); } } // namespace iw diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index f51acf15..1c5c0819 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -26,7 +26,6 @@ constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world -// using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; @@ -148,8 +147,7 @@ void tick() { // Special case of textinfo::draw_timer() useful for us void draw_timer(char *prefix, u32 frames, Format format) { - // textinfo::draw_timer(Mod::LoadlessTimer, Slot::Left, draw::WHITE, prefix, frames, format); - textinfo::draw_timer_new(Slot::Left, draw::WHITE, prefix, frames, format); + textinfo::draw_timer(Slot::Left, draw::WHITE, prefix, frames, format); } // The run breakdown screen replaces the segment timer at the end of the run @@ -225,7 +223,6 @@ void draw_timers() { // We only use this function for 0 <= row <= 10 s32 get_breakdown_row_x_pos(u16 row) { - // s32 num_x_pos = textinfo::module_and_slot_to_x_alignment(Mod::LoadlessTimer, Slot::Left); s32 num_x_pos = textinfo::get_slot_x_alignment(Slot::Left); if (row < WORLD_COUNT - 1) { // "Wk:" is 3 characters long if 1 <= k <= 9 return num_x_pos - 3 * draw::DEBUG_CHAR_WIDTH; @@ -252,8 +249,7 @@ void draw_breakdown_screen() { Format::MinutesAlwaysLeadingZero); mkb::sprintf(row_info_buf, "W%d:%s (%s) (%d)", idx + 1, split_buf, seg_buf, world_deaths); - // textinfo::draw(Mod::LoadlessTimer, Slot::Left, pos_x, draw::WHITE, true, row_info_buf); - textinfo::draw_new(Slot::Left, pos_x, draw::WHITE, true, row_info_buf); + textinfo::draw(Slot::Left, pos_x, draw::WHITE, true, row_info_buf); } // For the totals row @@ -262,10 +258,8 @@ void draw_breakdown_screen() { char total_time_buf[16] = {}; timerdisp::format_time(total_time_buf, get_loadless_time(), Format::MinutesAlwaysLeadingZero); - /* textinfo::draw(Mod::LoadlessTimer, Slot::Left, totals_x_pos, draw::WHITE, true, - "Totals:%s (%d)", total_time_buf, total_deaths); */ - textinfo::draw_new(Slot::Left, totals_x_pos, draw::WHITE, true, "Totals:%s (%d)", - total_time_buf, total_deaths); + textinfo::draw(Slot::Left, totals_x_pos, draw::WHITE, true, "Totals:%s (%d)", total_time_buf, + total_deaths); } bool should_not_display_timer_at_all() { diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index 40b748be..47b0f2f4 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -57,22 +57,15 @@ void disp() { } } - // using Mod = textinfo::Module; using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { - /* textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "RTA:", s_rta_timer, - Format::SecondsOnly); */ - textinfo::draw_timer_new(Slot::Right, draw::WHITE, "RTA:", s_rta_timer, - Format::SecondsOnly); + textinfo::draw_timer(Slot::Right, draw::WHITE, "RTA:", s_rta_timer, Format::SecondsOnly); } if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { - /* textinfo::draw_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "PAU:", s_pause_timer, - Format::SecondsOnly); */ - textinfo::draw_timer_new(Slot::Right, draw::WHITE, "PAU:", s_pause_timer, - Format::SecondsOnly); + textinfo::draw_timer(Slot::Right, draw::WHITE, "PAU:", s_pause_timer, Format::SecondsOnly); } switch (mkb::sub_mode) { @@ -88,8 +81,6 @@ void disp() { u32 framesave = validate::get_framesave(); if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { - /* textinfo::draw_subtick_timer(Mod::StageTimer, Slot::Right, draw::WHITE, "SUB:", - s_rta_timer, framesave, false); */ textinfo::draw_subtick_timer(Slot::Right, draw::WHITE, "SUB:", s_rta_timer, framesave, false); } @@ -97,9 +88,7 @@ void disp() { if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { s32 num_x = textinfo::get_slot_x_alignment(Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; // "FSV:" is 4 characters long - // textinfo::draw(Mod::StageTimer, Slot::Right, x, draw::WHITE, true, "FSV:%2d%", - // framesave); - textinfo::draw_new(Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); + textinfo::draw(Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); } } diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 54905ad4..657af7df 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -10,35 +10,39 @@ namespace textinfo { -// We can get away with making this buffer relatively small since (currently) only text that gets -// drawn to the right side of the screen needs to be stored to the buffer and potentially moved -// up/down -constexpr u16 SLOT_BUF_COUNT = 1; // number of slots whose draw calls get stored in a buffer -constexpr u16 MAX_ROWS = 6; // max number of text displays that can be stored to a buffer -static char s_row_buf[SLOT_BUF_COUNT][MAX_ROWS][16] = {}; -static u16 s_slot_display_count[SLOT_BUF_COUNT] = {}; // maybe call it s_buffer_slot_display_count - -static bool s_enable_drawing = true; +struct TextData { + s32 pos_x; + u8 row; + GXColor color; + char *text; // always a ptr to s_row_buf +}; static const Slot s_slot_list[]{ Slot::Left, Slot::Right, }; -static u16 s_active_row[LEN(s_slot_list)] = {}; - -struct TextData { - s32 pos_x; - u8 row; - GXColor color; - char *text; -}; +// We can get away with using relatively few and small buffers since (currently) only text that gets +// drawn to the right side of the screen needs to be stored to the buffer and potentially moved +// up/down +// We call any slot that stores its text to buffers a buffer slot + +// Number of slots whose draw calls get stored in a buffer +constexpr u16 BUFFER_SLOT_COUNT = 1; +// Max number of strings that each buffer slot can accept +constexpr u16 BUFFER_SLOT_MAX_STRS = 6; +static char s_row_buf[BUFFER_SLOT_COUNT][BUFFER_SLOT_MAX_STRS][16] = {}; +// How many strings are actually being displayed in each buffer slot +static u16 s_buffer_slot_display_count[BUFFER_SLOT_COUNT] = {}; +// We bundle up relevant display parameters when writing things to the buffer +static TextData s_text_data[BUFFER_SLOT_COUNT][BUFFER_SLOT_MAX_STRS]; -static TextData s_text_data[SLOT_BUF_COUNT][MAX_ROWS]; +static u16 s_active_row[LEN(s_slot_list)] = {}; +static bool s_enable_drawing = true; void init_text_data() { - for (u16 j = 0; j < SLOT_BUF_COUNT; j++) { - for (u16 k = 0; k < MAX_ROWS; k++) { + for (u16 j = 0; j < BUFFER_SLOT_COUNT; j++) { + for (u16 k = 0; k < BUFFER_SLOT_MAX_STRS; k++) { s_text_data[j][k].pos_x = 0; s_text_data[j][k].row = 0; s_text_data[j][k].color = draw::WHITE; @@ -55,32 +59,25 @@ void set_drawing_state(bool draw_elements) { // We only need to potentially center text that gets drawn on the right side of the screen, so there // is no need to store things in other slots to the buffer -bool store_slot_to_buf(Slot slot) { - switch (slot) { - case Slot::Right: - return true; - default: - return false; - } -} - -s16 slot_to_buffer_idx(Slot slot) { +s16 get_buffer_slot_idx(Slot slot) { switch (slot) { case Slot::Right: return 0; default: + // If not a buffer slot return -1; } } +bool slot_is_buffer_slot(Slot slot) { + return get_buffer_slot_idx(slot) != -1; +} + // "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get // stacked on top of each other -// In addition to the generic text drawing functions, it's useful to be able to line up our displays -// by numbers (this situation covers a toooooon of special cases) -// Each module may have slightly different requirements for this alignment -// Where the numbers get lined up (this acts as the "default" number alignment per slot, but this -// can be overridden on a per-mod basis) +// Where the numbers get lined up (this acts as the "default" number alignment per slot, but +// individual mods can specify a different x-position in the relevant draw() calls if necessary) s32 get_slot_x_alignment(Slot slot) { switch (slot) { case Slot::Left: @@ -92,17 +89,7 @@ s32 get_slot_x_alignment(Slot slot) { } } -/* s32 module_and_slot_to_x_alignment(Module module, Slot slot) { - switch (module) { - case Module::IlBattle: - // IL Battle needs different number alignment from loadless timer/death counter - return 160 - 6; - default: - return get_slot_x_alignment(slot); - } -} */ - -// Per-slot minimum row number. Individual modules are allowed to have higher min rows, however +// Per-slot minimum row number u16 get_slot_min_row(Slot slot) { switch (slot) { case Slot::Left: @@ -114,34 +101,22 @@ u16 get_slot_min_row(Slot slot) { } } -// Modules are allowed to draw to multiple slots, and can have different min rows for each slot (we -// don't have any modules currently that need to do this though) -/* u16 module_and_slot_to_min_row(Module module, Slot slot) { - switch (module) { - case Module::StageTimer: - return 2; - default: - return get_slot_min_row(slot); - } -} */ - s32 get_modified_slot_y_pos(Slot slot, u16 row) { - if (store_slot_to_buf(slot) && slot == Slot::Right) { - u16 idx = slot_to_buffer_idx(slot); - if (s_slot_display_count[idx] < 3) { + if (slot_is_buffer_slot(slot) && slot == Slot::Right) { + u16 idx = get_buffer_slot_idx(slot); + if (s_buffer_slot_display_count[idx] < 3) { return timerdisp::row_number_to_vertical_pos(row + 2); } } return timerdisp::row_number_to_vertical_pos(row); } -// Option to not incr row is only supported for slots that don't store stuff to a buffer -void draw_main_v_new(Slot slot, - s32 pos_x, - GXColor color, - bool incr_row, - char *format, - va_list args) { +// The main text drawing function +// We will almost always want to input true for the incr_row argument +// However, in cases where we want to draw two (or more) pieces of text in the same slot and in +// the same row with custom horizontal spacing, inputting false can be useful +// This is used in ilbattle.cpp +void draw_v(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, va_list args) { if (!s_enable_drawing) { return; } @@ -149,23 +124,18 @@ void draw_main_v_new(Slot slot, for (u16 k = 0; k < LEN(s_slot_list); k++) { if (s_slot_list[k] == slot) { u16 row = s_active_row[k]; - if (store_slot_to_buf(slot)) { + if (slot_is_buffer_slot(slot)) { // Store to buffer so we can draw and move the text later - // idx = buffer slot index - u16 idx = slot_to_buffer_idx(slot); - // u16 row = s_slot_display_count[idx]; - u16 text_idx = s_slot_display_count[idx]; + u16 idx = get_buffer_slot_idx(slot); // idx = buffer slot index + u16 text_idx = s_buffer_slot_display_count[idx]; - mkb::vsprintf(s_row_buf[idx][row], format, args); + // We also store relevant display parameters (position, color) + mkb::vsprintf(s_row_buf[idx][text_idx], format, args); s_text_data[idx][text_idx].pos_x = pos_x; s_text_data[idx][text_idx].row = row; s_text_data[idx][text_idx].color = color; - s_slot_display_count[idx] += 1; - - if (incr_row) { - s_active_row[k] += 1; - } + s_buffer_slot_display_count[idx] += 1; if (pad::button_pressed(mkb::PAD_BUTTON_B)) { mkb::OSReport("Stored to buf. Idx: %d Row: %d \n", idx, row); @@ -173,12 +143,12 @@ void draw_main_v_new(Slot slot, } } else { // If not storing this slot's draw calls to the buffer, draw instantly - // u16 row = s_active_row[k]; s32 pos_y = timerdisp::row_number_to_vertical_pos(row); draw::debug_text_v(pos_x, pos_y, color, format, args); - if (incr_row) { - s_active_row[k] += 1; - } + } + + if (incr_row) { + s_active_row[k] += 1; } } } @@ -192,59 +162,55 @@ void draw_from_buf() { } for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (store_slot_to_buf(s_slot_list[k])) { - u16 idx = slot_to_buffer_idx(s_slot_list[k]); - for (u16 j = 0; j < s_slot_display_count[idx]; j++) { - // u16 row = s_active_row[k]; - // u16 row = j; + if (slot_is_buffer_slot(s_slot_list[k])) { + u16 idx = get_buffer_slot_idx(s_slot_list[k]); + for (u16 j = 0; j < s_buffer_slot_display_count[idx]; j++) { u16 row = s_text_data[idx][j].row; s32 pos_y = get_modified_slot_y_pos(s_slot_list[k], row); - draw::debug_text(s_text_data[idx][row].pos_x, pos_y, s_text_data[idx][row].color, - s_text_data[idx][row].text); - // s_active_row[k] += 1; + draw::debug_text(s_text_data[idx][j].pos_x, pos_y, s_text_data[idx][j].color, + s_text_data[idx][j].text); + if (pad::button_pressed(mkb::PAD_BUTTON_A)) { mkb::OSReport("Drawing. Active Row: %d Count: %d \n", row, - s_slot_display_count[idx]); + s_buffer_slot_display_count[idx]); } } } } } -void draw_new(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { +void draw(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { va_list args; va_start(args, format); - draw_main_v_new(slot, pos_x, color, incr_row, format, args); + draw_v(slot, pos_x, color, incr_row, format, args); va_end(args); } -void draw_aligned_new(Slot slot, GXColor color, char *format, ...) { +void draw_aligned(Slot slot, GXColor color, char *format, ...) { va_list args; va_start(args, format); - draw_main_v_new(slot, get_slot_x_alignment(slot), color, true, format, args); + draw_v(slot, get_slot_x_alignment(slot), color, true, format, args); va_end(args); } -void draw_timer_main_new(Slot slot, - GXColor color, - s32 pos_x, - char *prefix, - s32 frames, - timerdisp::TimeFormat format) { +// Aligns the time according to get_slot_x_alignment; the prefix gets automatically moved to the +// left depending on string length +void draw_timer_main(Slot slot, + GXColor color, + s32 pos_x, + char *prefix, + s32 frames, + timerdisp::TimeFormat format) { u32 prefix_len = mkb::strlen(prefix); s32 x = pos_x - prefix_len * draw::DEBUG_CHAR_WIDTH; char time_buf[16] = {}; timerdisp::format_signed_time(time_buf, frames, format); - draw_new(slot, x, color, true, "%s%s", prefix, time_buf); + draw(slot, x, color, true, "%s%s", prefix, time_buf); } -void draw_timer_new(Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format) { - draw_timer_main_new(slot, color, get_slot_x_alignment(slot), prefix, frames, format); +void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format) { + draw_timer_main(slot, color, get_slot_x_alignment(slot), prefix, frames, format); } void draw_subtick_timer_main(Slot slot, @@ -259,7 +225,7 @@ void draw_subtick_timer_main(Slot slot, char time_buf[16] = {}; timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); - draw_new(slot, x, color, true, "%s%s", prefix, time_buf); + draw(slot, x, color, true, "%s%s", prefix, time_buf); } void draw_subtick_timer(Slot slot, @@ -272,99 +238,12 @@ void draw_subtick_timer(Slot slot, extra_precision); } -// --- - -// The main text drawing function -// We will almost always want to input true for the incr_row argument -// However, in cases where we want to draw two (or more) pieces of text in the same slot and in -// the same row with custom horizontal spacing, inputting false can be useful -// This is used in ilbattle.cpp -/* void draw_main_v(s32 min_row, - Slot slot, - s32 pos_x, - GXColor color, - bool incr_row, - char *format, - va_list args) { - for (u16 k = 0; k < LEN(s_slot_list); k++) { - if (s_slot_list[k] == slot) { - u16 row = MAX(min_row, s_active_row[k]); - s32 pos_y = timerdisp::row_number_to_vertical_pos(row); - - draw::debug_text_v(pos_x, pos_y, color, format, args); - - if (incr_row) { - s_active_row[k] = row + 1; - } - } - } -} */ - -/* void draw_v(Module module, - Slot slot, - s32 pos_x, - GXColor color, - bool incr_row, - char *format, - va_list args) { - draw_main_v(module_and_slot_to_min_row(module, slot), slot, pos_x, color, incr_row, format, - args); -} - -void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { - va_list args; - va_start(args, format); - draw_v(module, slot, pos_x, color, incr_row, format, args); - va_end(args); -} - -// When we want to align according to module_and_slot_to_x_alignment() -void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...) { - va_list args; - va_start(args, format); - draw_v(module, slot, module_and_slot_to_x_alignment(module, slot), color, true, format, args); - va_end(args); -} */ - -// Aligns the time according to get_slot_x_alignment; the prefix gets automatically moved to the -// left depending on string length -/* void draw_timer(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format) { - s32 num_x = module_and_slot_to_x_alignment(module, slot); - u32 prefix_len = mkb::strlen(prefix); - s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; - - char time_buf[16] = {}; - timerdisp::format_signed_time(time_buf, frames, format); - draw(module, slot, x, color, true, "%s%s", prefix, time_buf); -} - -void draw_subtick_timer(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - u32 framesave, - bool extra_precision) { - s32 num_x = module_and_slot_to_x_alignment(module, slot); - u32 prefix_len = mkb::strlen(prefix); - s32 x = num_x - prefix_len * draw::DEBUG_CHAR_WIDTH; - - char time_buf[16] = {}; - timerdisp::format_subtick_time(time_buf, frames, framesave, extra_precision); - draw(module, slot, x, color, true, "%s%s", prefix, time_buf); -} */ - void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { s_active_row[k] = get_slot_min_row(s_slot_list[k]); } - for (u16 k = 0; k < SLOT_BUF_COUNT; k++) { - s_slot_display_count[k] = 0; + for (u16 k = 0; k < BUFFER_SLOT_COUNT; k++) { + s_buffer_slot_display_count[k] = 0; } } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 5ab8a63f..8a0c9c43 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -12,56 +12,19 @@ enum class Slot { Right, }; -enum class Module { - StageTimer, // Includes RTA/Pause timer + framesave stuff - IwTimer, - CmSeg, - IlBattle, - DeathCounter, - LoadlessTimer, -}; - s32 get_slot_x_alignment(Slot slot); -// s32 module_and_slot_to_x_alignment(Module module, Slot slot); - -// void draw(Module module, Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); -// void draw_aligned(Module module, Slot slot, GXColor color, char *format, ...); -/* void draw_timer(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format); */ -/* void draw_subtick_timer(Module module, - Slot slot, - GXColor color, - char *prefix, - s32 frames, - u32 framesave, - bool extra_precision); */ +void draw_v(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, va_list args); +void draw(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); +void draw_aligned(Slot slot, GXColor color, char *format, ...); -// new stuff -void draw_main_v_new(Slot slot, - s32 pos_x, +void draw_timer_main(Slot slot, GXColor color, - bool incr_row, - char *format, - va_list args); -void draw_new(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...); -void draw_aligned_new(Slot slot, GXColor color, char *format, ...); - -void draw_timer_main_new(Slot slot, - GXColor color, - s32 pos_x, - char *prefix, - s32 frames, - timerdisp::TimeFormat format); -void draw_timer_new(Slot slot, - GXColor color, - char *prefix, - s32 frames, - timerdisp::TimeFormat format); + s32 pos_x, + char *prefix, + s32 frames, + timerdisp::TimeFormat format); +void draw_timer(Slot slot, GXColor color, char *prefix, s32 frames, timerdisp::TimeFormat format); void draw_subtick_timer(Slot slot, GXColor color, char *prefix, From 4087f0fabf48879323f59b04bddf3c2d4a60772e Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 17:03:36 -0500 Subject: [PATCH 82/90] removed a lot of freecam::should_hide_hud() calls instead, freecam itself directly uses a setter provided by textinfo --- src/mods/cmseg.cpp | 2 +- src/mods/deathcounter.cpp | 3 ++- src/mods/freecam.cpp | 8 ++++++++ src/mods/ilbattle.cpp | 7 +++---- src/mods/iw.cpp | 3 +-- src/mods/storytimer.cpp | 4 ++-- src/mods/timer.cpp | 8 ++++---- src/systems/textinfo.h | 2 ++ 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index af7deb99..2d71d36b 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -416,7 +416,7 @@ void tick() { } void disp() { - if (!pref::get(pref::Pref::CmTimer) || freecam::should_hide_hud()) return; + if (!pref::get(pref::Pref::CmTimer)) return; if (s_state == State::SegActive || s_state == State::SegComplete) { u32 seg = static_cast(s_seg_request); diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 182e390c..122f4ac2 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -139,8 +139,9 @@ bool should_not_display_counter_at_all() { // If we're in the menus outside of a story mode run due to an accidental exit game, we // still want to be able to display the counter if we haven't reset it yet return !storyreset::is_run_active(); + } else { + return false; } - return freecam::should_hide_hud(); } void disp() { diff --git a/src/mods/freecam.cpp b/src/mods/freecam.cpp index 91b86f05..1f0810ee 100644 --- a/src/mods/freecam.cpp +++ b/src/mods/freecam.cpp @@ -5,6 +5,7 @@ #include "systems/binds.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" @@ -169,6 +170,13 @@ void tick() { pref::save(); } } + + // Handle hiding all ui elements coming from textinfo + if (should_hide_hud()) { + textinfo::set_drawing_state(false); + } else { + textinfo::set_drawing_state(true); + } } } // namespace freecam diff --git a/src/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 5390b7aa..6c995f7b 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -1,15 +1,14 @@ #include "ilbattle.h" + +#include #include "mkb/mkb.h" #include "mods/freecam.h" #include "mods/validate.h" #include "systems/binds.h" -#include "systems/pad.h" #include "systems/pref.h" -#include "systems/savest.h" #include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" -#include "utils/patch.h" #include "utils/relutil.h" #include "utils/timerdisp.h" @@ -385,7 +384,7 @@ void disp() { return; } - if (!pref::get(pref::Pref::IlBattleDisplay) || freecam::should_hide_hud()) return; + if (!pref::get(pref::Pref::IlBattleDisplay)) return; switch (s_state) { case IlBattleState::NotReady: { diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index 44468cb7..c5d2bde2 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -122,8 +122,7 @@ void tick() { void disp() { if (!pref::get(pref::Pref::IwTimer) || mkb::main_mode != mkb::MD_GAME || - mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw || - freecam::should_hide_hud()) + mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw) return; textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), timerdisp::TimeFormat::AlwaysLeadNonHours); diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 1c5c0819..eb3a7a7e 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -268,9 +268,9 @@ bool should_not_display_timer_at_all() { if (!mode::is_main_game_mode_story(mkb::main_game_mode)) { // return get_loadless_time() == 0; return !is_run_active(); + } else { + return false; } - // If in story, hide the timer if freecamming (if the pref is on) - return freecam::should_hide_hud(); } void disp() { diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index 47b0f2f4..93736a01 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -60,11 +60,11 @@ void disp() { using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; - if (pref::get(pref::Pref::TimerShowRTA) && !freecam::should_hide_hud()) { + if (pref::get(pref::Pref::TimerShowRTA)) { textinfo::draw_timer(Slot::Right, draw::WHITE, "RTA:", s_rta_timer, Format::SecondsOnly); } - if (pref::get(pref::Pref::TimerShowPause) && !freecam::should_hide_hud()) { + if (pref::get(pref::Pref::TimerShowPause)) { textinfo::draw_timer(Slot::Right, draw::WHITE, "PAU:", s_pause_timer, Format::SecondsOnly); } @@ -80,12 +80,12 @@ void disp() { u32 framesave = validate::get_framesave(); - if (pref::get(pref::Pref::TimerShowSubtick) && !freecam::should_hide_hud()) { + if (pref::get(pref::Pref::TimerShowSubtick)) { textinfo::draw_subtick_timer(Slot::Right, draw::WHITE, "SUB:", s_rta_timer, framesave, false); } - if (pref::get(pref::Pref::TimerShowFramesave) && !freecam::should_hide_hud()) { + if (pref::get(pref::Pref::TimerShowFramesave)) { s32 num_x = textinfo::get_slot_x_alignment(Slot::Right); s32 x = num_x - 4 * draw::DEBUG_CHAR_WIDTH; // "FSV:" is 4 characters long textinfo::draw(Slot::Right, x, draw::WHITE, true, "FSV:%2d%", framesave); diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index 8a0c9c43..e1867a7e 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -12,6 +12,8 @@ enum class Slot { Right, }; +void set_drawing_state(bool draw_elements); + s32 get_slot_x_alignment(Slot slot); void draw_v(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, va_list args); From 7e9cb067b48accfe9e04d9ff04f375f11891d960 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 17:04:29 -0500 Subject: [PATCH 83/90] reset story run if you used cm segments --- src/mods/storyreset.cpp | 8 +++++++- src/utils/mode.cpp | 15 +++++++++++---- src/utils/mode.h | 9 +++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/mods/storyreset.cpp b/src/mods/storyreset.cpp index 361c9852..69e5980d 100644 --- a/src/mods/storyreset.cpp +++ b/src/mods/storyreset.cpp @@ -80,6 +80,12 @@ bool is_on_wrong_menu() { return false; } +// We also need this in case we use challenge mode segments to enter challenge mode without going +// through the menus +bool in_challenge_mode() { + return mode::is_main_game_mode_challenge(mkb::main_game_mode); +} + bool used_go_to_story() { return gotostory::get_gotostory_state() != gotostory::State::Default; } @@ -93,7 +99,7 @@ bool should_reset_completed_run() { bool should_reset_run() { return should_reset_on_file_screen() || is_on_wrong_menu() || used_go_to_story() || - should_reset_completed_run(); + in_challenge_mode() || should_reset_completed_run(); } void tick() { diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp index 94f39911..c8d1dd4d 100644 --- a/src/utils/mode.cpp +++ b/src/utils/mode.cpp @@ -2,6 +2,7 @@ // #include "../internal/relutil.h" #include "mkb/mkb.h" +#include "mkb/mkb2_ghidra.h" namespace mode { @@ -141,6 +142,16 @@ bool is_sel_ngc(mkb::SubMode submode) { return (submode == mkb::SMD_SEL_NGC_INIT || submode == mkb::SMD_SEL_NGC_MAIN); } +// --- main game modes --- + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode) { + return main_game_mode == mkb::STORY_MODE; +} + +bool is_main_game_mode_challenge(mkb::MainGameMode main_game_mode) { + return main_game_mode == mkb::CHALLENGE_MODE; +} + // --- story exit game submodes --- bool is_story_exit_game_init(mkb::SubMode submode) { @@ -172,10 +183,6 @@ bool is_stage_exit_init(mkb::SubMode submode) { // --- some story mode stuff --- -bool is_main_game_mode_story(mkb::MainGameMode main_game_mode) { - return main_game_mode == mkb::STORY_MODE; -} - bool is_story_cutscene(mkb::SubMode submode) { return (submode == mkb::SMD_AUTHOR_PLAY_INIT || submode == mkb::SMD_AUTHOR_PLAY_MAIN || submode == mkb::SMD_AUTHOR_PLAY_STORY_INIT || diff --git a/src/utils/mode.h b/src/utils/mode.h index 702e51e4..22775ab0 100644 --- a/src/utils/mode.h +++ b/src/utils/mode.h @@ -43,6 +43,11 @@ bool is_story_exit_game(mkb::SubMode submode); bool is_stage_exit_init(mkb::SubMode submode); bool is_stage_exit_submode(mkb::SubMode submode); +// main game modes + +bool is_main_game_mode_story(mkb::MainGameMode main_game_mode); +bool is_main_game_mode_challenge(mkb::MainGameMode main_game_mode); + // 10 ball screen stuff bool is_on_10_ball_spin_in(mkb::StoryModeStageSelectState state); @@ -54,8 +59,6 @@ bool is_on_10_ball_screen(mkb::SubMode sub_mode, mkb::ScenInfo scen_info); // Other storymode stuff -bool is_main_game_mode_story(mkb::MainGameMode main_game_mode); - bool is_storymode_file_screen_init(mkb::ScenInfo scen_info); bool is_storymode_file_screen_main(mkb::ScenInfo scen_info); bool is_storymode_name_entry_screen_main(mkb::ScenInfo scen_info); @@ -66,8 +69,6 @@ bool is_story_cutscene(mkb::SubMode submode); u16 get_clear_count_for_world(); u16 get_storymode_total_clear_count(); -// bool is_between_worlds(bool has_entered_goal); -// bool is_run_complete(bool has_entered_goal); // misc utility From 736f32927460b2cd9ce19298ec570c68f773c525 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 21:53:31 -0500 Subject: [PATCH 84/90] move run breakdown code to its own file not sure if this is too frivolous, but I like storytimer being fully independent from deathcounter --- src/mods/storybreakdown.cpp | 71 +++++++++++++++++++++++++++++++++++++ src/mods/storybreakdown.h | 9 +++++ src/mods/storytimer.cpp | 49 +------------------------ src/mods/storytimer.h | 2 ++ src/systems/main.cpp | 6 +++- 5 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 src/mods/storybreakdown.cpp create mode 100644 src/mods/storybreakdown.h diff --git a/src/mods/storybreakdown.cpp b/src/mods/storybreakdown.cpp new file mode 100644 index 00000000..d43ba9b6 --- /dev/null +++ b/src/mods/storybreakdown.cpp @@ -0,0 +1,71 @@ +#include "storybreakdown.h" + +#include "deathcounter.h" +#include "storytimer.h" +#include "systems/goal.h" +#include "systems/pref.h" +#include "systems/textinfo.h" +#include "utils/draw.h" +#include "utils/mode.h" + +namespace storybreakdown { + +constexpr u16 WORLD_COUNT = mode::WORLD_COUNT; +constexpr u16 STAGES_PER_WORLD = mode::STAGES_PER_WORLD; + +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + +// We only use this function for 0 <= row <= 10 +s32 get_breakdown_row_x_pos(u16 row) { + s32 num_x_pos = textinfo::get_slot_x_alignment(Slot::Left); + if (row < WORLD_COUNT - 1) { // "Wk:" is 3 characters long if 1 <= k <= 9 + return num_x_pos - 3 * draw::DEBUG_CHAR_WIDTH; + } else if (row == WORLD_COUNT - 1) { // "W10:" is 4 characters long + return num_x_pos - 4 * draw::DEBUG_CHAR_WIDTH; + } else { // "Totals:" is 7 characters long + return num_x_pos - 7 * draw::DEBUG_CHAR_WIDTH; + } +} + +void draw_breakdown_screen() { + // Format: "Wk: world k split time (world k segment time) (world k deaths)" + char split_buf[16] = {}; + char seg_buf[16] = {}; + + for (u16 idx = 0; idx < WORLD_COUNT; idx++) { + s32 pos_x = get_breakdown_row_x_pos(idx); + u32 world_split = storytimer::get_split_timer_for_world(idx); + u32 world_segment = storytimer::get_world_timer_info(idx).segment; + u32 world_deaths = deathcounter::get_world_death_count(idx); + + timerdisp::format_time(split_buf, world_split, Format::MinutesAlwaysLeadingZero); + timerdisp::format_time(seg_buf, world_segment, Format::MinutesAlwaysLeadingZero); + + textinfo::draw(Slot::Left, pos_x, draw::WHITE, true, "W%d:%s (%s) (%d)", idx + 1, split_buf, + seg_buf, world_deaths); + } + + // For the totals row + s32 totals_x_pos = get_breakdown_row_x_pos(WORLD_COUNT); + u32 loadless_time = storytimer::get_loadless_time(); + u32 total_deaths = deathcounter::get_total_death_count(); + + char total_time_buf[16] = {}; + timerdisp::format_time(total_time_buf, loadless_time, Format::MinutesAlwaysLeadingZero); + + textinfo::draw(Slot::Left, totals_x_pos, draw::WHITE, true, "Totals:%s (%d)", total_time_buf, + total_deaths); +} + +void disp() { + if (storytimer::should_not_display_timer_at_all()) { + return; + } + + if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { + draw_breakdown_screen(); + } +} + +} // namespace storybreakdown \ No newline at end of file diff --git a/src/mods/storybreakdown.h b/src/mods/storybreakdown.h new file mode 100644 index 00000000..b0938d7b --- /dev/null +++ b/src/mods/storybreakdown.h @@ -0,0 +1,9 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace storybreakdown { + +void disp(); + +} // namespace storybreakdown diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index eb3a7a7e..229759db 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -2,8 +2,6 @@ #include "mkb/mkb.h" -#include "deathcounter.h" -#include "freecam.h" #include "storyreset.h" #include "systems/goal.h" #include "systems/pref.h" @@ -29,7 +27,7 @@ static WorldTimer s_world_timer[WORLD_COUNT]; // timer info for each world using Slot = textinfo::Slot; using Format = timerdisp::TimeFormat; -// --- some getters that other files can use (if needed) --- +// --- some getters that other files can use --- WorldTimer get_world_timer_info(u16 world_idx) { // clamp for safety so we don't access outside the bounds of the array @@ -221,47 +219,6 @@ void draw_timers() { } } -// We only use this function for 0 <= row <= 10 -s32 get_breakdown_row_x_pos(u16 row) { - s32 num_x_pos = textinfo::get_slot_x_alignment(Slot::Left); - if (row < WORLD_COUNT - 1) { // "Wk:" is 3 characters long if 1 <= k <= 9 - return num_x_pos - 3 * draw::DEBUG_CHAR_WIDTH; - } else if (row == WORLD_COUNT - 1) { // "W10:" is 4 characters long - return num_x_pos - 4 * draw::DEBUG_CHAR_WIDTH; - } else { // "Totals:" is 7 characters long - return num_x_pos - 7 * draw::DEBUG_CHAR_WIDTH; - } -} - -void draw_breakdown_screen() { - // Format: "Wk: world k split time (world k segment time) (world k deaths)" - char split_buf[16] = {}; - char seg_buf[16] = {}; - char row_info_buf[32] = {}; - - for (u16 idx = 0; idx < WORLD_COUNT; idx++) { - s32 pos_x = get_breakdown_row_x_pos(idx); - u32 world_deaths = deathcounter::get_world_death_count(idx); - - timerdisp::format_time(split_buf, get_split_timer_for_world(idx), - Format::MinutesAlwaysLeadingZero); - timerdisp::format_time(seg_buf, s_world_timer[idx].segment, - Format::MinutesAlwaysLeadingZero); - mkb::sprintf(row_info_buf, "W%d:%s (%s) (%d)", idx + 1, split_buf, seg_buf, world_deaths); - - textinfo::draw(Slot::Left, pos_x, draw::WHITE, true, row_info_buf); - } - - // For the totals row - s32 totals_x_pos = get_breakdown_row_x_pos(WORLD_COUNT); - u32 total_deaths = deathcounter::get_total_death_count(); - - char total_time_buf[16] = {}; - timerdisp::format_time(total_time_buf, get_loadless_time(), Format::MinutesAlwaysLeadingZero); - textinfo::draw(Slot::Left, totals_x_pos, draw::WHITE, true, "Totals:%s (%d)", total_time_buf, - total_deaths); -} - bool should_not_display_timer_at_all() { // The only time we can ever display the timer outside of story mode is // when we fully exit game and the timer is still running @@ -279,10 +236,6 @@ void disp() { } draw_timers(); - - if (pref::get(pref::Pref::ShowRunBreakdown) && goal::is_run_complete()) { - draw_breakdown_screen(); - } } static patch::Tramp diff --git a/src/mods/storytimer.h b/src/mods/storytimer.h index c4dff0f0..ac6e0cc0 100644 --- a/src/mods/storytimer.h +++ b/src/mods/storytimer.h @@ -13,6 +13,8 @@ WorldTimer get_world_timer_info(u16 world_idx); u32 get_split_timer_for_world(u16 world_idx); u32 get_loadless_time(); +bool should_not_display_timer_at_all(); + void init_main_game(); void tick(); void disp(); diff --git a/src/systems/main.cpp b/src/systems/main.cpp index a665d55f..478ddb91 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -39,6 +39,7 @@ #include "mods/scratch.h" #include "mods/sfx.h" #include "mods/stage_edits.h" +#include "mods/storybreakdown.h" #include "mods/storyreset.h" #include "mods/storytimer.h" #include "mods/timer.h" @@ -139,16 +140,19 @@ static patch::Tramp s_draw_debug_text_tramp([]() { iw::disp(); deathcounter::disp(); storytimer::disp(); + storybreakdown::disp(); ilbattle::disp(); cmseg::disp(); timer::disp(); + textinfo::disp(); // Everything using textinfo's draw functions must have their disp() + // functions come before it inputdisp::disp(); menu_impl::disp(); draw::disp(); ilmark::disp(); physics::disp(); scratch::disp(); - textinfo::disp(); + // textinfo::disp(); }); static patch::Tramp s_smd_game_ready_init_tramp([]() { From 39db605ef725364e98805364cb165f74db8fb263 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Mon, 24 Aug 2026 21:55:09 -0500 Subject: [PATCH 85/90] simplify should_display_timer() a bit also added some textinfo type-aliases to cmseg/iw for consistency with other files --- src/mods/cmseg.cpp | 7 +++++-- src/mods/iw.cpp | 7 +++++-- src/mods/storytimer.cpp | 22 +++++++--------------- src/mods/timer.cpp | 6 +++--- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/mods/cmseg.cpp b/src/mods/cmseg.cpp index 2d71d36b..de9374f4 100644 --- a/src/mods/cmseg.cpp +++ b/src/mods/cmseg.cpp @@ -36,6 +36,9 @@ static s8 s_overwritten_starting_monkeys; static u32 s_pbs[14]; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + // static void debug_print_course(mkb::CourseCommand *course, u32 entry_count) //{ // static const char *type_strs[] = {"COURSE_CMD_IF", "COURSE_CMD_THEN", "COURSE_CMD_INFO", @@ -425,8 +428,8 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - textinfo::draw_timer(textinfo::Slot::Right, color, "SEG:", static_cast(s_seg_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + textinfo::draw_timer(Slot::Right, color, "SEG:", static_cast(s_seg_time), + Format::AlwaysLeadNonHours); } } diff --git a/src/mods/iw.cpp b/src/mods/iw.cpp index c5d2bde2..c8c19bf3 100644 --- a/src/mods/iw.cpp +++ b/src/mods/iw.cpp @@ -27,6 +27,9 @@ static u32 s_iw_files; // Bitflag for which save files are IW save files static u32 s_iw_time; static u32 s_prev_retrace_count; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + static void handle_iw_selection() { if (mkb::scen_info.mode != 5) return; @@ -124,8 +127,8 @@ void disp() { if (!pref::get(pref::Pref::IwTimer) || mkb::main_mode != mkb::MD_GAME || mkb::main_game_mode != mkb::STORY_MODE || !main::currently_playing_iw) return; - textinfo::draw_timer(textinfo::Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), - timerdisp::TimeFormat::AlwaysLeadNonHours); + textinfo::draw_timer(Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + Format::AlwaysLeadNonHours); } } // namespace iw diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp index 229759db..4518a16c 100644 --- a/src/mods/storytimer.cpp +++ b/src/mods/storytimer.cpp @@ -158,26 +158,18 @@ bool should_display_timer(TimerType type) { pref = pref::get(pref::Pref::SegmentTimerOptions); } + if (type == TimerType::Segment && pref::get(pref::Pref::ShowRunBreakdown) && + goal::is_run_complete()) { + return false; + } + using TimerOptions = storyreset::StoryDisplayOptions; switch (TimerOptions(pref)) { case TimerOptions::AlwaysShow: - if (type == TimerType::Fullgame) { - return true; - } else if (pref::get(pref::Pref::ShowRunBreakdown)) { - // type is segment timer + show breakdown on - return !goal::is_run_complete(); - } else { // type is segment timer + show breakdown off - return true; - } + return true; case TimerOptions::BetweenWorlds: - if (type == TimerType::Fullgame) { - return goal::is_between_worlds(); - } else if (pref::get(pref::Pref::ShowRunBreakdown)) { - return goal::is_between_worlds() && !goal::is_run_complete(); - } else { - return goal::is_between_worlds(); - } + return goal::is_between_worlds(); case TimerOptions::EndOfRun: if (type == TimerType::Fullgame) { return goal::is_run_complete(); diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index 93736a01..fe952037 100644 --- a/src/mods/timer.cpp +++ b/src/mods/timer.cpp @@ -16,6 +16,9 @@ static u32 s_prev_retrace_count; static s32 s_rta_timer; static s32 s_pause_timer; +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + void init() { s_retrace_count = mkb::VIGetRetraceCount(); } @@ -57,9 +60,6 @@ void disp() { } } - using Slot = textinfo::Slot; - using Format = timerdisp::TimeFormat; - if (pref::get(pref::Pref::TimerShowRTA)) { textinfo::draw_timer(Slot::Right, draw::WHITE, "RTA:", s_rta_timer, Format::SecondsOnly); } From d8b0b1968f9a56a0ddd364468fd0a5f8d5b68f38 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 25 Aug 2026 08:13:16 -0500 Subject: [PATCH 86/90] update input display to use textinfo api Updated textinfo to give other mods the ability to decide to move the right side slot farther right; to do so, I moved set_sprite_visible() from inputdisp to textinfo. inputdisp now runs before textinfo in main. Input display raw stick vals now play nicely with other rows of text on the right side of the screen --- src/mods/inputdisp.cpp | 56 ++++++++++++----------------- src/systems/main.cpp | 3 +- src/systems/textinfo.cpp | 78 +++++++++++++++++++++++++++++++--------- src/systems/textinfo.h | 3 ++ 4 files changed, 90 insertions(+), 50 deletions(-) diff --git a/src/mods/inputdisp.cpp b/src/mods/inputdisp.cpp index b4ea9d3f..c46956c1 100644 --- a/src/mods/inputdisp.cpp +++ b/src/mods/inputdisp.cpp @@ -7,9 +7,11 @@ #include "systems/log.h" #include "systems/pad.h" #include "systems/pref.h" +#include "systems/textinfo.h" #include "utils/draw.h" #include "utils/macro_utils.h" #include "utils/patch.h" +#include "utils/timerdisp.h" namespace inputdisp { @@ -134,28 +136,6 @@ static void draw_circle(u32 pts, Vec2d center, f32 radius, GXColor color) { } } -static void set_sprite_visible(bool visible) { - if (mkb::main_mode != mkb::MD_GAME) return; - - // Hide distracting score sprites under the input display - for (u32 i = 0; i < mkb::sprite_pool_info.upper_bound; i++) { - if (mkb::sprite_pool_info.status_list[i] == 0) continue; - - mkb::Sprite &sprite = mkb::sprites[i]; - if (sprite.bmp == 0x503 || sprite.tick_func == mkb::sprite_monkey_counter_tick || - sprite.disp_func == mkb::sprite_monkey_counter_icon_disp || sprite.bmp == 0x502 || - sprite.tick_func == mkb::sprite_banana_icon_tick || - sprite.tick_func == mkb::sprite_banana_icon_shadow_tick || - sprite.tick_func == mkb::sprite_banana_count_tick || - mkb::strcmp(sprite.text, ":") == 0 || - sprite.disp_func == mkb::sprite_hud_player_num_disp) { - if ((visible && sprite.depth < 0.f) || (!visible && sprite.depth >= 0.f)) { - sprite.depth = -sprite.depth; - } - } - } -} - static patch::Tramp s_create_speed_sprites_tramp([](f32 x, f32 y) { s_create_speed_sprites_tramp.chain(x + 5, y); }); @@ -166,9 +146,21 @@ void init() { void tick() { s_rainbow = (s_rainbow + 3) % 1080; - set_sprite_visible(!pref::get(pref::Pref::InputDisp) || - (pref::get(pref::Pref::InputDispLocation) && - !pref::get(pref::Pref::InputDispRawStickInputs))); + + bool input_disp_right_side = pref::get(pref::Pref::InputDispLocation) == 0; + + if (pref::get(pref::Pref::InputDisp) && !input_disp_right_side) { + // On + center location + textinfo::move_right_side_text_farther_right(true); + } else { + textinfo::move_right_side_text_farther_right(false); + } + + if (input_disp_right_side) { + // Any time we're drawing the input display on the right side, hide the monkey head/banana + // counter sprites + textinfo::set_sprite_visible(false); + } } static bool get_notch_pos(const pad::StickState &stick_inputs, Vec2d *out_pos) { @@ -344,15 +336,13 @@ static void draw_raw_stick_inputs(const pad::StickState &raw_stick_inputs, const pad::StickState &stick_inputs) { if (!pref::get(pref::Pref::InputDispRawStickInputs)) return; - Vec2d center = { - .x = pref::get(pref::Pref::InputDispLocation) ? 390.f : 540.f, - .y = 28.f, - }; + using Slot = textinfo::Slot; + using Format = timerdisp::TimeFormat; - draw::debug_text(center.x, center.y + 0 * 14, draw::WHITE, "rX: %d", raw_stick_inputs.x); - draw::debug_text(center.x, center.y + 1 * 14, draw::WHITE, "rY: %d", raw_stick_inputs.y); - draw::debug_text(center.x, center.y + 2 * 14, draw::WHITE, "gX: %d", stick_inputs.x); - draw::debug_text(center.x, center.y + 3 * 14, draw::WHITE, "gY: %d", stick_inputs.y); + textinfo::draw_timer(Slot::Right, draw::WHITE, "rX:", raw_stick_inputs.x, Format::Unformatted); + textinfo::draw_timer(Slot::Right, draw::WHITE, "rY:", raw_stick_inputs.y, Format::Unformatted); + textinfo::draw_timer(Slot::Right, draw::WHITE, "gX:", stick_inputs.x, Format::Unformatted); + textinfo::draw_timer(Slot::Right, draw::WHITE, "gY:", stick_inputs.x, Format::Unformatted); } void disp() { diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 478ddb91..20109068 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -137,6 +137,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { draw::predraw(); // timer::disp(); + inputdisp::disp(); iw::disp(); deathcounter::disp(); storytimer::disp(); @@ -146,7 +147,7 @@ static patch::Tramp s_draw_debug_text_tramp([]() { timer::disp(); textinfo::disp(); // Everything using textinfo's draw functions must have their disp() // functions come before it - inputdisp::disp(); + // inputdisp::disp(); menu_impl::disp(); draw::disp(); ilmark::disp(); diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 657af7df..89dd285e 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -10,6 +10,13 @@ namespace textinfo { +// General Notes +// (1) "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get +// stacked on top of each other +// (2) Run order of disp() functions in main.cpp determines stacking order +// (3) Every mod that uses textinfo should have their disp() function run before textinfo::disp() in +// main + struct TextData { s32 pos_x; u8 row; @@ -30,7 +37,7 @@ static const Slot s_slot_list[]{ // Number of slots whose draw calls get stored in a buffer constexpr u16 BUFFER_SLOT_COUNT = 1; // Max number of strings that each buffer slot can accept -constexpr u16 BUFFER_SLOT_MAX_STRS = 6; +constexpr u16 BUFFER_SLOT_MAX_STRS = 8; static char s_row_buf[BUFFER_SLOT_COUNT][BUFFER_SLOT_MAX_STRS][16] = {}; // How many strings are actually being displayed in each buffer slot static u16 s_buffer_slot_display_count[BUFFER_SLOT_COUNT] = {}; @@ -39,6 +46,7 @@ static TextData s_text_data[BUFFER_SLOT_COUNT][BUFFER_SLOT_MAX_STRS]; static u16 s_active_row[LEN(s_slot_list)] = {}; static bool s_enable_drawing = true; +static bool s_move_right_slot = false; void init_text_data() { for (u16 j = 0; j < BUFFER_SLOT_COUNT; j++) { @@ -57,6 +65,10 @@ void set_drawing_state(bool draw_elements) { s_enable_drawing = draw_elements; } +void move_right_side_text_farther_right(bool move_farther_right) { + s_move_right_slot = move_farther_right; +} + // We only need to potentially center text that gets drawn on the right side of the screen, so there // is no need to store things in other slots to the buffer s16 get_buffer_slot_idx(Slot slot) { @@ -73,9 +85,6 @@ bool slot_is_buffer_slot(Slot slot) { return get_buffer_slot_idx(slot) != -1; } -// "Slots" are used to determine stacking behavior, ie rows of text in the same slot will get -// stacked on top of each other - // Where the numbers get lined up (this acts as the "default" number alignment per slot, but // individual mods can specify a different x-position in the relevant draw() calls if necessary) s32 get_slot_x_alignment(Slot slot) { @@ -83,7 +92,11 @@ s32 get_slot_x_alignment(Slot slot) { case Slot::Left: return 102; case Slot::Right: - return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + if (!s_move_right_slot) { + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + } else { + return 540; + } default: return 0; } @@ -136,11 +149,6 @@ void draw_v(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, va s_text_data[idx][text_idx].color = color; s_buffer_slot_display_count[idx] += 1; - - if (pad::button_pressed(mkb::PAD_BUTTON_B)) { - mkb::OSReport("Stored to buf. Idx: %d Row: %d \n", idx, row); - mkb::OSReport(s_row_buf[idx][row]); - } } else { // If not storing this slot's draw calls to the buffer, draw instantly s32 pos_y = timerdisp::row_number_to_vertical_pos(row); @@ -169,11 +177,6 @@ void draw_from_buf() { s32 pos_y = get_modified_slot_y_pos(s_slot_list[k], row); draw::debug_text(s_text_data[idx][j].pos_x, pos_y, s_text_data[idx][j].color, s_text_data[idx][j].text); - - if (pad::button_pressed(mkb::PAD_BUTTON_A)) { - mkb::OSReport("Drawing. Active Row: %d Count: %d \n", row, - s_buffer_slot_display_count[idx]); - } } } } @@ -238,6 +241,41 @@ void draw_subtick_timer(Slot slot, extra_precision); } +// Mods have the ability to decide if we should move the right side slot text farther right +// (example: input display) +// So, we need to hide the right side sprites if we're drawing text there +void set_sprite_visible(bool visible) { + if (mkb::main_mode != mkb::MD_GAME) return; + + // Hide distracting score sprites under the input display + for (u32 i = 0; i < mkb::sprite_pool_info.upper_bound; i++) { + if (mkb::sprite_pool_info.status_list[i] == 0) continue; + + mkb::Sprite &sprite = mkb::sprites[i]; + if (sprite.bmp == 0x503 || sprite.tick_func == mkb::sprite_monkey_counter_tick || + sprite.disp_func == mkb::sprite_monkey_counter_icon_disp || sprite.bmp == 0x502 || + sprite.tick_func == mkb::sprite_banana_icon_tick || + sprite.tick_func == mkb::sprite_banana_icon_shadow_tick || + sprite.tick_func == mkb::sprite_banana_count_tick || + mkb::strcmp(sprite.text, ":") == 0 || + sprite.disp_func == mkb::sprite_hud_player_num_disp) { + if ((visible && sprite.depth < 0.f) || (!visible && sprite.depth >= 0.f)) { + sprite.depth = -sprite.depth; + } + } + } +} + +bool should_hide_right_side_sprites() { + u16 idx = get_buffer_slot_idx(Slot::Right); + // When some mod decides we should be moving the right slot farther right + displaying at least + // one row of text in the right slot + if (s_move_right_slot && s_buffer_slot_display_count[idx] != 0) { + return true; + } + return false; +} + void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { s_active_row[k] = get_slot_min_row(s_slot_list[k]); @@ -251,9 +289,17 @@ void init() { init_text_data(); } +// textinfo's disp() runs after all other disp() functions that use textinfo void disp() { - // textinfo's disp() runs after all other disp() functions draw_from_buf(); + + if (should_hide_right_side_sprites()) { + set_sprite_visible(false); + } else { + set_sprite_visible(true); + } + + // This runs after all possible textinfo::draw() calls reset_active_rows(); } diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index e1867a7e..f9e88012 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -13,6 +13,7 @@ enum class Slot { }; void set_drawing_state(bool draw_elements); +void move_right_side_text_farther_right(bool move_farther_right); s32 get_slot_x_alignment(Slot slot); @@ -34,6 +35,8 @@ void draw_subtick_timer(Slot slot, u32 framesave, bool extra_precision); +void set_sprite_visible(bool visible); + void init(); void disp(); From d130de8793ea97699a1626ec1a97df0387341333 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 25 Aug 2026 18:34:05 -0500 Subject: [PATCH 87/90] add pref for right side sprite hiding --- src/systems/menu_defn.cpp | 12 ++++++++++++ src/systems/pref.cpp | 3 ++- src/systems/pref.h | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index 8af4347c..8bd0c6f5 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1641,7 +1641,19 @@ static Widget s_il_mark_widgets[] = { }, }; +const char *RIGHT_SIDE_UI_HIDING_OPTIONS[] = {"Hide All", "Hide Monkey Head", "Hide None"}; + static Widget s_displays_widgets[] = { + { + .type = WidgetType::Choose, + .choose = + { + .label = "Right Side UI", + .choices = RIGHT_SIDE_UI_HIDING_OPTIONS, + .num_choices = LEN(RIGHT_SIDE_UI_HIDING_OPTIONS), + .pref = pref::Pref::RightSideUIHide, + }, + }, { .type = WidgetType::Menu, .menu = {"Input Display", s_inputdisp_widgets, LEN(s_inputdisp_widgets)}, diff --git a/src/systems/pref.cpp b/src/systems/pref.cpp index a79d109e..8746efee 100644 --- a/src/systems/pref.cpp +++ b/src/systems/pref.cpp @@ -106,6 +106,7 @@ static const Pref PREF_IDS[] = { Pref::DeathCounterDisplayOptions, Pref::CountFirstStageDeaths, Pref::MenuAcceleration, + Pref::RightSideUIHide, }; struct DefaultPref { @@ -146,7 +147,7 @@ static const DefaultPref DEFAULT_PREFS[] = { // End preferences definition // -static constexpr u32 MAX_PREFS = 100; +static constexpr u32 MAX_PREFS = 101; struct FileHeader { char magic[4]; // "APMP" diff --git a/src/systems/pref.h b/src/systems/pref.h index 326b017d..94fa9ca6 100644 --- a/src/systems/pref.h +++ b/src/systems/pref.h @@ -110,6 +110,7 @@ enum class Pref : u8 { DeathCounterDisplayOptions = 97, CountFirstStageDeaths = 98, MenuAcceleration = 99, + RightSideUIHide = 100, }; void init(); From fe9bdd0a66b734ccd48829f48bee4d3e9ebdebcd Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 25 Aug 2026 18:52:04 -0500 Subject: [PATCH 88/90] add hide_sprites.cpp/h --- src/mods/hide_sprites.cpp | 90 +++++++++++++++++++++++++++++++++++++++ src/mods/hide_sprites.h | 21 +++++++++ src/systems/main.cpp | 2 + 3 files changed, 113 insertions(+) create mode 100644 src/mods/hide_sprites.cpp create mode 100644 src/mods/hide_sprites.h diff --git a/src/mods/hide_sprites.cpp b/src/mods/hide_sprites.cpp new file mode 100644 index 00000000..ecfcc8c7 --- /dev/null +++ b/src/mods/hide_sprites.cpp @@ -0,0 +1,90 @@ +#include "hide_sprites.h" + +#include "utils/mode.h" + +namespace hide_sprites { + +static bool s_is_showing_banana_counter = false; +static bool s_is_showing_monkey_head = false; + +static void set_banana_counter_show_flag(bool show) { + s_is_showing_banana_counter = show; +} + +static void set_monkey_head_show_flag(bool show) { + s_is_showing_monkey_head = show; +} + +static void set_show_flags(bool show) { + set_banana_counter_show_flag(show); + set_monkey_head_show_flag(show); +} + +void hide_banana_counter() { + set_banana_counter_show_flag(false); +} + +void hide_monkey_head() { + set_monkey_head_show_flag(false); +} + +void hide_right_side_sprites() { + set_show_flags(false); +} + +bool right_side_sprites_normally_visible() { + return mode::is_on_stage(mkb::sub_mode) && !mode::is_story_retry_screen(mkb::sub_mode); +} + +static void set_banana_counter_visible(bool show) { + if (mkb::main_mode != mkb::MD_GAME) return; + + for (u32 i = 0; i < mkb::sprite_pool_info.upper_bound; i++) { + if (mkb::sprite_pool_info.status_list[i] == 0) continue; + + mkb::Sprite &sprite = mkb::sprites[i]; + if (sprite.bmp == 0x502 || sprite.tick_func == mkb::sprite_banana_icon_tick || + sprite.tick_func == mkb::sprite_banana_icon_shadow_tick || + sprite.tick_func == mkb::sprite_banana_count_tick) { + if ((show && sprite.depth < 0.f) || (!show && sprite.depth >= 0.f)) { + sprite.depth = -sprite.depth; + } + } + } +} + +static void set_monkey_head_visible(bool show) { + if (mkb::main_mode != mkb::MD_GAME) return; + + for (u32 i = 0; i < mkb::sprite_pool_info.upper_bound; i++) { + if (mkb::sprite_pool_info.status_list[i] == 0) continue; + + mkb::Sprite &sprite = mkb::sprites[i]; + if (sprite.bmp == 0x503 || sprite.tick_func == mkb::sprite_monkey_counter_tick || + sprite.disp_func == mkb::sprite_monkey_counter_icon_disp || + mkb::strcmp(sprite.text, ":") == 0 || + sprite.disp_func == mkb::sprite_hud_player_num_disp) { + if ((show && sprite.depth < 0.f) || (!show && sprite.depth >= 0.f)) { + sprite.depth = -sprite.depth; + } + } + } +} + +// Mods are only allowed to set the flags s_is_showing_banana_counter (and the corresponding monkey +// head one) to false, they are not allowed to directly use the static functions above +// The reason for doing this is that this means mods never have to worry about "undoing" a hiding +// action/setting these flags to true +// In other words, the relevant sprites are hidden when at least one mod decides that it should be +// hidden + +void tick() { + set_banana_counter_visible(s_is_showing_banana_counter); + set_monkey_head_visible(s_is_showing_monkey_head); + + // Important that these get set to true after the above calls + s_is_showing_banana_counter = true; + s_is_showing_monkey_head = true; +} + +} // namespace hide_sprites \ No newline at end of file diff --git a/src/mods/hide_sprites.h b/src/mods/hide_sprites.h new file mode 100644 index 00000000..79be969c --- /dev/null +++ b/src/mods/hide_sprites.h @@ -0,0 +1,21 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace hide_sprites { + +enum class RightSideUI { + HideAll, + HideMonkeyHead, + HideNone, +}; + +void hide_banana_counter(); +void hide_monkey_head(); +void hide_right_side_sprites(); + +bool right_side_sprites_normally_visible(); + +void tick(); + +} // namespace hide_sprites diff --git a/src/systems/main.cpp b/src/systems/main.cpp index 20109068..c784fa59 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -27,6 +27,7 @@ #include "mods/freecam.h" #include "mods/gotostory.h" #include "mods/hide.h" +#include "mods/hide_sprites.h" #include "mods/ilbattle.h" #include "mods/ilmark.h" #include "mods/inputdisp.h" @@ -99,6 +100,7 @@ static patch::Tramp s_process_inputs_tramp([]() { storyreset::tick(); deathcounter::tick(); storytimer::tick(); + hide_sprites::tick(); fallout::tick(); jump::tick(); // (edits physics preset) physics::tick(); // anything editing physics presets must run before physics::tick() From aca9527ba2e2f205468d67492643ecd05857f740 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Tue, 25 Aug 2026 18:53:44 -0500 Subject: [PATCH 89/90] update textinfo/inputdisp to use hide_sprites.cpp --- src/mods/inputdisp.cpp | 5 +-- src/systems/textinfo.cpp | 73 ++++++++++++++++++++++------------------ src/systems/textinfo.h | 2 -- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/mods/inputdisp.cpp b/src/mods/inputdisp.cpp index c46956c1..2aaefa0a 100644 --- a/src/mods/inputdisp.cpp +++ b/src/mods/inputdisp.cpp @@ -4,6 +4,7 @@ #include "mods/ballcolor.h" #include "mods/freecam.h" +#include "mods/hide_sprites.h" #include "systems/log.h" #include "systems/pad.h" #include "systems/pref.h" @@ -156,10 +157,10 @@ void tick() { textinfo::move_right_side_text_farther_right(false); } - if (input_disp_right_side) { + if (input_disp_right_side && pref::get(pref::Pref::InputDisp)) { // Any time we're drawing the input display on the right side, hide the monkey head/banana // counter sprites - textinfo::set_sprite_visible(false); + hide_sprites::hide_right_side_sprites(); } } diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index 89dd285e..b965740a 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -6,6 +6,7 @@ #include "../utils/draw.h" #include "../utils/macro_utils.h" #include "../utils/timerdisp.h" +#include "mods/hide_sprites.h" #include "systems/pad.h" namespace textinfo { @@ -37,7 +38,7 @@ static const Slot s_slot_list[]{ // Number of slots whose draw calls get stored in a buffer constexpr u16 BUFFER_SLOT_COUNT = 1; // Max number of strings that each buffer slot can accept -constexpr u16 BUFFER_SLOT_MAX_STRS = 8; +constexpr u16 BUFFER_SLOT_MAX_STRS = 9; static char s_row_buf[BUFFER_SLOT_COUNT][BUFFER_SLOT_MAX_STRS][16] = {}; // How many strings are actually being displayed in each buffer slot static u16 s_buffer_slot_display_count[BUFFER_SLOT_COUNT] = {}; @@ -48,6 +49,8 @@ static u16 s_active_row[LEN(s_slot_list)] = {}; static bool s_enable_drawing = true; static bool s_move_right_slot = false; +using HideOptions = hide_sprites::RightSideUI; + void init_text_data() { for (u16 j = 0; j < BUFFER_SLOT_COUNT; j++) { for (u16 k = 0; k < BUFFER_SLOT_MAX_STRS; k++) { @@ -105,12 +108,28 @@ s32 get_slot_x_alignment(Slot slot) { // Per-slot minimum row number u16 get_slot_min_row(Slot slot) { switch (slot) { - case Slot::Left: + case Slot::Left: { return 2; - case Slot::Right: - return 0; // 1 - default: + } + case Slot::Right: { + if (!hide_sprites::right_side_sprites_normally_visible()) { + // Monkey head/banana counter not visible, so don't shift text down + // This is important so that we don't shift our text rows on the menus/etc + return 0; + } + + HideOptions option = HideOptions(pref::get(pref::Pref::RightSideUIHide)); + if (s_move_right_slot && option == HideOptions::HideNone) { + return 6; + } else if (s_move_right_slot && option == HideOptions::HideMonkeyHead) { + return 2; + } else { + return 0; + } + } + default: { return 0; + } } } @@ -118,7 +137,7 @@ s32 get_modified_slot_y_pos(Slot slot, u16 row) { if (slot_is_buffer_slot(slot) && slot == Slot::Right) { u16 idx = get_buffer_slot_idx(slot); if (s_buffer_slot_display_count[idx] < 3) { - return timerdisp::row_number_to_vertical_pos(row + 2); + // return timerdisp::row_number_to_vertical_pos(row + 2); } } return timerdisp::row_number_to_vertical_pos(row); @@ -244,29 +263,8 @@ void draw_subtick_timer(Slot slot, // Mods have the ability to decide if we should move the right side slot text farther right // (example: input display) // So, we need to hide the right side sprites if we're drawing text there -void set_sprite_visible(bool visible) { - if (mkb::main_mode != mkb::MD_GAME) return; - - // Hide distracting score sprites under the input display - for (u32 i = 0; i < mkb::sprite_pool_info.upper_bound; i++) { - if (mkb::sprite_pool_info.status_list[i] == 0) continue; - - mkb::Sprite &sprite = mkb::sprites[i]; - if (sprite.bmp == 0x503 || sprite.tick_func == mkb::sprite_monkey_counter_tick || - sprite.disp_func == mkb::sprite_monkey_counter_icon_disp || sprite.bmp == 0x502 || - sprite.tick_func == mkb::sprite_banana_icon_tick || - sprite.tick_func == mkb::sprite_banana_icon_shadow_tick || - sprite.tick_func == mkb::sprite_banana_count_tick || - mkb::strcmp(sprite.text, ":") == 0 || - sprite.disp_func == mkb::sprite_hud_player_num_disp) { - if ((visible && sprite.depth < 0.f) || (!visible && sprite.depth >= 0.f)) { - sprite.depth = -sprite.depth; - } - } - } -} -bool should_hide_right_side_sprites() { +bool is_displaying_text_on_far_right() { u16 idx = get_buffer_slot_idx(Slot::Right); // When some mod decides we should be moving the right slot farther right + displaying at least // one row of text in the right slot @@ -276,6 +274,19 @@ bool should_hide_right_side_sprites() { return false; } +void handle_right_side_sprite_hiding() { + if (!is_displaying_text_on_far_right()) { + return; + } + + HideOptions option = HideOptions(pref::get(pref::Pref::RightSideUIHide)); + if (option == HideOptions::HideAll) { + hide_sprites::hide_right_side_sprites(); + } else if (option == HideOptions::HideMonkeyHead) { + hide_sprites::hide_monkey_head(); + } +} + void reset_active_rows() { for (u16 k = 0; k < LEN(s_slot_list); k++) { s_active_row[k] = get_slot_min_row(s_slot_list[k]); @@ -293,11 +304,7 @@ void init() { void disp() { draw_from_buf(); - if (should_hide_right_side_sprites()) { - set_sprite_visible(false); - } else { - set_sprite_visible(true); - } + handle_right_side_sprite_hiding(); // This runs after all possible textinfo::draw() calls reset_active_rows(); diff --git a/src/systems/textinfo.h b/src/systems/textinfo.h index f9e88012..adf58cb0 100644 --- a/src/systems/textinfo.h +++ b/src/systems/textinfo.h @@ -35,8 +35,6 @@ void draw_subtick_timer(Slot slot, u32 framesave, bool extra_precision); -void set_sprite_visible(bool visible); - void init(); void disp(); From 26dc9d498b35632b71e8e08ad5984977dfc63245 Mon Sep 17 00:00:00 2001 From: eddy0777 Date: Wed, 26 Aug 2026 09:14:46 -0500 Subject: [PATCH 90/90] simplify hide_sprites a bit --- src/mods/deathcounter.cpp | 3 +-- src/mods/hide_sprites.cpp | 20 ++++---------------- src/mods/hide_sprites.h | 2 +- src/systems/textinfo.cpp | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp index 122f4ac2..52da938e 100644 --- a/src/mods/deathcounter.cpp +++ b/src/mods/deathcounter.cpp @@ -2,7 +2,6 @@ #include "mkb/mkb.h" -#include "freecam.h" #include "storyreset.h" #include "systems/goal.h" #include "systems/pref.h" @@ -44,7 +43,7 @@ void increment_world_death_counter() { return; } s_world_death_count[mkb::scen_info.world] += 1; // death counter for the current world - s_can_incr_death_counter = false; + s_can_incr_death_counter = false; // so we only increment once per death } void reset_flag() { diff --git a/src/mods/hide_sprites.cpp b/src/mods/hide_sprites.cpp index ecfcc8c7..45237fd6 100644 --- a/src/mods/hide_sprites.cpp +++ b/src/mods/hide_sprites.cpp @@ -7,29 +7,17 @@ namespace hide_sprites { static bool s_is_showing_banana_counter = false; static bool s_is_showing_monkey_head = false; -static void set_banana_counter_show_flag(bool show) { - s_is_showing_banana_counter = show; -} - -static void set_monkey_head_show_flag(bool show) { - s_is_showing_monkey_head = show; -} - -static void set_show_flags(bool show) { - set_banana_counter_show_flag(show); - set_monkey_head_show_flag(show); -} - void hide_banana_counter() { - set_banana_counter_show_flag(false); + s_is_showing_banana_counter = false; } void hide_monkey_head() { - set_monkey_head_show_flag(false); + s_is_showing_monkey_head = false; } void hide_right_side_sprites() { - set_show_flags(false); + hide_banana_counter(); + hide_monkey_head(); } bool right_side_sprites_normally_visible() { diff --git a/src/mods/hide_sprites.h b/src/mods/hide_sprites.h index 79be969c..95b54c3f 100644 --- a/src/mods/hide_sprites.h +++ b/src/mods/hide_sprites.h @@ -4,7 +4,7 @@ namespace hide_sprites { -enum class RightSideUI { +enum class RightSideUIHideOptions { HideAll, HideMonkeyHead, HideNone, diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp index b965740a..2a03ca4a 100644 --- a/src/systems/textinfo.cpp +++ b/src/systems/textinfo.cpp @@ -49,7 +49,7 @@ static u16 s_active_row[LEN(s_slot_list)] = {}; static bool s_enable_drawing = true; static bool s_move_right_slot = false; -using HideOptions = hide_sprites::RightSideUI; +using HideOptions = hide_sprites::RightSideUIHideOptions; void init_text_data() { for (u16 j = 0; j < BUFFER_SLOT_COUNT; j++) {