From 04c5d50b3ed8b9fa9e5c23cd7c179c2e9a1c213a Mon Sep 17 00:00:00 2001 From: ItsHeckinPat <115201185+Patrick12115@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:01:39 -0500 Subject: [PATCH] Ported BtnSelector from 2ship (#6158) Allows the rebinding for Resetting the game, Debug Map Select, and Debug No-Clip. Also allows for Speed Modifiers to use the new button combos, instead of only the modifier buttons. --- soh/soh/Enhancements/ResetHotKey.cpp | 37 +++++++ .../controls/SohInputEditorWindow.cpp | 19 ++++ soh/soh/SohGui/Menu.cpp | 9 ++ soh/soh/SohGui/MenuTypes.h | 13 ++- soh/soh/SohGui/SohMenu.cpp | 3 + soh/soh/SohGui/SohMenuDevTools.cpp | 8 ++ soh/soh/SohGui/SohMenuSettings.cpp | 3 + soh/soh/SohGui/UIWidgets.cpp | 99 +++++++++++++++++++ soh/soh/SohGui/UIWidgets.hpp | 34 +++++++ soh/src/code/graph.c | 6 +- .../actors/ovl_player_actor/z_player.c | 35 ++++--- 11 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 soh/soh/Enhancements/ResetHotKey.cpp diff --git a/soh/soh/Enhancements/ResetHotKey.cpp b/soh/soh/Enhancements/ResetHotKey.cpp new file mode 100644 index 000000000..b57ae4ff2 --- /dev/null +++ b/soh/soh/Enhancements/ResetHotKey.cpp @@ -0,0 +1,37 @@ +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" +#include "functions.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "z64.h" +#include "overlays/gamestates/ovl_file_choose/file_choose.h" +} + +static constexpr int32_t CVAR_RESET_BTN_MASK_DEFAULT = BTN_CUSTOM_MODIFIER2; +#define CVAR_RESET_BTN_MASK_NAME "gSettings.ResetBtn" +#define CVAR_RESET_BTN_MASK_VALUE CVarGetInteger(CVAR_RESET_BTN_MASK_NAME, CVAR_RESET_BTN_MASK_DEFAULT) + +static void OnGameStateMainStartResetHotkey() { + const int32_t packed = CVarGetInteger("gSettings.ResetBtn", BTN_CUSTOM_MODIFIER2); + + const uint16_t mask = static_cast(packed & 0xFFFF); + + if (mask != 0 && CHECK_BTN_ANY(gGameState->input[0].press.button, mask) && + CHECK_BTN_ALL(gGameState->input[0].cur.button, mask)) { + + auto consoleWin = std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")); + + if (consoleWin) { + consoleWin->Dispatch("reset"); + } + } +} + +static void RegisterResetHotkey() { + COND_HOOK(OnGameStateMainStart, true, OnGameStateMainStartResetHotkey); +} + +static RegisterShipInitFunc initFuncResetHotkey(RegisterResetHotkey, { CVAR_RESET_BTN_MASK_NAME }); diff --git a/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp b/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp index 208833d61..f26500f70 100644 --- a/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp +++ b/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp @@ -1609,6 +1609,25 @@ void SohInputEditorWindow::DrawLinkTab() { .Color(THEME_COLOR) .Tooltip("Hold the assigned button to change the maximum walking or swimming speed")); if (CVarGetInteger(CVAR_SETTING("WalkModifier.Enabled"), 0)) { + CVarBtnSelector( + "Speed Modifier 1 Button Combo", CVAR_SETTING("WalkModifier.Mod1Btn"), + BtnSelectorOptions() + .DefaultValue(BTN_CUSTOM_MODIFIER1) + .Color(THEME_COLOR) + .Tooltip( + "Buttons that activate Speed Modifier 1.\n\n" + "If \"Toggle modifier instead of holding\" is off, hold this combo to apply the modifier.\n" + "If it is on, tap this combo to toggle the modifier on/off.")); + + CVarBtnSelector( + "Speed Modifier 2 Button Combo", CVAR_SETTING("WalkModifier.Mod2Btn"), + BtnSelectorOptions() + .DefaultValue(BTN_CUSTOM_MODIFIER2) + .Color(THEME_COLOR) + .Tooltip( + "Buttons that activate Speed Modifier 2.\n\n" + "If \"Toggle modifier instead of holding\" is off, hold this combo to apply the modifier.\n" + "If it is on, tap this combo to toggle the modifier on/off.")); UIWidgets::Spacer(5); Ship::GuiWindow::BeginGroupPanel("Speed Modifier", ImGui::GetContentRegionAvail()); CVarCheckbox("Toggle modifier instead of holding", CVAR_SETTING("WalkModifier.SpeedToggle"), diff --git a/soh/soh/SohGui/Menu.cpp b/soh/soh/SohGui/Menu.cpp index cddb68877..f786b53c4 100644 --- a/soh/soh/SohGui/Menu.cpp +++ b/soh/soh/SohGui/Menu.cpp @@ -460,6 +460,15 @@ void Menu::MenuDrawItem(WidgetInfo& widget, uint32_t width, UIWidgets::Colors me } } } break; + case WIDGET_CVAR_BTN_SELECTOR: { + auto options = std::static_pointer_cast(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarBtnSelector(widget.name.c_str(), widget.cVar, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; case WIDGET_BUTTON: { auto options = std::static_pointer_cast(widget.options); options->color = menuThemeIndex; diff --git a/soh/soh/SohGui/MenuTypes.h b/soh/soh/SohGui/MenuTypes.h index f8aaaa1e1..d14905a5b 100644 --- a/soh/soh/SohGui/MenuTypes.h +++ b/soh/soh/SohGui/MenuTypes.h @@ -37,6 +37,7 @@ typedef enum { WIDGET_CVAR_COMBOBOX, WIDGET_CVAR_SLIDER_INT, WIDGET_CVAR_SLIDER_FLOAT, + WIDGET_CVAR_BTN_SELECTOR, WIDGET_BUTTON, WIDGET_INPUT, WIDGET_CVAR_INPUT, @@ -71,10 +72,10 @@ typedef enum { // holds the widget values for a widget, contains all CVar types available from LUS. int32_t is used for boolean // evaluation using CVarVariant = std::variant; -using OptionsVariant = - std::variant; +using OptionsVariant = std::variant; // All the info needed for display and search of all widgets in the menu. // `name` is the label displayed, @@ -135,6 +136,10 @@ struct WidgetInfo { options = std::make_shared(std::get(options_)); break; + case WIDGET_CVAR_BTN_SELECTOR: + options = + std::make_shared(std::get(options_)); + break; case WIDGET_SLIDER_INT: case WIDGET_CVAR_SLIDER_INT: options = diff --git a/soh/soh/SohGui/SohMenu.cpp b/soh/soh/SohGui/SohMenu.cpp index 3a077920a..0e8ea5cf8 100644 --- a/soh/soh/SohGui/SohMenu.cpp +++ b/soh/soh/SohGui/SohMenu.cpp @@ -45,6 +45,9 @@ WidgetInfo& SohMenu::AddWidget(WidgetPath& pathInfo, std::string widgetName, Wid case WIDGET_CVAR_SLIDER_FLOAT: widget.options = std::make_shared(); break; + case WIDGET_CVAR_BTN_SELECTOR: + widget.options = std::make_shared(); + break; case WIDGET_SLIDER_INT: case WIDGET_CVAR_SLIDER_INT: widget.options = std::make_shared(); diff --git a/soh/soh/SohGui/SohMenuDevTools.cpp b/soh/soh/SohGui/SohMenuDevTools.cpp index de47b4e92..470d9c0dd 100644 --- a/soh/soh/SohGui/SohMenuDevTools.cpp +++ b/soh/soh/SohGui/SohMenuDevTools.cpp @@ -46,6 +46,14 @@ void SohMenu::AddMenuDevTools() { .Options( CheckboxOptions().Tooltip("Enables Debug Mode, allowing you to select maps with L + R + Z, noclip " "with L + D-pad Right, and open the debug menu with L on the pause screen.")); + AddWidget(path, "Map Select Button Combination:", WIDGET_CVAR_BTN_SELECTOR) + .CVar("gDeveloperTools.MapSelectBtn") + .Options(BtnSelectorOptions().DefaultValue(BTN_R | BTN_L | BTN_Z)) + .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0); }); + AddWidget(path, "No Clip Button Combination:", WIDGET_CVAR_BTN_SELECTOR) + .CVar("gDeveloperTools.NoClipBtn") + .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0); }) + .Options(BtnSelectorOptions().DefaultValue(BTN_L | BTN_DRIGHT)); AddWidget(path, "OoT Registry Editor", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_DEVELOPER_TOOLS("RegEditEnabled")) .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0); }) diff --git a/soh/soh/SohGui/SohMenuSettings.cpp b/soh/soh/SohGui/SohMenuSettings.cpp index 97b2bae91..63587f2da 100644 --- a/soh/soh/SohGui/SohMenuSettings.cpp +++ b/soh/soh/SohGui/SohMenuSettings.cpp @@ -178,6 +178,9 @@ void SohMenu::AddMenuSettings() { .RaceDisable(false) .Options(CheckboxOptions().Tooltip( "Search input box gets autofocus when visible. Does not affect using other widgets.")); + AddWidget(path, "Reset Button Combination:", WIDGET_CVAR_BTN_SELECTOR) + .CVar("gSettings.ResetBtn") + .Options(BtnSelectorOptions().DefaultValue(BTN_CUSTOM_MODIFIER2)); AddWidget(path, "Open App Files Folder", WIDGET_BUTTON) .RaceDisable(false) .Callback([](WidgetInfo& info) { diff --git a/soh/soh/SohGui/UIWidgets.cpp b/soh/soh/SohGui/UIWidgets.cpp index e338344b0..ee5c39057 100644 --- a/soh/soh/SohGui/UIWidgets.cpp +++ b/soh/soh/SohGui/UIWidgets.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "soh/OTRGlobals.h" namespace UIWidgets { @@ -1149,6 +1150,104 @@ void DrawFlagArray8Mask(const std::string& name, uint8_t& flags, Colors color) { } ImGui::PopID(); } + +std::map buttonMap = { + { "A", BTN_A }, + { "B", BTN_B }, + { "Z", BTN_Z }, + { "START", BTN_START }, + { "D-Up", BTN_DUP }, + { "D-Down", BTN_DDOWN }, + { "D-Left", BTN_DLEFT }, + { "D-Right", BTN_DRIGHT }, + { "L", BTN_L }, + { "R", BTN_R }, + { "C-Up", BTN_CUP }, + { "C-Down", BTN_CDOWN }, + { "C-Left", BTN_CLEFT }, + { "C-Right", BTN_CRIGHT }, + { "Modifier 1", BTN_CUSTOM_MODIFIER1 }, + { "Modifier 2", BTN_CUSTOM_MODIFIER2 }, +}; + +bool BtnSelector(const char* label, int32_t* value, const BtnSelectorOptions& options) { + bool dirty = false; + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", label); + ImGui::BeginDisabled(false); + PushStyleCombobox(options.color); + ImGui::BeginChild("ButtonCombo", ImVec2(0, ImGui::GetFrameHeightWithSpacing() + 14.0f), ImGuiChildFlags_None, + ImGuiWindowFlags_HorizontalScrollbar); + int32_t currentValue = *value; + int index = 0; + for (const auto& [buttonName, buttonMask] : buttonMap) { + if (currentValue & buttonMask) { + ImGui::PushID(buttonName.c_str()); + if (index++ > 0) { + ImGui::Text("+"); + ImGui::SameLine(); + } + if (UIWidgets::Button(buttonName.c_str(), UIWidgets::ButtonOptions() + .Tooltip("Remove this button from the combination") + .Color(UIWidgets::Colors::Gray) + .Size(UIWidgets::Sizes::Inline))) { + currentValue &= ~buttonMask; + dirty = true; + } + ImGui::PopID(); + ImGui::SameLine(); + } + } + if (UIWidgets::Button("+", UIWidgets::ButtonOptions({ { .tooltip = "Add a button to the combination" } }) + .Size(UIWidgets::Sizes::Inline) + .Color(options.color))) { + ImGui::OpenPopup("Add Button"); + } + if (ImGui::BeginPopup("Add Button")) { + UIWidgets::PushStyleMenuItem(); + for (const auto& [buttonName, buttonMask] : buttonMap) { + if (!(currentValue & buttonMask)) { + if (ImGui::MenuItem(buttonName.c_str())) { + currentValue |= buttonMask; + dirty = true; + } + } + } + UIWidgets::PopStyleMenuItem(); + ImGui::EndPopup(); + } + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_UNDO, + UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline).Color(options.color))) { + currentValue = options.defaultValue; + dirty = true; + } + ImGui::EndChild(); + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + ImGui::PopID(); + if (dirty) { + *value = currentValue; + } + return dirty; +} + +bool CVarBtnSelector(const char* label, const char* cvarName, const BtnSelectorOptions& options) { + bool dirty = false; + + int32_t value = CVarGetInteger(cvarName, options.defaultValue); + if (BtnSelector(label, &value, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + + return dirty; +} } // namespace UIWidgets ImVec4 GetRandomValue() { diff --git a/soh/soh/SohGui/UIWidgets.hpp b/soh/soh/SohGui/UIWidgets.hpp index 1504b38f3..2cbc1d5ff 100644 --- a/soh/soh/SohGui/UIWidgets.hpp +++ b/soh/soh/SohGui/UIWidgets.hpp @@ -502,6 +502,38 @@ struct FloatSliderOptions : WidgetOptions { } }; +struct BtnSelectorOptions : WidgetOptions { + s32 defaultValue = 0; + ComponentAlignments alignment = ComponentAlignments::Left; + LabelPositions labelPosition = LabelPositions::Above; + Colors color = Colors::Gray; + + BtnSelectorOptions& DefaultValue(int32_t defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + + BtnSelectorOptions& ComponentAlignment(ComponentAlignments alignment_) { + alignment = alignment_; + return *this; + } + + BtnSelectorOptions& LabelPosition(LabelPositions labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + + BtnSelectorOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + + BtnSelectorOptions& Color(Colors color_) { + color = color_; + return *this; + } +}; + struct RadioButtonsOptions : WidgetOptions { std::map buttonMap; int32_t defaultIndex = 0; @@ -1046,6 +1078,8 @@ void DrawFlagArray32(const std::string& name, uint32_t& flags, Colors color = Co void DrawFlagArray16(const std::string& name, uint16_t& flags, Colors color = Colors::LightBlue); void DrawFlagArray8(const std::string& name, uint8_t& flags, Colors color = Colors::LightBlue); void DrawFlagArray8Mask(const std::string& name, uint8_t& flags, Colors color = Colors::LightBlue); +bool BtnSelector(const char* label, int32_t* value, const BtnSelectorOptions& options); +bool CVarBtnSelector(const char* label, const char* cvarName, const BtnSelectorOptions& options); void InsertHelpHoverText(const std::string& text); void InsertHelpHoverText(const char* text); diff --git a/soh/src/code/graph.c b/soh/src/code/graph.c index 9694b7a4e..31dcb211e 100644 --- a/soh/src/code/graph.c +++ b/soh/src/code/graph.c @@ -408,9 +408,11 @@ void Graph_Update(GraphicsContext* gfxCtx, GameState* gameState) { sGraphUpdateTime = time; } + s32 mask = CVarGetInteger("gDeveloperTools.MapSelectBtn", BTN_Z | BTN_L | BTN_R); + if (CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0)) { - if (CHECK_BTN_ALL(gameState->input[0].press.button, BTN_Z) && - CHECK_BTN_ALL(gameState->input[0].cur.button, BTN_L | BTN_R)) { + if (CHECK_BTN_ANY(gameState->input[0].press.button, mask) && + CHECK_BTN_ALL(gameState->input[0].cur.button, mask)) { gSaveContext.gameMode = GAMEMODE_NORMAL; SET_NEXT_GAMESTATE(gameState, Select_Init, SelectContext); gameState->running = false; diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index b144771d7..6e8d3a467 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -7146,9 +7146,12 @@ void func_8083DFE0(Player* this, f32* arg1, s16* arg2) { maxSpeed *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping2"), 1.0f); } } else { - if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER1)) { + const s32 mod1Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod1Btn"), BTN_CUSTOM_MODIFIER1); + const s32 mod2Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod2Btn"), BTN_CUSTOM_MODIFIER2); + + if (mod1Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod1Mask)) { maxSpeed *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping1"), 1.0f); - } else if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER2)) { + } else if (mod2Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod2Mask)) { maxSpeed *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping2"), 1.0f); } } @@ -8892,9 +8895,12 @@ void Player_Action_80842180(Player* this, PlayState* play) { sp2C *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping2"), 1.0f); } } else { - if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER1)) { + const s32 mod1Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod1Btn"), BTN_CUSTOM_MODIFIER1); + const s32 mod2Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod2Btn"), BTN_CUSTOM_MODIFIER2); + + if (mod1Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod1Mask)) { sp2C *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping1"), 1.0f); - } else if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER2)) { + } else if (mod2Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod2Mask)) { sp2C *= CVarGetFloat(CVAR_SETTING("WalkModifier.Mapping2"), 1.0f); } } @@ -12411,10 +12417,15 @@ void Player_Update(Actor* thisx, PlayState* play) { if (CVarGetInteger(CVAR_SETTING("WalkModifier.Enabled"), 0) && CVarGetInteger(CVAR_SETTING("WalkModifier.SpeedToggle"), 0)) { - if (CHECK_BTN_ALL(sControlInput->press.button, BTN_CUSTOM_MODIFIER1)) { + const s32 mod1Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod1Btn"), BTN_CUSTOM_MODIFIER1); + const s32 mod2Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod2Btn"), BTN_CUSTOM_MODIFIER2); + + if (mod1Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod1Mask) && + CHECK_BTN_ANY(sControlInput->press.button, mod1Mask)) { gWalkSpeedToggle1 = !gWalkSpeedToggle1; } - if (CHECK_BTN_ALL(sControlInput->press.button, BTN_CUSTOM_MODIFIER2)) { + if (mod2Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod2Mask) && + CHECK_BTN_ANY(sControlInput->press.button, mod2Mask)) { gWalkSpeedToggle2 = !gWalkSpeedToggle2; } } @@ -12876,9 +12887,12 @@ void func_8084AEEC(Player* this, f32* arg1, f32 arg2, s16 arg3) { // sControlInput is NULL to prevent inputs while surfacing after obtaining an underwater item so we want to // ignore it for that case } else if (sControlInput != NULL) { - if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER1)) { + const s32 mod1Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod1Btn"), BTN_CUSTOM_MODIFIER1); + const s32 mod2Mask = CVarGetInteger(CVAR_SETTING("WalkModifier.Mod2Btn"), BTN_CUSTOM_MODIFIER2); + + if (mod1Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod1Mask)) { swimMod *= CVarGetFloat(CVAR_SETTING("WalkModifier.SwimMapping1"), 1.0f); - } else if (CHECK_BTN_ALL(sControlInput->cur.button, BTN_CUSTOM_MODIFIER2)) { + } else if (mod2Mask != 0 && CHECK_BTN_ALL(sControlInput->cur.button, mod2Mask)) { swimMod *= CVarGetFloat(CVAR_SETTING("WalkModifier.SwimMapping2"), 1.0f); } } @@ -15104,11 +15118,10 @@ void Player_Action_8084FBF4(Player* this, PlayState* play) { */ s32 Player_UpdateNoclip(Player* this, PlayState* play) { sControlInput = &play->state.input[0]; + s32 mask = CVarGetInteger("gDeveloperTools.NoClipBtn", BTN_L | BTN_DRIGHT); if (CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0) && - ((CHECK_BTN_ALL(sControlInput->cur.button, BTN_A | BTN_L | BTN_R) && - CHECK_BTN_ALL(sControlInput->press.button, BTN_B)) || - (CHECK_BTN_ALL(sControlInput->cur.button, BTN_L) && CHECK_BTN_ALL(sControlInput->press.button, BTN_DRIGHT)))) { + (CHECK_BTN_ALL(sControlInput->cur.button, mask) && CHECK_BTN_ANY(sControlInput->press.button, mask))) { sNoclipEnabled ^= 1;