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/mods/cmseg.cpp b/src/mods/cmseg.cpp index 656859cb..de9374f4 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" @@ -35,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", @@ -415,7 +419,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); @@ -424,7 +428,8 @@ void disp() { color = draw::GOLD; else color = draw::WHITE; - timerdisp::draw_timer(static_cast(s_seg_time), "SEG:", 0, color, false); + textinfo::draw_timer(Slot::Right, color, "SEG:", static_cast(s_seg_time), + Format::AlwaysLeadNonHours); } } diff --git a/src/mods/deathcounter.cpp b/src/mods/deathcounter.cpp new file mode 100644 index 00000000..52da938e --- /dev/null +++ b/src/mods/deathcounter.cpp @@ -0,0 +1,159 @@ +#include "deathcounter.h" + +#include "mkb/mkb.h" + +#include "storyreset.h" +#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" + +namespace deathcounter { + +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; + +using Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + +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; // so we only increment once per death +} + +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(); +} + +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() { + 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; + } +} + +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; + // 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; +} + +bool should_count_as_savestate_death() { + return 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() || 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(); + } + + 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() { + 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; + default: + // Unreachable + 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(); + } else { + return false; + } +} + +void disp() { + if (should_not_display_counter_at_all()) { + return; + } + + if (should_display_death_counter()) { + // Technically not a timer, but we can still use this function without specifying any + // special formatting + textinfo::draw_timer(Slot::Left, draw::WHITE, "Deaths:", get_total_death_count(), + Format::Unformatted); + } +} + +} // namespace deathcounter diff --git a/src/mods/deathcounter.h b/src/mods/deathcounter.h new file mode 100644 index 00000000..1070cdb5 --- /dev/null +++ b/src/mods/deathcounter.h @@ -0,0 +1,14 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace deathcounter { + +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/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/gotostory.cpp b/src/mods/gotostory.cpp index e00b6cc6..f1038e26 100644 --- a/src/mods/gotostory.cpp +++ b/src/mods/gotostory.cpp @@ -4,14 +4,12 @@ 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 diff --git a/src/mods/hide_sprites.cpp b/src/mods/hide_sprites.cpp new file mode 100644 index 00000000..45237fd6 --- /dev/null +++ b/src/mods/hide_sprites.cpp @@ -0,0 +1,78 @@ +#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; + +void hide_banana_counter() { + s_is_showing_banana_counter = false; +} + +void hide_monkey_head() { + s_is_showing_monkey_head = false; +} + +void hide_right_side_sprites() { + hide_banana_counter(); + hide_monkey_head(); +} + +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..95b54c3f --- /dev/null +++ b/src/mods/hide_sprites.h @@ -0,0 +1,21 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace hide_sprites { + +enum class RightSideUIHideOptions { + 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/mods/ilbattle.cpp b/src/mods/ilbattle.cpp index 05c1573c..6c995f7b 100644 --- a/src/mods/ilbattle.cpp +++ b/src/mods/ilbattle.cpp @@ -1,13 +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/textinfo.h" #include "utils/draw.h" -#include "systems/savest.h" -#include "utils/patch.h" +#include "utils/macro_utils.h" #include "utils/relutil.h" #include "utils/timerdisp.h" @@ -34,12 +35,14 @@ 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; -// 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 +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[] = { + 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; @@ -62,96 +65,100 @@ static u32 s_rainbow = 0; static bool s_time_buzzer = false; static bool s_score_buzzer = false; +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(Slot::Left, pos_x, color, incr_row, text); +} + +void draw_buzzer_beater_row(GXColor color) { + 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]); + } + } } -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; +void draw_battle_text(GXColor color, char *text) { + draw_battle_text_main(TEXT_POS_X, color, false, text); +} - u32 best_seconds = s_best_frames / SECOND_FRAMES; - u32 best_centiseconds = (s_best_frames % SECOND_FRAMES) * 100 / 60; +void draw_battle_breakdown_text(GXColor color, char *text) { + draw_battle_text_main(TEXT_POS_X + 3 * CWIDTH, color, false, text); +} - u32 best_score_seconds = s_best_score_frames / SECOND_FRAMES; - u32 best_score_centiseconds = (s_best_score_frames % SECOND_FRAMES) * 100 / 60; +// 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 +// 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_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_v(Slot::Left, NUM_POS_X, color, true, format, args); + va_end(args); +} +static void battle_display(GXColor text_color) { 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 - 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); - } + 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:"); + 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); + draw_battle_text_right_aligned(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_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:"); + 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); + draw_battle_text_right_aligned(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_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); + draw_battle_text_right_aligned(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); - 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_breakdown_text(text_color, "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); } } 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:"); + draw_battle_num(text_color, s_attempts, Format::Unformatted); } if (pref::get(pref::Pref::IlBattleBuzzerOld) && @@ -377,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: { @@ -385,14 +392,14 @@ 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(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; } @@ -400,7 +407,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); } diff --git a/src/mods/ilmark.cpp b/src/mods/ilmark.cpp index e5b73f5b..30b0c9d5 100644 --- a/src/mods/ilmark.cpp +++ b/src/mods/ilmark.cpp @@ -4,17 +4,24 @@ #include "mods/freecam.h" #include "mods/physics.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; @@ -39,28 +46,45 @@ void tick() { s_valid_run = false; } - if (savest::get_last_action() == savest::Action::Load) { + if (savest::get_last_action() != savest::Action::None) { s_valid_run = false; } } +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 we've finished the run + return goal::is_run_complete(); + case StoryIlMarkSettings::AlwaysInStory: + return true; + default: + // Unreachable + return false; + } +} + 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; } diff --git a/src/mods/inputdisp.cpp b/src/mods/inputdisp.cpp index b4ea9d3f..2aaefa0a 100644 --- a/src/mods/inputdisp.cpp +++ b/src/mods/inputdisp.cpp @@ -4,12 +4,15 @@ #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" +#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 +137,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 +147,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 && pref::get(pref::Pref::InputDisp)) { + // Any time we're drawing the input display on the right side, hide the monkey head/banana + // counter sprites + hide_sprites::hide_right_side_sprites(); + } } static bool get_notch_pos(const pad::StickState &stick_inputs, Vec2d *out_pos) { @@ -344,15 +337,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/mods/iw.cpp b/src/mods/iw.cpp index feaf2589..c8c19bf3 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" @@ -26,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; @@ -121,10 +125,10 @@ 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; - timerdisp::draw_timer(static_cast(s_iw_time), "IW:", 0, draw::WHITE, false); + textinfo::draw_timer(Slot::Right, draw::WHITE, "IW:", static_cast(s_iw_time), + Format::AlwaysLeadNonHours); } } // namespace iw diff --git a/src/mods/menu_accel.cpp b/src/mods/menu_accel.cpp new file mode 100644 index 00000000..9841cfa8 --- /dev/null +++ b/src/mods/menu_accel.cpp @@ -0,0 +1,53 @@ +#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 { + +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 + 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..57876bd7 --- /dev/null +++ b/src/mods/menu_accel.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../mkb/mkb.h" + +namespace menu_accel { + +void tick(); + +} // namespace menu_accel 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/storyreset.cpp b/src/mods/storyreset.cpp new file mode 100644 index 00000000..69e5980d --- /dev/null +++ b/src/mods/storyreset.cpp @@ -0,0 +1,180 @@ +#include "storyreset.h" + +#include "gotostory.h" +#include "systems/goal.h" +#include "systems/pref.h" +#include "utils/draw.h" +#include "utils/mode.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; +} + +// 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; +} + +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() || + in_challenge_mode() || should_reset_completed_run(); +} + +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(); + } +} + +// --- stuff for handling displaying the run reset message --- + +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 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(); +} + +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 (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; + } + + 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"); +} + +} // 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..c6ec1106 --- /dev/null +++ b/src/mods/storyreset.h @@ -0,0 +1,30 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace storyreset { + +// 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, +}; + +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 diff --git a/src/mods/storytimer.cpp b/src/mods/storytimer.cpp new file mode 100644 index 00000000..4518a16c --- /dev/null +++ b/src/mods/storytimer.cpp @@ -0,0 +1,243 @@ +#include "storytimer.h" + +#include "mkb/mkb.h" + +#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" +#include "utils/patch.h" +#include "utils/timerdisp.h" + +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 Slot = textinfo::Slot; +using Format = timerdisp::TimeFormat; + +// --- 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 + u16 clamped_idx = MIN(world_idx, WORLD_COUNT - 1); + return s_world_timer[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_world_timer[k].full_world; + } + return prev_world_sum + s_world_timer[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_world_timer[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_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[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_world_timer[k].full_world += 1; + s_world_timer[k].segment = s_world_timer[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_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_world_timer[k].segment = s_world_timer[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_world_timer[active_world_idx].full_world += 1; + s_world_timer[active_world_idx].segment = s_world_timer[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 --- + +// Special case of textinfo::draw_timer() useful for us +void draw_timer(char *prefix, u32 frames, Format 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 +// 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); + } + + 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: + return true; + case TimerOptions::BetweenWorlds: + 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; + default: + // Unreachable + return false; + } +} + +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 + // 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() { + if (should_display_timer(TimerType::Fullgame)) { + draw_timer("Time:", get_loadless_time(), Format::AlwaysLeadNonHours); + } + + u16 world_idx = get_current_segment_idx(); // index of the current world + if (should_display_timer(TimerType::Segment)) { + draw_timer("Seg:", s_world_timer[world_idx].segment, Format::AlwaysLeadNonHours); + } +} + +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(); + } else { + return false; + } +} + +void disp() { + if (should_not_display_timer_at_all()) { + return; + } + + draw_timers(); +} + +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() { + 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..ac6e0cc0 --- /dev/null +++ b/src/mods/storytimer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace storytimer { + +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 +}; + +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(); + +} // namespace storytimer \ No newline at end of file diff --git a/src/mods/timer.cpp b/src/mods/timer.cpp index a7c475a5..fe952037 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" @@ -15,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(); } @@ -56,14 +60,12 @@ 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); + 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()) { - timerdisp::draw_timer(s_pause_timer, "PAU:", row++, draw::WHITE, true); + if (pref::get(pref::Pref::TimerShowPause)) { + textinfo::draw_timer(Slot::Right, draw::WHITE, "PAU:", s_pause_timer, Format::SecondsOnly); } switch (mkb::sub_mode) { @@ -78,13 +80,15 @@ 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); + 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()) { - timerdisp::draw_percentage(framesave, "FSV:", row++, draw::WHITE); + 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/mods/validate.cpp b/src/mods/validate.cpp index e36343d9..12069cf5 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; + // 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 new file mode 100644 index 00000000..1eb99750 --- /dev/null +++ b/src/systems/goal.cpp @@ -0,0 +1,178 @@ +#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 + +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) { + bool orig_result = + s_goal_tramp.chain(ball, out_stage_goal_idx, out_itemgroup_id, out_goal_flags); + + if (orig_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 orig_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 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::get_last_action() != savest::Action::None || 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 init() { + 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..6eb1d1fb --- /dev/null +++ b/src/systems/goal.h @@ -0,0 +1,15 @@ +#pragma once + +#include "mkb/mkb.h" + +namespace goal { + +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..c784fa59 100644 --- a/src/systems/main.cpp +++ b/src/systems/main.cpp @@ -4,12 +4,14 @@ #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/textinfo.h" #include "systems/version.h" #include "utils/draw.h" #include "utils/patch.h" @@ -19,22 +21,28 @@ #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" #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" #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" #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" #include "mods/unlock.h" @@ -89,12 +97,17 @@ static patch::Tramp s_process_inputs_tramp([]() { 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(); + hide_sprites::tick(); fallout::tick(); jump::tick(); // (edits physics preset) physics::tick(); // anything editing physics presets must run before physics::tick() inputdisp::tick(); gotostory::tick(); cmseg::tick(); + menu_accel::tick(); banans::tick(); marathon::tick(); ballcolor::tick(); @@ -125,16 +138,24 @@ static patch::Tramp s_draw_debug_text_tramp([]() { } draw::predraw(); - timer::disp(); + // timer::disp(); + inputdisp::disp(); iw::disp(); + deathcounter::disp(); + storytimer::disp(); + storybreakdown::disp(); ilbattle::disp(); cmseg::disp(); - inputdisp::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(); }); static patch::Tramp s_smd_game_ready_init_tramp([]() { @@ -160,6 +181,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) { @@ -180,9 +202,11 @@ void init() { pref::init(); unlock::init(); draw::init(); + textinfo::init(); physics::init(); iw::init(); savest::init(); + goal::init(); timer::init(); inputdisp::init(); cmseg::init(); diff --git a/src/systems/menu_defn.cpp b/src/systems/menu_defn.cpp index 5ca94c84..8bd0c6f5 100644 --- a/src/systems/menu_defn.cpp +++ b/src/systems/menu_defn.cpp @@ -1024,6 +1024,96 @@ static Widget s_cm_seg_widgets[] = { }, }; +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 = + { + // Applies to both loadless timer and deathcounter (they share the same pref) + .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, + }, + }, + { + .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[] = { {.type = WidgetType::Header, .header = {"Realtime Timers"}}, { @@ -1061,7 +1151,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 +1168,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 +1483,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 +1543,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 +1580,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 +1605,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, }, }, @@ -1531,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)}, @@ -1540,6 +1662,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)}, diff --git a/src/systems/pref.cpp b/src/systems/pref.cpp index 2890a34f..8746efee 100644 --- a/src/systems/pref.cpp +++ b/src/systems/pref.cpp @@ -99,6 +99,14 @@ 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, + Pref::RightSideUIHide, }; struct DefaultPref { @@ -131,13 +139,15 @@ static const DefaultPref DEFAULT_PREFS[] = { {Pref::CustomPhysicsDisp, 1}, {Pref::SavestateClearAllBind, 255}, {Pref::InputDispGradientEnd, 100}, + {Pref::HideRunResetMessage, 1}, + {Pref::CountFirstStageDeaths, 1}, }; // // 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 9566ad28..94fa9ca6 100644 --- a/src/systems/pref.h +++ b/src/systems/pref.h @@ -103,6 +103,14 @@ enum class Pref : u8 { InputDispGradientStart = 90, InputDispGradientEnd = 91, RgbFormat = 92, + FullgameTimerOptions = 93, + SegmentTimerOptions = 94, + ShowRunBreakdown = 95, + HideRunResetMessage = 96, + DeathCounterDisplayOptions = 97, + CountFirstStageDeaths = 98, + MenuAcceleration = 99, + RightSideUIHide = 100, }; void init(); diff --git a/src/systems/textinfo.cpp b/src/systems/textinfo.cpp new file mode 100644 index 00000000..2a03ca4a --- /dev/null +++ b/src/systems/textinfo.cpp @@ -0,0 +1,313 @@ +#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" +#include "mods/hide_sprites.h" +#include "systems/pad.h" + +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; + GXColor color; + char *text; // always a ptr to s_row_buf +}; + +static const Slot s_slot_list[]{ + Slot::Left, + Slot::Right, +}; + +// 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 = 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] = {}; +// 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 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::RightSideUIHideOptions; + +void init_text_data() { + 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; + 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; +} + +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) { + 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; +} + +// 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: + return 102; + case Slot::Right: + if (!s_move_right_slot) { + return 378 + 4 * draw::DEBUG_CHAR_WIDTH; + } else { + return 540; + } + default: + return 0; + } +} + +// Per-slot minimum row number +u16 get_slot_min_row(Slot slot) { + switch (slot) { + case Slot::Left: { + return 2; + } + 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; + } + } +} + +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); +} + +// 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; + } + + for (u16 k = 0; k < LEN(s_slot_list); k++) { + if (s_slot_list[k] == slot) { + u16 row = s_active_row[k]; + if (slot_is_buffer_slot(slot)) { + // Store to buffer so we can draw and move the text later + u16 idx = get_buffer_slot_idx(slot); // idx = buffer slot index + u16 text_idx = s_buffer_slot_display_count[idx]; + + // 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_buffer_slot_display_count[idx] += 1; + } else { + // If not storing this slot's draw calls to the buffer, draw instantly + 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 (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][j].pos_x, pos_y, s_text_data[idx][j].color, + s_text_data[idx][j].text); + } + } + } +} + +void draw(Slot slot, s32 pos_x, GXColor color, bool incr_row, char *format, ...) { + va_list args; + va_start(args, format); + draw_v(slot, pos_x, color, incr_row, format, args); + va_end(args); +} + +void draw_aligned(Slot slot, GXColor color, char *format, ...) { + va_list args; + va_start(args, format); + draw_v(slot, get_slot_x_alignment(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_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(slot, x, color, true, "%s%s", prefix, time_buf); +} + +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, + 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(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); +} + +// 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 + +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 + if (s_move_right_slot && s_buffer_slot_display_count[idx] != 0) { + return true; + } + 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]); + } + for (u16 k = 0; k < BUFFER_SLOT_COUNT; k++) { + s_buffer_slot_display_count[k] = 0; + } +} + +void init() { + init_text_data(); +} + +// textinfo's disp() runs after all other disp() functions that use textinfo +void disp() { + draw_from_buf(); + + handle_right_side_sprite_hiding(); + + // This runs after all possible textinfo::draw() calls + reset_active_rows(); +} + +} // 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..adf58cb0 --- /dev/null +++ b/src/systems/textinfo.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "mkb/mkb.h" +#include "utils/timerdisp.h" + +namespace textinfo { + +enum class Slot { + Unused, + Left, + Right, +}; + +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); + +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, ...); + +void draw_timer_main(Slot slot, + GXColor color, + 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, + s32 frames, + u32 framesave, + bool extra_precision); + +void init(); +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 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(); diff --git a/src/utils/mode.cpp b/src/utils/mode.cpp new file mode 100644 index 00000000..c8d1dd4d --- /dev/null +++ b/src/utils/mode.cpp @@ -0,0 +1,314 @@ +#include "mode.h" + +// #include "../internal/relutil.h" +#include "mkb/mkb.h" +#include "mkb/mkb2_ghidra.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); +} + +// --- 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) { + 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_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_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; +} + +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..22775ab0 --- /dev/null +++ b/src/utils/mode.h @@ -0,0 +1,79 @@ +#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); + +// 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); +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_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(); + +// 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 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 diff --git a/src/utils/timerdisp.cpp b/src/utils/timerdisp.cpp index f96e1f8e..4b804f87 100644 --- a/src/utils/timerdisp.cpp +++ b/src/utils/timerdisp.cpp @@ -1,88 +1,105 @@ #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; -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(s32 row_num) { + return Y + ROW_HEIGHT * row_num; +} +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(char *buffer, u32 frames, TimeFormat format) { + TimeComp time = get_time_components(frames); + + switch (format) { + case TimeFormat::SecondsOnly: { + u32 total_seconds = frames / SECOND_FRAMES; + mkb::sprintf(buffer, "%d.%02d", total_seconds, time.centiseconds); + break; + } + case TimeFormat::HoursAlways: { + mkb::sprintf(buffer, "%d:%02d:%02d.%02d", time.hours, time.minutes, time.seconds, + time.centiseconds); + break; + } + case TimeFormat::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 TimeFormat::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 TimeFormat::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; + } + case TimeFormat::Unformatted: { + mkb::sprintf(buffer, "%d", frames); + } + } +} + +void format_signed_time(char *buffer, s32 frames, TimeFormat format) { 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 centiseconds = (frames % SECOND_FRAMES) * 100 / 60; - - s32 y = Y + row * 16; - - 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, - 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); - } 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); - } + char time_buf[16] = {}; + format_time(time_buf, frames, format); - mkb::reset_ui_widescreen_scale_mtx(); + mkb::sprintf(buffer, "%s%s", sign, time_buf); } -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); - +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 ? "" : "-"; - 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 - u32 extra = (((frames % SECOND_FRAMES) * 100 + framesave) * 10) / 6; // 4 digit + 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 - s32 y = Y + row * 16; - - 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); + mkb::sprintf(buffer, "%s%02d.%04d", sign, total_seconds, extra); } else { - draw::debug_text(X + 48, y, color, "%s%02d.%03d", sign, total_seconds, milliseconds); + mkb::sprintf(buffer, "%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 = Y + row * 16; - 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 9f03968e..e3d5a5c4 100644 --- a/src/utils/timerdisp.h +++ b/src/utils/timerdisp.h @@ -4,14 +4,28 @@ namespace timerdisp { -void draw_timer(s32 frames, const char *prefix, u32 row, GXColor color, bool show_minutes); -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); +// TimeComp = "Time Components" +struct TimeComp { + u32 hours; + u32 minutes; + u32 seconds; + u32 centiseconds; +}; + +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 + 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 + Unformatted +}; + +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(s32 row_num); } // namespace timerdisp