diff --git a/mm/2s2h/BenGui/BenGui.cpp b/mm/2s2h/BenGui/BenGui.cpp index b41e537e0..dfc57bf34 100644 --- a/mm/2s2h/BenGui/BenGui.cpp +++ b/mm/2s2h/BenGui/BenGui.cpp @@ -27,7 +27,6 @@ std::shared_ptr mBenMenuBar; std::shared_ptr mConsoleWindow; std::shared_ptr mStatsWindow; -std::shared_ptr mInputEditorWindow; std::shared_ptr mGfxDebuggerWindow; std::shared_ptr mSaveEditorWindow; @@ -35,6 +34,8 @@ std::shared_ptr mHudEditorWindow; std::shared_ptr mActorViewerWindow; std::shared_ptr mCollisionViewerWindow; std::shared_ptr mEventLogWindow; +std::shared_ptr mBenMenu; +std::shared_ptr mBenInputEditorWindow; void SetupGuiElements() { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); @@ -47,7 +48,7 @@ void SetupGuiElements() { mBenMenuBar = std::make_shared(CVAR_MENU_BAR_OPEN, CVarGetInteger(CVAR_MENU_BAR_OPEN, 0)); gui->SetMenuBar(std::reinterpret_pointer_cast(mBenMenuBar)); - if (gui->GetMenuBar() && !gui->GetMenuBar()->IsVisible()) { + if (!gui->GetMenuBar() && !CVarGetInteger("gSettings.DisableMenuShortcutNotify", 0)) { #if defined(__SWITCH__) || defined(__WIIU__) gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Press - to access enhancements menu"); #else @@ -55,6 +56,9 @@ void SetupGuiElements() { #endif } + mBenMenu = std::make_shared("gWindows.Menu", "Settings Menu"); + gui->SetMenu(mBenMenu); + mStatsWindow = gui->GetGuiWindow("Stats"); if (mStatsWindow == nullptr) { SPDLOG_ERROR("Could not find stats window"); @@ -65,37 +69,38 @@ void SetupGuiElements() { SPDLOG_ERROR("Could not find console window"); } - mInputEditorWindow = gui->GetGuiWindow("Input Editor"); - if (mInputEditorWindow == nullptr) { - SPDLOG_ERROR("Could not find input editor window"); - } - mGfxDebuggerWindow = gui->GetGuiWindow("GfxDebuggerWindow"); if (mGfxDebuggerWindow == nullptr) { SPDLOG_ERROR("Could not find input GfxDebuggerWindow"); } - mSaveEditorWindow = std::make_shared("gWindows.SaveEditor", "Save Editor"); + mBenInputEditorWindow = std::make_shared("gWindows.BenInputEditor", "2S2H Input Editor"); + gui->AddGuiWindow(mBenInputEditorWindow); + + mSaveEditorWindow = std::make_shared("gWindows.SaveEditor", "Save Editor", ImVec2(480, 600)); gui->AddGuiWindow(mSaveEditorWindow); - mHudEditorWindow = std::make_shared("gWindows.HudEditor", "Hud Editor"); + mHudEditorWindow = std::make_shared("gWindows.HudEditor", "HUD Editor", ImVec2(480, 600)); gui->AddGuiWindow(mHudEditorWindow); - mActorViewerWindow = std::make_shared("gWindows.ActorViewer", "Actor Viewer"); + mActorViewerWindow = std::make_shared("gWindows.ActorViewer", "Actor Viewer", ImVec2(520, 600)); gui->AddGuiWindow(mActorViewerWindow); - mCollisionViewerWindow = std::make_shared("gWindows.CollisionViewer", "Collision Viewer"); + mCollisionViewerWindow = + std::make_shared("gWindows.CollisionViewer", "Collision Viewer", ImVec2(390, 475)); gui->AddGuiWindow(mCollisionViewerWindow); - mEventLogWindow = std::make_shared("gWindows.EventLog", "Event Log"); + mEventLogWindow = std::make_shared("gWindows.EventLog", "Event Log", ImVec2(520, 600)); gui->AddGuiWindow(mEventLogWindow); + gui->SetPadBtnTogglesMenu(); } void Destroy() { mBenMenuBar = nullptr; + mBenMenu = nullptr; mStatsWindow = nullptr; mConsoleWindow = nullptr; - mInputEditorWindow = nullptr; + mBenInputEditorWindow = nullptr; mGfxDebuggerWindow = nullptr; mCollisionViewerWindow = nullptr; mEventLogWindow = nullptr; diff --git a/mm/2s2h/BenGui/BenGui.hpp b/mm/2s2h/BenGui/BenGui.hpp index e5017c217..b292b9c3d 100644 --- a/mm/2s2h/BenGui/BenGui.hpp +++ b/mm/2s2h/BenGui/BenGui.hpp @@ -7,6 +7,8 @@ #include "DeveloperTools/ActorViewer.h" #include "DeveloperTools/CollisionViewer.h" #include "DeveloperTools/EventLog.h" +#include "BenInputEditorWindow.h" +#include "Menu.h" namespace BenGui { void SetupHooks(); diff --git a/mm/2s2h/BenGui/BenInputEditorWindow.cpp b/mm/2s2h/BenGui/BenInputEditorWindow.cpp new file mode 100644 index 000000000..8e3b4334e --- /dev/null +++ b/mm/2s2h/BenGui/BenInputEditorWindow.cpp @@ -0,0 +1,1671 @@ +#include "BenInputEditorWindow.h" +#include "Context.h" +#include "Gui.h" +#include "utils/StringHelper.h" +#include "public/bridge/consolevariablebridge.h" +#include "controller/controldevice/controller/mapping/sdl/SDLAxisDirectionToButtonMapping.h" + +#define SCALE_IMGUI_SIZE(value) ((value / 13.0f) * ImGui::GetFontSize()) + +BenInputEditorWindow::~BenInputEditorWindow() { + SPDLOG_TRACE("destruct input editor window"); +} + +void BenInputEditorWindow::InitElement() { + mGameInputBlockTimer = INT32_MAX; + mMappingInputBlockTimer = INT32_MAX; + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest = nullptr; + mInputEditorPopupOpen = false; + + mButtonsBitmasks = { BTN_A, BTN_B, BTN_START, BTN_L, BTN_R, BTN_Z, BTN_CUP, BTN_CDOWN, BTN_CLEFT, BTN_CRIGHT }; + mDpadBitmasks = { BTN_DUP, BTN_DDOWN, BTN_DLEFT, BTN_DRIGHT }; + + mDeviceIndexVisiblity.clear(); + mDeviceIndexVisiblity[Ship::ShipDeviceIndex::Keyboard] = true; + mDeviceIndexVisiblity[Ship::ShipDeviceIndex::Blue] = true; + for (auto index = 1; index < Ship::ShipDeviceIndex::Max; index++) { + mDeviceIndexVisiblity[static_cast(index)] = false; + } +} + +#define INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID 95237929 +void BenInputEditorWindow::UpdateElement() { + if (mRumbleTimer != INT32_MAX) { + mRumbleTimer--; + if (mRumbleMappingToTest != nullptr) { + mRumbleMappingToTest->StartRumble(); + } + if (mRumbleTimer <= 0) { + if (mRumbleMappingToTest != nullptr) { + mRumbleMappingToTest->StopRumble(); + } + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest = nullptr; + } + } + + if (mInputEditorPopupOpen && ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId)) { + Ship::Context::GetInstance()->GetControlDeck()->BlockGameInput(INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID); + + // continue to block input for a third of a second after getting the mapping + mGameInputBlockTimer = ImGui::GetIO().Framerate / 3; + + if (mMappingInputBlockTimer != INT32_MAX) { + mMappingInputBlockTimer--; + if (mMappingInputBlockTimer <= 0) { + mMappingInputBlockTimer = INT32_MAX; + } + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->BlockImGuiGamepadNavigation(); + } else { + if (mGameInputBlockTimer != INT32_MAX) { + mGameInputBlockTimer--; + if (mGameInputBlockTimer <= 0) { + Ship::Context::GetInstance()->GetControlDeck()->UnblockGameInput( + INPUT_EDITOR_WINDOW_GAME_INPUT_BLOCK_ID); + mGameInputBlockTimer = INT32_MAX; + } + } + + if (Ship::Context::GetInstance()->GetWindow()->GetGui()->ImGuiGamepadNavigationEnabled()) { + mMappingInputBlockTimer = ImGui::GetIO().Framerate / 3; + } else { + mMappingInputBlockTimer = INT32_MAX; + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->UnblockImGuiGamepadNavigation(); + } +} + +void BenInputEditorWindow::DrawAnalogPreview(const char* label, ImVec2 stick, float deadzone, bool gyro) { + ImGui::BeginChild(label, ImVec2(gyro ? SCALE_IMGUI_SIZE(78) : SCALE_IMGUI_SIZE(96), SCALE_IMGUI_SIZE(85)), false); + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + gyro ? SCALE_IMGUI_SIZE(10) : SCALE_IMGUI_SIZE(18), + ImGui::GetCursorPos().y + gyro ? SCALE_IMGUI_SIZE(10) : 0)); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const ImVec2 cursorScreenPosition = ImGui::GetCursorScreenPos(); + + // Draw the border box + float borderSquareLeft = cursorScreenPosition.x + SCALE_IMGUI_SIZE(2.0f); + float borderSquareTop = cursorScreenPosition.y + SCALE_IMGUI_SIZE(2.0f); + float borderSquareSize = SCALE_IMGUI_SIZE(65.0f); + drawList->AddRect(ImVec2(borderSquareLeft, borderSquareTop), + ImVec2(borderSquareLeft + borderSquareSize, borderSquareTop + borderSquareSize), + ImColor(100, 100, 100, 255), 0.0f, 0, 1.5f); + + // Draw the gate background + float cardinalRadius = SCALE_IMGUI_SIZE(22.5f); + float diagonalRadius = SCALE_IMGUI_SIZE(22.5f * (69.0f / 85.0f)); + + ImVec2 joystickCenterpoint = ImVec2(cursorScreenPosition.x + cardinalRadius + SCALE_IMGUI_SIZE(12), + cursorScreenPosition.y + cardinalRadius + SCALE_IMGUI_SIZE(11)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImVec2(joystickCenterpoint.x, joystickCenterpoint.y + cardinalRadius), + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImVec2(joystickCenterpoint.x + cardinalRadius, joystickCenterpoint.y), + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x + diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImVec2(joystickCenterpoint.x, joystickCenterpoint.y - cardinalRadius), + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImColor(130, 130, 130, 255)); + drawList->AddQuadFilled(joystickCenterpoint, + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y - diagonalRadius), + ImVec2(joystickCenterpoint.x - cardinalRadius, joystickCenterpoint.y), + ImVec2(joystickCenterpoint.x - diagonalRadius, joystickCenterpoint.y + diagonalRadius), + ImColor(130, 130, 130, 255)); + + // Draw the joystick position indicator + ImVec2 joystickIndicatorDistanceFromCenter = ImVec2(0, 0); + if ((stick.x * stick.x + stick.y * stick.y) > (deadzone * deadzone)) { + joystickIndicatorDistanceFromCenter = + ImVec2((stick.x * (cardinalRadius / 85.0f)), -(stick.y * (cardinalRadius / 85.0f))); + } + float indicatorRadius = SCALE_IMGUI_SIZE(5.0f); + drawList->AddCircleFilled(ImVec2(joystickCenterpoint.x + joystickIndicatorDistanceFromCenter.x, + joystickCenterpoint.y + joystickIndicatorDistanceFromCenter.y), + indicatorRadius, ImColor(34, 51, 76, 255), 7); + + if (!gyro) { + ImGui::SetCursorPos( + ImVec2(ImGui::GetCursorPos().x - SCALE_IMGUI_SIZE(8), ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(72))); + ImGui::Text("X:%3d, Y:%3d", static_cast(stick.x), static_cast(stick.y)); + } + ImGui::EndChild(); +} + +#define CHIP_COLOR_N64_GREY ImVec4(0.4f, 0.4f, 0.4f, 1.0f) +#define CHIP_COLOR_N64_BLUE ImVec4(0.176f, 0.176f, 0.5f, 1.0f) +#define CHIP_COLOR_N64_GREEN ImVec4(0.0f, 0.294f, 0.0f, 1.0f) +#define CHIP_COLOR_N64_YELLOW ImVec4(0.5f, 0.314f, 0.0f, 1.0f) +#define CHIP_COLOR_N64_RED ImVec4(0.392f, 0.0f, 0.0f, 1.0f) + +#define BUTTON_COLOR_KEYBOARD_BEIGE ImVec4(0.651f, 0.482f, 0.357f, 0.5f) +#define BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED ImVec4(0.651f, 0.482f, 0.357f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_BLUE ImVec4(0.0f, 0.255f, 0.976f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_BLUE_HOVERED ImVec4(0.0f, 0.255f, 0.976f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_RED ImVec4(0.976f, 0.0f, 0.094f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_RED_HOVERED ImVec4(0.976f, 0.0f, 0.094f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_ORANGE ImVec4(0.976f, 0.376f, 0.0f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_ORANGE_HOVERED ImVec4(0.976f, 0.376f, 0.0f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_GREEN ImVec4(0.0f, 0.5f, 0.0f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_GREEN_HOVERED ImVec4(0.0f, 0.5f, 0.0f, 1.0f) + +#define BUTTON_COLOR_GAMEPAD_PURPLE ImVec4(0.431f, 0.369f, 0.706f, 0.5f) +#define BUTTON_COLOR_GAMEPAD_PURPLE_HOVERED ImVec4(0.431f, 0.369f, 0.706f, 1.0f) + +void BenInputEditorWindow::GetButtonColorsForShipDeviceIndex(Ship::ShipDeviceIndex lusIndex, ImVec4& buttonColor, + ImVec4& buttonHoveredColor) { + switch (lusIndex) { + case Ship::ShipDeviceIndex::Keyboard: + buttonColor = BUTTON_COLOR_KEYBOARD_BEIGE; + buttonHoveredColor = BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED; + break; + case Ship::ShipDeviceIndex::Blue: + buttonColor = BUTTON_COLOR_GAMEPAD_BLUE; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_BLUE_HOVERED; + break; + case Ship::ShipDeviceIndex::Red: + buttonColor = BUTTON_COLOR_GAMEPAD_RED; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_RED_HOVERED; + break; + case Ship::ShipDeviceIndex::Orange: + buttonColor = BUTTON_COLOR_GAMEPAD_ORANGE; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_ORANGE_HOVERED; + break; + case Ship::ShipDeviceIndex::Green: + buttonColor = BUTTON_COLOR_GAMEPAD_GREEN; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_GREEN_HOVERED; + break; + default: + buttonColor = BUTTON_COLOR_GAMEPAD_PURPLE; + buttonHoveredColor = BUTTON_COLOR_GAMEPAD_PURPLE_HOVERED; + } +} + +void BenInputEditorWindow::DrawInputChip(const char* buttonName, ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::BeginDisabled(); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::Button(buttonName, ImVec2(SCALE_IMGUI_SIZE(50.0f), 0)); + ImGui::PopStyleColor(); + ImGui::EndDisabled(); +} + +void BenInputEditorWindow::DrawButtonLineAddMappingButton(uint8_t port, CONTROLLERBUTTONS_T bitmask) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addButtonMappingPopup##%d-%d", port, bitmask); + if (ImGui::Button(StringHelper::Sprintf("%s###addButtonMappingButton%d-%d", ICON_FA_PLUS, port, bitmask).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + }; + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto add mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + // todo: figure out why optional params (using id = "" in the definition) wasn't working + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->AddOrEditButtonMappingFromRawPress(bitmask, "")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawButtonLineEditMappingButton(uint8_t port, CONTROLLERBUTTONS_T bitmask, std::string id) { + auto mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->GetButtonMappingById(id); + if (mapping == nullptr) { + return; + } + if (!mDeviceIndexVisiblity[mapping->GetShipDeviceIndex()]) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + std::string icon = ""; + switch (mapping->GetMappingType()) { + case MAPPING_TYPE_GAMEPAD: + icon = ICON_FA_GAMEPAD; + break; + case MAPPING_TYPE_KEYBOARD: + icon = ICON_FA_KEYBOARD_O; + break; + case MAPPING_TYPE_UNKNOWN: + icon = ICON_FA_BUG; + break; + } + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + auto physicalInputDisplayName = + StringHelper::Sprintf("%s %s", icon.c_str(), mapping->GetPhysicalInputName().c_str()); + GetButtonColorsForShipDeviceIndex(mapping->GetShipDeviceIndex(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + auto popupId = StringHelper::Sprintf("editButtonMappingPopup##%s", id.c_str()); + if (ImGui::Button( + StringHelper::Sprintf("%s###editButtonMappingButton%s", physicalInputDisplayName.c_str(), id.c_str()) + .c_str(), + ImVec2(ImGui::CalcTextSize(physicalInputDisplayName.c_str()).x + SCALE_IMGUI_SIZE(12.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip(mapping->GetPhysicalDeviceName().c_str()); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto edit mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->AddOrEditButtonMappingFromRawPress(bitmask, id)) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + + auto sdlAxisDirectionToButtonMapping = std::dynamic_pointer_cast(mapping); + auto indexMapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetDeviceIndexMappingFromShipDeviceIndex(mapping->GetShipDeviceIndex()); + auto sdlIndexMapping = std::dynamic_pointer_cast(indexMapping); + + if (sdlIndexMapping != nullptr && sdlAxisDirectionToButtonMapping != nullptr) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(mapping->GetShipDeviceIndex(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("editAxisThresholdPopup##%s", id.c_str()); + if (ImGui::Button(StringHelper::Sprintf("%s###editAxisThresholdButton%s", ICON_FA_COG, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_COG).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip("Edit axis threshold"); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Axis Threshold\n\nThe extent to which the joystick\nmust be moved or the trigger\npressed to " + "initiate the assigned\nbutton action.\n\n"); + + if (sdlAxisDirectionToButtonMapping->AxisIsStick()) { + ImGui::Text("Stick axis threshold:"); + + int32_t stickAxisThreshold = sdlIndexMapping->GetStickAxisThresholdPercentage(); + if (stickAxisThreshold == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Stick Axis Threshold%s", id.c_str()).c_str())) { + sdlIndexMapping->SetStickAxisThresholdPercentage(stickAxisThreshold - 1); + sdlIndexMapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (stickAxisThreshold == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Stick Axis Threshold%s", id.c_str()).c_str(), + &stickAxisThreshold, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + sdlIndexMapping->SetStickAxisThresholdPercentage(stickAxisThreshold); + sdlIndexMapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (stickAxisThreshold == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Stick Axis Threshold%s", id.c_str()).c_str())) { + sdlIndexMapping->SetStickAxisThresholdPercentage(stickAxisThreshold + 1); + sdlIndexMapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (stickAxisThreshold == 100) { + ImGui::EndDisabled(); + } + } + + if (sdlAxisDirectionToButtonMapping->AxisIsTrigger()) { + ImGui::Text("Trigger axis threshold:"); + + int32_t triggerAxisThreshold = sdlIndexMapping->GetTriggerAxisThresholdPercentage(); + if (triggerAxisThreshold == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Trigger Axis Threshold%s", id.c_str()).c_str())) { + sdlIndexMapping->SetTriggerAxisThresholdPercentage(triggerAxisThreshold - 1); + sdlIndexMapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (triggerAxisThreshold == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Trigger Axis Threshold%s", id.c_str()).c_str(), + &triggerAxisThreshold, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + sdlIndexMapping->SetTriggerAxisThresholdPercentage(triggerAxisThreshold); + sdlIndexMapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (triggerAxisThreshold == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Trigger Axis Threshold%s", id.c_str()).c_str())) { + sdlIndexMapping->SetTriggerAxisThresholdPercentage(triggerAxisThreshold + 1); + sdlIndexMapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (triggerAxisThreshold == 100) { + ImGui::EndDisabled(); + } + } + + if (ImGui::Button("Close")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + } + + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeButtonMappingButton%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_TIMES).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetButton(bitmask) + ->ClearButtonMapping(id); + }; + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + ImGui::SameLine(0, SCALE_IMGUI_SIZE(4.0f)); +} + +void BenInputEditorWindow::DrawButtonLine(const char* buttonName, uint8_t port, CONTROLLERBUTTONS_T bitmask, + ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::NewLine(); + ImGui::SameLine(SCALE_IMGUI_SIZE(32.0f)); + DrawInputChip(buttonName, color); + ImGui::SameLine(SCALE_IMGUI_SIZE(86.0f)); + for (auto id : mBitmaskToMappingIds[port][bitmask]) { + DrawButtonLineEditMappingButton(port, bitmask, id); + } + DrawButtonLineAddMappingButton(port, bitmask); +} + +void BenInputEditorWindow::DrawStickDirectionLineAddMappingButton(uint8_t port, uint8_t stick, + Ship::Direction direction) { + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addStickDirectionMappingPopup##%d-%d-%d", port, stick, direction); + if (ImGui::Button( + StringHelper::Sprintf("%s###addStickDirectionMappingButton%d-%d-%d", ICON_FA_PLUS, port, stick, direction) + .c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + }; + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto add mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + if (stick == Ship::LEFT) { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, "")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + } else { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, "")) { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawStickDirectionLineEditMappingButton(uint8_t port, uint8_t stick, + Ship::Direction direction, std::string id) { + std::shared_ptr mapping = nullptr; + if (stick == Ship::LEFT) { + mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->GetAxisDirectionMappingById(direction, id); + } else { + mapping = Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->GetAxisDirectionMappingById(direction, id); + } + + if (mapping == nullptr) { + return; + } + if (!mDeviceIndexVisiblity[mapping->GetShipDeviceIndex()]) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); + std::string icon = ""; + switch (mapping->GetMappingType()) { + case MAPPING_TYPE_GAMEPAD: + icon = ICON_FA_GAMEPAD; + break; + case MAPPING_TYPE_KEYBOARD: + icon = ICON_FA_KEYBOARD_O; + break; + case MAPPING_TYPE_UNKNOWN: + icon = ICON_FA_BUG; + break; + } + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + auto physicalInputDisplayName = + StringHelper::Sprintf("%s %s", icon.c_str(), mapping->GetPhysicalInputName().c_str()); + GetButtonColorsForShipDeviceIndex(mapping->GetShipDeviceIndex(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + auto popupId = StringHelper::Sprintf("editStickDirectionMappingPopup##%s", id.c_str()); + if (ImGui::Button( + StringHelper::Sprintf("%s###editStickDirectionMappingButton%s", physicalInputDisplayName.c_str(), + id.c_str()) + .c_str(), + ImVec2(ImGui::CalcTextSize(physicalInputDisplayName.c_str()).x + SCALE_IMGUI_SIZE(12.0f), 0.0f))) { + ImGui::OpenPopup(popupId.c_str()); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) { + ImGui::SetTooltip(mapping->GetPhysicalDeviceName().c_str()); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button,\nmove any axis,\nor press any key\nto edit mapping"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (stick == Ship::LEFT) { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, id)) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + } else { + if (mMappingInputBlockTimer == INT32_MAX && + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->AddOrEditAxisDirectionMappingFromRawPress(direction, id)) { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(); + ImGui::SameLine(0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button( + StringHelper::Sprintf("%s###removeStickDirectionMappingButton%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(ImGui::CalcTextSize(ICON_FA_TIMES).x + SCALE_IMGUI_SIZE(10.0f), 0.0f))) { + if (stick == Ship::LEFT) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLeftStick() + ->ClearAxisDirectionMapping(direction, id); + } else { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRightStick() + ->ClearAxisDirectionMapping(direction, id); + } + }; + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::SameLine(0, SCALE_IMGUI_SIZE(4.0f)); +} + +void BenInputEditorWindow::DrawStickDirectionLine(const char* axisDirectionName, uint8_t port, uint8_t stick, + Ship::Direction direction, ImVec4 color = CHIP_COLOR_N64_GREY) { + ImGui::NewLine(); + ImGui::SameLine(); + ImGui::BeginDisabled(); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::Button(axisDirectionName, ImVec2(SCALE_IMGUI_SIZE(26.0f), 0)); + ImGui::PopStyleColor(); + ImGui::EndDisabled(); + ImGui::SameLine(0.0f, SCALE_IMGUI_SIZE(4.0f)); + for (auto id : mStickDirectionToMappingIds[port][stick][direction]) { + DrawStickDirectionLineEditMappingButton(port, stick, direction, id); + } + DrawStickDirectionLineAddMappingButton(port, stick, direction); +} + +void BenInputEditorWindow::DrawStickSection(uint8_t port, uint8_t stick, int32_t id, + ImVec4 color = CHIP_COLOR_N64_GREY) { + static int8_t sX, sY; + std::shared_ptr controllerStick = nullptr; + if (stick == Ship::LEFT) { + controllerStick = Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLeftStick(); + } else { + controllerStick = Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRightStick(); + } + controllerStick->Process(sX, sY); + DrawAnalogPreview(StringHelper::Sprintf("##AnalogPreview%d", id).c_str(), ImVec2(sX, sY)); + + ImGui::SameLine(); + ImGui::BeginGroup(); + DrawStickDirectionLine(ICON_FA_ARROW_UP, port, stick, Ship::UP, color); + DrawStickDirectionLine(ICON_FA_ARROW_DOWN, port, stick, Ship::DOWN, color); + DrawStickDirectionLine(ICON_FA_ARROW_LEFT, port, stick, Ship::LEFT, color); + DrawStickDirectionLine(ICON_FA_ARROW_RIGHT, port, stick, Ship::RIGHT, color); + ImGui::EndGroup(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode(StringHelper::Sprintf("Analog Stick Options##%d", id).c_str())) { + ImGui::Text("Sensitivity:"); + + int32_t sensitivityPercentage = controllerStick->GetSensitivityPercentage(); + if (sensitivityPercentage == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Sensitivity%d", id).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage - 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Sensitivity%d", id).c_str(), &sensitivityPercentage, 0, 200, + "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetSensitivity(sensitivityPercentage); + } + ImGui::SameLine(0.0f, 0.0f); + if (sensitivityPercentage == 200) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Sensitivity%d", id).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage + 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 200) { + ImGui::EndDisabled(); + } + if (!controllerStick->SensitivityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickSensitivity%d", id).c_str())) { + controllerStick->ResetSensitivityToDefault(); + } + } + + ImGui::Text("Deadzone:"); + + int32_t deadzonePercentage = controllerStick->GetDeadzonePercentage(); + if (deadzonePercentage == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Deadzone%d", id).c_str())) { + controllerStick->SetDeadzone(deadzonePercentage - 1); + } + ImGui::PopButtonRepeat(); + if (deadzonePercentage == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Deadzone%d", id).c_str(), &deadzonePercentage, 0, 100, "%d%%", + ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetDeadzone(deadzonePercentage); + } + ImGui::SameLine(0.0f, 0.0f); + if (deadzonePercentage == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Deadzone%d", id).c_str())) { + controllerStick->SetDeadzone(deadzonePercentage + 1); + } + ImGui::PopButtonRepeat(); + if (deadzonePercentage == 100) { + ImGui::EndDisabled(); + } + if (!controllerStick->DeadzoneIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickDeadzone%d", id).c_str())) { + controllerStick->ResetDeadzoneToDefault(); + } + } + + ImGui::Text("Notch Snap Angle:"); + int32_t notchSnapAngle = controllerStick->GetNotchSnapAngle(); + if (notchSnapAngle == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##NotchProximityThreshold%d", id).c_str())) { + controllerStick->SetNotchSnapAngle(notchSnapAngle - 1); + } + ImGui::PopButtonRepeat(); + if (notchSnapAngle == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##NotchProximityThreshold%d", id).c_str(), ¬chSnapAngle, 0, 45, + "%d°", ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetNotchSnapAngle(notchSnapAngle); + } + ImGui::SameLine(0.0f, 0.0f); + if (notchSnapAngle == 45) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##NotchProximityThreshold%d", id).c_str())) { + controllerStick->SetNotchSnapAngle(notchSnapAngle + 1); + } + ImGui::PopButtonRepeat(); + if (notchSnapAngle == 45) { + ImGui::EndDisabled(); + } + if (!controllerStick->NotchSnapAngleIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickSnap%d", id).c_str())) { + controllerStick->ResetNotchSnapAngleToDefault(); + } + } + + ImGui::TreePop(); + } +} + +void BenInputEditorWindow::UpdateBitmaskToMappingIds(uint8_t port) { + // todo: do we need this now that ControllerButton exists? + + for (auto [bitmask, button] : + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetAllButtons()) { + for (auto [id, mapping] : button->GetAllButtonMappings()) { + // using a vector here instead of a set because i want newly added mappings + // to go to the end of the list instead of autosorting + if (std::find(mBitmaskToMappingIds[port][bitmask].begin(), mBitmaskToMappingIds[port][bitmask].end(), id) == + mBitmaskToMappingIds[port][bitmask].end()) { + mBitmaskToMappingIds[port][bitmask].push_back(id); + } + } + } +} + +void BenInputEditorWindow::UpdateStickDirectionToMappingIds(uint8_t port) { + // todo: do we need this? + for (auto stick : + { std::make_pair>( + Ship::LEFT, Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLeftStick()), + std::make_pair>( + Ship::RIGHT, + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRightStick()) }) { + for (auto direction : { Ship::LEFT, Ship::RIGHT, Ship::UP, Ship::DOWN }) { + for (auto [id, mapping] : stick.second->GetAllAxisDirectionMappingByDirection(direction)) { + // using a vector here instead of a set because i want newly added mappings + // to go to the end of the list instead of autosorting + if (std::find(mStickDirectionToMappingIds[port][stick.first][direction].begin(), + mStickDirectionToMappingIds[port][stick.first][direction].end(), + id) == mStickDirectionToMappingIds[port][stick.first][direction].end()) { + mStickDirectionToMappingIds[port][stick.first][direction].push_back(id); + } + } + } + } +} + +void BenInputEditorWindow::DrawRemoveRumbleMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeRumbleMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetRumble()->ClearRumbleMapping(id); + } + ImGui::PopStyleVar(); +} + +void BenInputEditorWindow::DrawAddRumbleMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addRumbleMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addRumbleMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add rumble device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRumble() + ->AddRumbleMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +bool BenInputEditorWindow::TestingRumble() { + return mRumbleTimer != INT32_MAX; +} + +void BenInputEditorWindow::DrawRumbleSection(uint8_t port) { + for (auto [id, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetRumble() + ->GetAllRumbleMappings()) { + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(mapping->GetShipDeviceIndex(), buttonColor, buttonHoveredColor); + // begin hackaround https://github.com/ocornut/imgui/issues/282#issuecomment-123763192 + // spaces to have background color for text in a tree node + std::string spaces = ""; + for (size_t i = 0; i < mapping->GetPhysicalDeviceName().length(); i++) { + spaces += " "; + } + auto open = ImGui::TreeNode(StringHelper::Sprintf("%s###Rumble%s", spaces.c_str(), id.c_str()).c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(SCALE_IMGUI_SIZE(30.0f)); + // end hackaround + + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::Button(mapping->GetPhysicalDeviceName().c_str()); + ImGui::PopStyleColor(); + ImGui::PopItemFlag(); + + DrawRemoveRumbleMappingButton(port, id); + ImGui::SameLine(); + if (ImGui::Button( + StringHelper::Sprintf("%s###rumbleTestButton%s", TestingRumble() ? "Stop" : "Test", id.c_str()) + .c_str())) { + if (mRumbleTimer != INT32_MAX) { + mRumbleTimer = INT32_MAX; + mRumbleMappingToTest->StopRumble(); + mRumbleMappingToTest = nullptr; + } else { + mRumbleTimer = ImGui::GetIO().Framerate; + mRumbleMappingToTest = mapping; + } + } + if (open) { + ImGui::Text("Small Motor Intensity:"); + + int32_t smallMotorIntensity = mapping->GetHighFrequencyIntensityPercentage(); + if (smallMotorIntensity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Small Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (smallMotorIntensity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Small Motor Intensity%s", id.c_str()).c_str(), + &smallMotorIntensity, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (smallMotorIntensity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Small Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetHighFrequencyIntensity(smallMotorIntensity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (smallMotorIntensity == 100) { + ImGui::EndDisabled(); + } + if (!mapping->HighFrequencyIntensityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetHighFrequencyIntensity%s", id.c_str()) + .c_str())) { + mapping->ResetHighFrequencyIntensityToDefault(); + } + } + + ImGui::Text("Large Motor Intensity:"); + + int32_t largeMotorIntensity = mapping->GetLowFrequencyIntensityPercentage(); + if (largeMotorIntensity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Large Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (largeMotorIntensity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Large Motor Intensity%s", id.c_str()).c_str(), + &largeMotorIntensity, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (largeMotorIntensity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Large Motor Intensity%s", id.c_str()).c_str())) { + mapping->SetLowFrequencyIntensity(largeMotorIntensity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (largeMotorIntensity == 100) { + ImGui::EndDisabled(); + } + if (!mapping->LowFrequencyIntensityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button( + StringHelper::Sprintf("Reset to Default###resetLowFrequencyIntensity%s", id.c_str()).c_str())) { + mapping->ResetLowFrequencyIntensityToDefault(); + } + } + ImGui::Dummy(ImVec2(0, SCALE_IMGUI_SIZE(20))); + + ImGui::TreePop(); + } + } + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add rumble device"); + DrawAddRumbleMappingButton(port); +} + +void BenInputEditorWindow::DrawRemoveLEDMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeLEDMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLED()->ClearLEDMapping(id); + } + ImGui::PopStyleVar(); +} + +void BenInputEditorWindow::DrawAddLEDMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addLEDMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addLEDMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add LED device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetLED() + ->AddLEDMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawLEDSection(uint8_t port) { + for (auto [id, mapping] : + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetLED()->GetAllLEDMappings()) { + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + auto open = ImGui::TreeNode( + StringHelper::Sprintf("%s##LED%s", mapping->GetPhysicalDeviceName().c_str(), id.c_str()).c_str()); + DrawRemoveLEDMappingButton(port, id); + if (open) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("LED Color:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(80.0f)); + int32_t colorSource = mapping->GetColorSource(); + if (ImGui::Combo(StringHelper::Sprintf("###ledColorSource%s", mapping->GetLEDMappingId().c_str()).c_str(), + &colorSource, "Off\0Set\0Game\0\0")) { + mapping->SetColorSource(colorSource); + }; + if (mapping->GetColorSource() == LED_COLOR_SOURCE_SET) { + ImGui::SameLine(); + ImVec4 color = { mapping->GetSavedColor().r / 255.0f, mapping->GetSavedColor().g / 255.0f, + mapping->GetSavedColor().b / 255.0f, 1.0f }; + if (ImGui::ColorEdit3( + StringHelper::Sprintf("###ledSavedColor%s", mapping->GetLEDMappingId().c_str()).c_str(), + (float*)&color, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + mapping->SetSavedColor( + Color_RGB8({ static_cast(color.x * 255.0), static_cast(color.y * 255.0), + static_cast(color.z * 255.0) })); + } + } + ImGui::TreePop(); + } + } + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add LED device"); + DrawAddLEDMappingButton(port); +} + +void BenInputEditorWindow::DrawRemoveGyroMappingButton(uint8_t port, std::string id) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(StringHelper::Sprintf("%s###removeGyroMapping%s", ICON_FA_TIMES, id.c_str()).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetGyro()->ClearGyroMapping(); + } + ImGui::PopStyleVar(); +} + +void BenInputEditorWindow::DrawAddGyroMappingButton(uint8_t port) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + auto popupId = StringHelper::Sprintf("addGyroMappingPopup##%d", port); + if (ImGui::Button(StringHelper::Sprintf("%s###addGyroMapping%d", ICON_FA_PLUS, port).c_str(), + ImVec2(SCALE_IMGUI_SIZE(20.0f), SCALE_IMGUI_SIZE(20.0f)))) { + ImGui::OpenPopup(popupId.c_str()); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginPopup(popupId.c_str())) { + mInputEditorPopupOpen = true; + ImGui::Text("Press any button\nor move any axis\nto add gyro device"); + if (ImGui::Button("Cancel")) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + + if (mMappingInputBlockTimer == INT32_MAX && Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(port) + ->GetGyro() + ->SetGyroMappingFromRawPress()) { + mInputEditorPopupOpen = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawGyroSection(uint8_t port) { + auto mapping = + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(port)->GetGyro()->GetGyroMapping(); + if (mapping != nullptr) { + auto id = mapping->GetGyroMappingId(); + ImGui::AlignTextToFramePadding(); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + ImGui::BulletText(mapping->GetPhysicalDeviceName().c_str()); + DrawRemoveGyroMappingButton(port, id); + + static float sPitch, sYaw = 0.0f; + mapping->UpdatePad(sPitch, sYaw); + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y - SCALE_IMGUI_SIZE(8))); + // to find a reasonable scaling factor gyro values + // I tried to find the maximum value reported by shaking + // a PS5 controller as hard as I could without worrying about breaking it + // the max I found for both pitch and yaw was ~21 + // the preview window expects values in an n64 analog stick range (-85 to 85) + // so I decided to multiply these by 85/21 + DrawAnalogPreview(StringHelper::Sprintf("###GyroPreview%s", id.c_str()).c_str(), + ImVec2(sYaw * (85.0f / 21.0f), sPitch * (85.0f / 21.0f)), 0.0f, true); + ImGui::SameLine(); + ImGui::SetCursorPos( + ImVec2(ImGui::GetCursorPos().x + SCALE_IMGUI_SIZE(8), ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(8))); + + ImGui::BeginGroup(); + ImGui::Text("Sensitivity:"); + + int32_t sensitivity = mapping->GetSensitivityPercent(); + if (sensitivity == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##GyroSensitivity%s", id.c_str()).c_str())) { + mapping->SetSensitivity(sensitivity - 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (sensitivity == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##GyroSensitivity%s", id.c_str()).c_str(), &sensitivity, 0, 100, + "%d%%", ImGuiSliderFlags_AlwaysClamp)) { + mapping->SetSensitivity(sensitivity); + mapping->SaveToConfig(); + } + ImGui::SameLine(0.0f, 0.0f); + if (sensitivity == 100) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##GyroSensitivity%s", id.c_str()).c_str())) { + mapping->SetSensitivity(sensitivity + 1); + mapping->SaveToConfig(); + } + ImGui::PopButtonRepeat(); + if (sensitivity == 100) { + ImGui::EndDisabled(); + } + + if (!mapping->SensitivityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetGyroSensitivity%s", id.c_str()).c_str())) { + mapping->ResetSensitivityToDefault(); + } + } + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y + SCALE_IMGUI_SIZE(8))); + if (ImGui::Button("Recalibrate")) { + mapping->Recalibrate(); + mapping->SaveToConfig(); + } + ImGui::EndGroup(); + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x, ImGui::GetCursorPos().y - SCALE_IMGUI_SIZE(8))); + } else { + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Add gyro device"); + DrawAddGyroMappingButton(port); + } +} + +void BenInputEditorWindow::DrawButtonDeviceIcons(uint8_t portIndex, std::set bitmasks) { + std::set allLusDeviceIndices; + allLusDeviceIndices.insert(Ship::ShipDeviceIndex::Keyboard); + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappingsFromConfig()) { + allLusDeviceIndices.insert(lusIndex); + } + + std::vector> lusDeviceIndiciesWithMappings; + for (auto lusIndex : allLusDeviceIndices) { + for (auto [bitmask, button] : + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->GetAllButtons()) { + if (!bitmasks.contains(bitmask)) { + continue; + } + + if (button->HasMappingsForShipDeviceIndex(lusIndex)) { + for (auto [id, mapping] : button->GetAllButtonMappings()) { + if (mapping->GetShipDeviceIndex() == lusIndex) { + lusDeviceIndiciesWithMappings.push_back( + std::pair(lusIndex, mapping->PhysicalDeviceIsConnected())); + break; + } + } + break; + } + } + } + + for (auto [lusIndex, connected] : lusDeviceIndiciesWithMappings) { + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::SameLine(); + if (lusIndex == Ship::ShipDeviceIndex::Keyboard) { + ImGui::SmallButton(ICON_FA_KEYBOARD_O); + } else { + ImGui::SmallButton(connected ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } +} + +void BenInputEditorWindow::DrawAnalogStickDeviceIcons(uint8_t portIndex, Ship::Stick stick) { + std::set allLusDeviceIndices; + allLusDeviceIndices.insert(Ship::ShipDeviceIndex::Keyboard); + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappingsFromConfig()) { + allLusDeviceIndices.insert(lusIndex); + } + + std::vector> lusDeviceIndiciesWithMappings; + for (auto lusIndex : allLusDeviceIndices) { + auto controllerStick = + stick == Ship::Stick::LEFT_STICK + ? Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->GetLeftStick() + : Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->GetRightStick(); + if (controllerStick->HasMappingsForShipDeviceIndex(lusIndex)) { + for (auto [direction, mappings] : controllerStick->GetAllAxisDirectionMappings()) { + bool foundMapping = false; + for (auto [id, mapping] : mappings) { + if (mapping->GetShipDeviceIndex() == lusIndex) { + foundMapping = true; + lusDeviceIndiciesWithMappings.push_back( + std::pair(lusIndex, mapping->PhysicalDeviceIsConnected())); + break; + } + } + if (foundMapping) { + break; + } + } + } + } + + for (auto [lusIndex, connected] : lusDeviceIndiciesWithMappings) { + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::SameLine(); + if (lusIndex == Ship::ShipDeviceIndex::Keyboard) { + ImGui::SmallButton(ICON_FA_KEYBOARD_O); + } else { + ImGui::SmallButton(connected ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } +} + +void BenInputEditorWindow::DrawRumbleDeviceIcons(uint8_t portIndex) { + std::set allLusDeviceIndices; + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappingsFromConfig()) { + allLusDeviceIndices.insert(lusIndex); + } + + std::vector> lusDeviceIndiciesWithMappings; + for (auto lusIndex : allLusDeviceIndices) { + if (Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->GetRumble() + ->HasMappingsForShipDeviceIndex(lusIndex)) { + for (auto [id, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->GetRumble() + ->GetAllRumbleMappings()) { + if (mapping->GetShipDeviceIndex() == lusIndex) { + lusDeviceIndiciesWithMappings.push_back( + std::pair(lusIndex, mapping->PhysicalDeviceIsConnected())); + break; + } + } + } + } + + for (auto [lusIndex, connected] : lusDeviceIndiciesWithMappings) { + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::SameLine(); + ImGui::SmallButton(connected ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } +} + +void BenInputEditorWindow::DrawGyroDeviceIcons(uint8_t portIndex) { + auto mapping = + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->GetGyro()->GetGyroMapping(); + if (mapping == nullptr) { + return; + } + + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(mapping->GetShipDeviceIndex(), buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::SameLine(); + ImGui::SmallButton(mapping->PhysicalDeviceIsConnected() ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} + +void BenInputEditorWindow::DrawLEDDeviceIcons(uint8_t portIndex) { + std::set allLusDeviceIndices; + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappingsFromConfig()) { + allLusDeviceIndices.insert(lusIndex); + } + + std::vector> lusDeviceIndiciesWithMappings; + for (auto lusIndex : allLusDeviceIndices) { + if (Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->GetRumble() + ->HasMappingsForShipDeviceIndex(lusIndex)) { + for (auto [id, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->GetLED() + ->GetAllLEDMappings()) { + if (mapping->GetShipDeviceIndex() == lusIndex) { + lusDeviceIndiciesWithMappings.push_back( + std::pair(lusIndex, mapping->PhysicalDeviceIsConnected())); + break; + } + } + } + } + + for (auto [lusIndex, connected] : lusDeviceIndiciesWithMappings) { + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + ImGui::SameLine(); + ImGui::SmallButton(connected ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } +} + +void BenInputEditorWindow::DrawDeviceVisibilityButtons() { + std::map> indexMappings; + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappingsFromConfig()) { + auto sdlIndexMapping = std::static_pointer_cast(mapping); + if (sdlIndexMapping == nullptr) { + continue; + } + + indexMappings[lusIndex] = { sdlIndexMapping->GetSDLControllerName(), -1 }; + } + + for (auto [lusIndex, mapping] : + Ship::Context::GetInstance()->GetControlDeck()->GetDeviceIndexMappingManager()->GetAllDeviceIndexMappings()) { + auto sdlIndexMapping = std::static_pointer_cast(mapping); + if (sdlIndexMapping == nullptr) { + continue; + } + + indexMappings[lusIndex] = { sdlIndexMapping->GetSDLControllerName(), sdlIndexMapping->GetSDLDeviceIndex() }; + } + + auto keyboardButtonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto keyboardButtonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(Ship::ShipDeviceIndex::Keyboard, keyboardButtonColor, keyboardButtonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, keyboardButtonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, keyboardButtonHoveredColor); + bool keyboardVisible = mDeviceIndexVisiblity[Ship::ShipDeviceIndex::Keyboard]; + if (ImGui::Button(StringHelper::Sprintf("%s %s Keyboard", keyboardVisible ? ICON_FA_EYE : ICON_FA_EYE_SLASH, + ICON_FA_KEYBOARD_O) + .c_str())) { + mDeviceIndexVisiblity[Ship::ShipDeviceIndex::Keyboard] = !keyboardVisible; + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + for (auto [lusIndex, info] : indexMappings) { + auto [name, sdlIndex] = info; + bool connected = sdlIndex != -1; + + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + bool visible = mDeviceIndexVisiblity[lusIndex]; + if (ImGui::Button( + StringHelper::Sprintf("%s %s %s (%s)", visible ? ICON_FA_EYE : ICON_FA_EYE_SLASH, + connected ? ICON_FA_GAMEPAD : ICON_FA_CHAIN_BROKEN, name.c_str(), + connected ? StringHelper::Sprintf("SDL %d", sdlIndex).c_str() : "Disconnected") + .c_str())) { + mDeviceIndexVisiblity[lusIndex] = !visible; + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } +} + +void BenInputEditorWindow::DrawClearAllButton(uint8_t portIndex) { + if (ImGui::Button("Clear All", ImGui::CalcTextSize("Clear All") * 2)) { + ImGui::OpenPopup("Clear All##clearAllPopup"); + } + if (ImGui::BeginPopupModal("Clear All##clearAllPopup", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all mappings for port %d.\n\nContinue?", portIndex + 1); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Clear All")) { + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->ClearAllMappings(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawPortTab(uint8_t portIndex) { + if (ImGui::BeginTabItem(StringHelper::Sprintf("Port %d###port%d", portIndex + 1, portIndex).c_str())) { + DrawPortTabContents(portIndex); + ImGui::EndTabItem(); + } +} + +void BenInputEditorWindow::DrawPortTabContents(uint8_t portIndex) { + DrawClearAllButton(portIndex); + DrawSetDefaultsButton(portIndex); + if (!Ship::Context::GetInstance()->GetControlDeck()->IsSinglePlayerMappingMode()) { + ImGui::SameLine(); + if (ImGui::Button("Reorder controllers")) { + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Controller Reordering")->Show(); + } + } + DrawDeviceVisibilityButtons(); + + UpdateBitmaskToMappingIds(portIndex); + UpdateStickDirectionToMappingIds(portIndex); + + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.133f, 0.133f, 0.133f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + + if (ImGui::CollapsingHeader("Buttons", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawButtonDeviceIcons(portIndex, mButtonsBitmasks); + DrawButtonLine("A", portIndex, BTN_A, CHIP_COLOR_N64_BLUE); + DrawButtonLine("B", portIndex, BTN_B, CHIP_COLOR_N64_GREEN); + DrawButtonLine("Start", portIndex, BTN_START, CHIP_COLOR_N64_RED); + DrawButtonLine("L", portIndex, BTN_L); + DrawButtonLine("R", portIndex, BTN_R); + DrawButtonLine("Z", portIndex, BTN_Z); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_UP).c_str(), portIndex, BTN_CUP, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_DOWN).c_str(), portIndex, BTN_CDOWN, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_LEFT).c_str(), portIndex, BTN_CLEFT, + CHIP_COLOR_N64_YELLOW); + DrawButtonLine(StringHelper::Sprintf("C %s", ICON_FA_ARROW_RIGHT).c_str(), portIndex, BTN_CRIGHT, + CHIP_COLOR_N64_YELLOW); + } else { + DrawButtonDeviceIcons(portIndex, mButtonsBitmasks); + } + + if (ImGui::CollapsingHeader("D-Pad", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawButtonDeviceIcons(portIndex, mDpadBitmasks); + DrawButtonLine(StringHelper::Sprintf("%s", ICON_FA_ARROW_UP).c_str(), portIndex, BTN_DUP); + DrawButtonLine(StringHelper::Sprintf("%s", ICON_FA_ARROW_DOWN).c_str(), portIndex, BTN_DDOWN); + DrawButtonLine(StringHelper::Sprintf("%s", ICON_FA_ARROW_LEFT).c_str(), portIndex, BTN_DLEFT); + DrawButtonLine(StringHelper::Sprintf("%s", ICON_FA_ARROW_RIGHT).c_str(), portIndex, BTN_DRIGHT); + } else { + DrawButtonDeviceIcons(portIndex, mDpadBitmasks); + } + + if (ImGui::CollapsingHeader("Analog Stick", NULL, ImGuiTreeNodeFlags_DefaultOpen)) { + DrawAnalogStickDeviceIcons(portIndex, Ship::LEFT_STICK); + DrawStickSection(portIndex, Ship::LEFT, 0); + } else { + DrawAnalogStickDeviceIcons(portIndex, Ship::LEFT_STICK); + } + + if (ImGui::CollapsingHeader("Additional (\"Right\") Stick")) { + DrawAnalogStickDeviceIcons(portIndex, Ship::RIGHT_STICK); + DrawStickSection(portIndex, Ship::RIGHT, 1, CHIP_COLOR_N64_YELLOW); + } else { + DrawAnalogStickDeviceIcons(portIndex, Ship::RIGHT_STICK); + } + + if (ImGui::CollapsingHeader("Rumble")) { + DrawRumbleDeviceIcons(portIndex); + DrawRumbleSection(portIndex); + } else { + DrawRumbleDeviceIcons(portIndex); + } + + if (ImGui::CollapsingHeader("Gyro")) { + DrawGyroDeviceIcons(portIndex); + DrawGyroSection(portIndex); + } else { + DrawGyroDeviceIcons(portIndex); + } + + if (ImGui::CollapsingHeader("LEDs")) { + DrawLEDDeviceIcons(portIndex); + DrawLEDSection(portIndex); + } else { + DrawLEDDeviceIcons(portIndex); + } + + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} + +void BenInputEditorWindow::DrawSetDefaultsButton(uint8_t portIndex) { + ImGui::SameLine(); + auto popupId = StringHelper::Sprintf("setDefaultsPopup##%d", portIndex); + if (ImGui::Button(StringHelper::Sprintf("Set Defaults##%d", portIndex).c_str(), + ImVec2(ImGui::CalcTextSize("Set Defaults") * 2))) { + ImGui::OpenPopup(popupId.c_str()); + } + + if (ImGui::BeginPopup(popupId.c_str())) { + std::map> indexMappings; + for (auto [lusIndex, mapping] : Ship::Context::GetInstance() + ->GetControlDeck() + ->GetDeviceIndexMappingManager() + ->GetAllDeviceIndexMappings()) { + auto sdlIndexMapping = std::static_pointer_cast(mapping); + if (sdlIndexMapping == nullptr) { + continue; + } + + indexMappings[lusIndex] = { sdlIndexMapping->GetSDLControllerName(), sdlIndexMapping->GetSDLDeviceIndex() }; + } + + bool shouldClose = false; + ImGui::PushStyleColor(ImGuiCol_Button, BUTTON_COLOR_KEYBOARD_BEIGE); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, BUTTON_COLOR_KEYBOARD_BEIGE_HOVERED); + if (ImGui::Button(StringHelper::Sprintf("%s Keyboard", ICON_FA_KEYBOARD_O).c_str())) { + ImGui::OpenPopup("Set Defaults for Keyboard"); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + if (ImGui::BeginPopupModal("Set Defaults for Keyboard", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all existing mappings for\nKeyboard on port %d.\n\nContinue?", portIndex + 1); + if (ImGui::Button("Cancel")) { + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Set defaults")) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->ClearAllMappingsForDevice(Ship::ShipDeviceIndex::Keyboard); + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->AddDefaultMappings( + Ship::ShipDeviceIndex::Keyboard); + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + for (auto [lusIndex, info] : indexMappings) { + auto [name, sdlIndex] = info; + + auto buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button); + auto buttonHoveredColor = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + GetButtonColorsForShipDeviceIndex(lusIndex, buttonColor, buttonHoveredColor); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoveredColor); + if (ImGui::Button(StringHelper::Sprintf("%s %s (%s)", ICON_FA_GAMEPAD, name.c_str(), + StringHelper::Sprintf("SDL %d", sdlIndex).c_str()) + .c_str())) { + ImGui::OpenPopup(StringHelper::Sprintf("Set Defaults for %s", name.c_str()).c_str()); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + if (ImGui::BeginPopupModal(StringHelper::Sprintf("Set Defaults for %s", name.c_str()).c_str(), NULL, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("This will clear all existing mappings for\n%s (SDL %d) on port %d.\n\nContinue?", + name.c_str(), sdlIndex, portIndex + 1); + if (ImGui::Button("Cancel")) { + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + if (ImGui::Button("Set defaults")) { + Ship::Context::GetInstance() + ->GetControlDeck() + ->GetControllerByPort(portIndex) + ->ClearAllMappingsForDevice(lusIndex); + Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(portIndex)->AddDefaultMappings( + lusIndex); + shouldClose = true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + + if (ImGui::Button("Cancel") || shouldClose) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void BenInputEditorWindow::DrawFullContents() { + ImGui::BeginTabBar("##ControllerConfigPortTabs"); + for (uint8_t i = 0; i < 4; i++) { + DrawPortTab(i); + } + ImGui::EndTabBar(); +} + +void BenInputEditorWindow::DrawElement() { + DrawFullContents(); +} diff --git a/mm/2s2h/BenGui/BenInputEditorWindow.h b/mm/2s2h/BenGui/BenInputEditorWindow.h new file mode 100644 index 000000000..adb1b8be8 --- /dev/null +++ b/mm/2s2h/BenGui/BenInputEditorWindow.h @@ -0,0 +1,90 @@ +#pragma once + +#include "stdint.h" +#include "window/gui/GuiWindow.h" +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include +#include +#include +#include +#include +#include "controller/controldevice/controller/Controller.h" + +class BenInputEditorWindow : public Ship::GuiWindow { + public: + using Ship::GuiWindow::GuiWindow; + ~BenInputEditorWindow(); + + void DrawButton(const char* label, int32_t n64Btn, int32_t currentPort, int32_t* btnReading); + + void DrawInputChip(const char* buttonName, ImVec4 color); + void DrawAnalogPreview(const char* label, ImVec2 stick, float deadzone = 0, bool gyro = false); + void DrawControllerSchema(); + bool TestingRumble(); + void DrawFullContents(); + void DrawPortTabContents(uint8_t portIndex); + + protected: + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + + private: + void DrawStickDirectionLine(const char* axisDirectionName, uint8_t port, uint8_t stick, Ship::Direction direction, + ImVec4 color); + void DrawButtonLine(const char* buttonName, uint8_t port, CONTROLLERBUTTONS_T bitmask, ImVec4 color); + void DrawButtonLineEditMappingButton(uint8_t port, CONTROLLERBUTTONS_T bitmask, std::string id); + void DrawButtonLineAddMappingButton(uint8_t port, CONTROLLERBUTTONS_T bitmask); + + void DrawStickDirectionLineEditMappingButton(uint8_t port, uint8_t stick, Ship::Direction direction, + std::string id); + void DrawStickDirectionLineAddMappingButton(uint8_t port, uint8_t stick, Ship::Direction direction); + void DrawStickSection(uint8_t port, uint8_t stick, int32_t id, ImVec4 color); + + void DrawRumbleSection(uint8_t port); + void DrawRemoveRumbleMappingButton(uint8_t port, std::string id); + void DrawAddRumbleMappingButton(uint8_t port); + + void DrawLEDSection(uint8_t port); + void DrawRemoveLEDMappingButton(uint8_t port, std::string id); + void DrawAddLEDMappingButton(uint8_t port); + + void DrawGyroSection(uint8_t port); + void DrawRemoveGyroMappingButton(uint8_t port, std::string id); + void DrawAddGyroMappingButton(uint8_t port); + + int32_t mGameInputBlockTimer; + int32_t mMappingInputBlockTimer; + int32_t mRumbleTimer; + std::shared_ptr mRumbleMappingToTest; + + // mBitmaskToMappingIds[port][bitmask] = { id0, id1, ... } + std::unordered_map>> mBitmaskToMappingIds; + + // mStickDirectionToMappingIds[port][stick][direction] = { id0, id1, ... } + std::unordered_map>>> + mStickDirectionToMappingIds; + + void UpdateBitmaskToMappingIds(uint8_t port); + void UpdateStickDirectionToMappingIds(uint8_t port); + + void GetButtonColorsForShipDeviceIndex(Ship::ShipDeviceIndex lusIndex, ImVec4& buttonColor, + ImVec4& buttonHoveredColor); + void DrawPortTab(uint8_t portIndex); + std::set mButtonsBitmasks; + std::set mDpadBitmasks; + void DrawButtonDeviceIcons(uint8_t portIndex, std::set bitmasks); + void DrawAnalogStickDeviceIcons(uint8_t portIndex, Ship::Stick stick); + void DrawRumbleDeviceIcons(uint8_t portIndex); + void DrawGyroDeviceIcons(uint8_t portIndex); + void DrawLEDDeviceIcons(uint8_t portIndex); + bool mInputEditorPopupOpen; + void DrawSetDefaultsButton(uint8_t portIndex); + void DrawClearAllButton(uint8_t portIndex); + + std::map mDeviceIndexVisiblity; + void DrawDeviceVisibilityButtons(); +}; diff --git a/mm/2s2h/BenGui/BenMenuBar.cpp b/mm/2s2h/BenGui/BenMenuBar.cpp index 009f6c075..1a9ca3822 100644 --- a/mm/2s2h/BenGui/BenMenuBar.cpp +++ b/mm/2s2h/BenGui/BenMenuBar.cpp @@ -153,7 +153,7 @@ void DrawBenMenu() { } } -extern std::shared_ptr mInputEditorWindow; +extern std::shared_ptr mBenInputEditorWindow; void DrawSettingsMenu() { if (UIWidgets::BeginMenu("Settings")) { @@ -312,8 +312,8 @@ void DrawSettingsMenu() { // #region 2S2H [Todo] None of this works yet /* if (UIWidgets::BeginMenu("Controller")) { */ - if (mInputEditorWindow) { - UIWidgets::WindowButton("Controller Mapping", "gWindows.InputEditor", mInputEditorWindow); + if (mBenInputEditorWindow) { + UIWidgets::WindowButton("Controller Mapping", "gWindows.InputEditor", mBenInputEditorWindow); } /* #ifndef __SWITCH__ diff --git a/mm/2s2h/BenGui/BenMenuBar.h b/mm/2s2h/BenGui/BenMenuBar.h index 7f509f8fb..7342a0101 100644 --- a/mm/2s2h/BenGui/BenMenuBar.h +++ b/mm/2s2h/BenGui/BenMenuBar.h @@ -7,6 +7,7 @@ #include "DeveloperTools/ActorViewer.h" #include "DeveloperTools/CollisionViewer.h" #include "DeveloperTools/EventLog.h" +#include "BenInputEditorWindow.h" namespace BenGui { class BenMenuBar : public Ship::GuiMenuBar { diff --git a/mm/2s2h/BenGui/HudEditor.cpp b/mm/2s2h/BenGui/HudEditor.cpp index 95202bbbf..a14926542 100644 --- a/mm/2s2h/BenGui/HudEditor.cpp +++ b/mm/2s2h/BenGui/HudEditor.cpp @@ -167,12 +167,6 @@ enum Presets { }; void HudEditorWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(480, 600), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Hud Editor", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::End(); - return; - } - static HudEditor::Presets preset = HudEditor::Presets::VANILLA; if (UIWidgets::Combobox("Preset", &preset, presetNames)) { for (int i = HUD_EDITOR_ELEMENT_B; i < HUD_EDITOR_ELEMENT_MAX; i++) { @@ -252,7 +246,9 @@ void HudEditorWindow::DrawElement() { } if (CVarGetInteger(hudEditorElements[i].modeCvar, HUD_EDITOR_ELEMENT_MODE_VANILLA) >= HUD_EDITOR_ELEMENT_MODE_MOVABLE_43) { - if (ImGui::BeginTable("##table", 3, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoBordersInBody)) { + if (ImGui::BeginTable("##table", 3, + ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoBordersInBody | + ImGuiTableFlags_SizingStretchSame)) { ImGui::TableNextColumn(); UIWidgets::CVarSliderInt("X", hudEditorElements[i].xCvar, -10, 330, hudEditorElements[i].defaultX, { @@ -279,6 +275,4 @@ void HudEditorWindow::DrawElement() { } ImGui::PopID(); } - - ImGui::End(); } diff --git a/mm/2s2h/BenGui/Menu.cpp b/mm/2s2h/BenGui/Menu.cpp new file mode 100644 index 000000000..c788bcc58 --- /dev/null +++ b/mm/2s2h/BenGui/Menu.cpp @@ -0,0 +1,471 @@ +#include "Menu.h" +#include "BenPort.h" +#include "BenGui.hpp" +#include "UIWidgets.hpp" +#include "graphic/Fast3D/gfx_rendering_api.h" +#include "2s2h/Enhancements/Enhancements.h" +#include "2s2h/Enhancements/Graphics/MotionBlur.h" +#include "2s2h/Enhancements/Graphics/PlayAsKafei.h" +#include "2s2h/DeveloperTools/DeveloperTools.h" +#include "window/gui/GuiMenuBar.h" +#include "window/gui/GuiElement.h" +#include "DeveloperTools/SaveEditor.h" +#include "DeveloperTools/ActorViewer.h" +#include "DeveloperTools/CollisionViewer.h" +#include "DeveloperTools/EventLog.h" +#include "HudEditor.h" + +#include "SearchableMenuItems.h" + +extern "C" { +#include "z64.h" +#include "functions.h" +extern PlayState* gPlayState; +} +std::vector windowTypeSizes = { {} }; + +namespace BenGui { + +extern std::shared_ptr mHudEditorWindow; +extern std::shared_ptr mStatsWindow; +extern std::shared_ptr mConsoleWindow; +extern std::shared_ptr mGfxDebuggerWindow; +extern std::shared_ptr mSaveEditorWindow; +extern std::shared_ptr mActorViewerWindow; +extern std::shared_ptr mCollisionViewerWindow; +extern std::shared_ptr mEventLogWindow; +extern std::shared_ptr mBenInputEditorWindow; + +extern std::shared_ptr> availableWindowBackends; +extern std::unordered_map availableWindowBackendsMap; +extern Ship::WindowBackend configWindowBackend; +extern void UpdateWindowBackendObjects(); + +// BENTODO: Not implemented yet +// UIWidgets::CVarCheckbox("Widescreen Actor Culling", +// "gEnhancements.Graphics.ActorCullingAccountsForWidescreen", +// { .tooltip = "Adjusts the culling planes to account for widescreen resolutions. " +// "This may have unintended side effects." }); + +// if (gPlayState != NULL) { +// ImGui::Separator(); +// SearchMenuGetItem(MENU_ITEM_FRAME_ADVANCE_ENABLE); +// if (gPlayState->frameAdvCtx.enabled) { +// SearchMenuGetItem(MENU_ITEM_FRAME_ADVANCE_SINGLE); +// SearchMenuGetItem(MENU_ITEM_FRAME_ADVANCE_HOLD); +// if (ImGui::IsItemActive()) { +// CVarSetInteger("gDeveloperTools.FrameAdvanceTick", 1); +// } +// } +// } +// ImGui::PushStyleColor(ImGuiCol_Button, menuTheme[menuThemeIndex]); +// RenderWarpPointSection(); +// ImGui::PopStyleColor(1); +//} + +BenMenu::BenMenu(const std::string& consoleVariable, const std::string& name) : GuiWindow(consoleVariable, name) { +} + +void BenMenu::InitElement() { + popped = CVarGetInteger("gSettings.Menu.Popout", 0); + poppedSize.x = CVarGetInteger("gSettings.Menu.PoppedWidth", 1280); + poppedSize.y = CVarGetInteger("gSettings.Menu.PoppedHeight", 800); + poppedPos.x = CVarGetInteger("gSettings.Menu.PoppedPos.x", 0); + poppedPos.y = CVarGetInteger("gSettings.Menu.PoppedPos.y", 0); + AddSettings(); + AddEnhancements(); + AddDevTools(); + + menuEntries = { { "Settings", settingsSidebar, "gSettings.Menu.SettingsSidebarIndex" }, + { "Enhancements", enhancementsSidebar, "gSettings.Menu.EnhancementsSidebarIndex" }, + { "Developer Tools", devToolsSidebar, "gSettings.Menu.DevToolsSidebarIndex" } }; + + UpdateWindowBackendObjects(); +} + +void BenMenu::UpdateElement() { +} + +bool ModernMenuSidebarEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID sidebarId = window->GetID(std::string(label + "##Sidebar").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + pos.y += style.FramePadding.y; + pos.x = window->WorkRect.GetCenter().x - labelSize.x / 2; + ImRect bb = { pos - style.FramePadding, pos + labelSize + style.FramePadding }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, sidebarId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, sidebarId, &hovered, &held); + if (pressed) { + ImGui::MarkItemEdited(sidebarId); + } + window->DrawList->AddRectFilled(pos - style.FramePadding, pos + labelSize + style.FramePadding, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +bool ModernMenuHeaderEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID headerId = window->GetID(std::string(label + "##Header").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + ImRect bb = { pos, pos + labelSize + style.FramePadding * 2 }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, headerId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, headerId, &hovered, &held); + window->DrawList->AddRectFilled(bb.Min, bb.Max, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + pos += style.FramePadding; + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +void BenMenu::Draw() { + if (!IsVisible()) { + return; + } + DrawElement(); + // Sync up the IsVisible flag if it was changed by ImGui + SyncVisibilityConsoleVariable(); +} + +void BenMenu::DrawElement() { + for (auto& [reason, info] : disabledMap) { + info.active = info.evaluation(info); + } + menuThemeIndex = static_cast(CVarGetInteger("gSettings.MenuTheme", 3)); + + windowHeight = ImGui::GetMainViewport()->WorkSize.y; + windowWidth = ImGui::GetMainViewport()->WorkSize.x; + auto windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings; + bool popout = CVarGetInteger("gSettings.Menu.Popout", 0) && allowPopout; + if (popout) { + windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking; + } + if (popout != popped) { + if (popout) { + windowHeight = poppedSize.y; + windowWidth = poppedSize.x; + ImGui::SetNextWindowSize({ static_cast(windowWidth), static_cast(windowHeight) }, + ImGuiCond_Always); + ImGui::SetNextWindowPos(poppedPos, ImGuiCond_Always); + } else if (popped) { + CVarSetFloat("gSettings.Menu.PoppedWidth", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedHeight", poppedSize.y); + CVarSave(); + } + } + popped = popout; + auto windowCond = ImGuiCond_Always; + if (!popout) { + ImGui::SetNextWindowSize({ static_cast(windowWidth), static_cast(windowHeight) }, windowCond); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), windowCond, { 0.5f, 0.5f }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + } + if (!ImGui::Begin("Main Menu", NULL, windowFlags | ImGuiWindowFlags_NoBringToFrontOnFocus)) { + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::End(); + return; + } + if (popped != popout) { + if (!popout) { + ImGui::PopStyleVar(); + } + CVarSetInteger("gSettings.Menu.Popout", popped); + CVarSetFloat("gSettings.Menu.PoppedWidth", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedHeight", poppedSize.y); + CVarSetFloat("gSettings.Menu.PoppedPos.x", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedPos.y", poppedSize.y); + CVarSave(); + ImGui::End(); + return; + } + ImGui::PushFont(OTRGlobals::Instance->fontStandardLargest); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + windowHeight = window->WorkRect.GetHeight(); + windowWidth = window->WorkRect.GetWidth(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); + auto sectionCount = menuEntries.size(); + const char* headerCvar = "gSettings.Menu.SelectedHeader"; + uint8_t headerIndex = CVarGetInteger(headerCvar, 0); + ImVec2 pos = window->DC.CursorPos; + float centerX = pos.x + windowWidth / 2 - (style.ItemSpacing.x * (sectionCount + 1)); + std::vector headerSizes; + float headerWidth = 200.0f + style.ItemSpacing.x; + for (int i = 0; i < sectionCount; i++) { + ImVec2 size = ImGui::CalcTextSize(menuEntries.at(i).label.c_str()); + headerSizes.push_back(size); + headerWidth += size.x + style.FramePadding.x * 2; + if (i + 1 < sectionCount) { + headerWidth += style.ItemSpacing.x; + } + } + ImVec2 menuSize = { std::fminf(1280, windowWidth), std::fminf(800, windowHeight) }; + pos += window->WorkRect.GetSize() / 2 - menuSize / 2; + ImGui::SetNextWindowPos(pos); + ImGui::BeginChild("Menu Block", menuSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar); + + std::vector sidebar; + float headerHeight = headerSizes.at(0).y + style.FramePadding.y * 2; + ImVec2 buttonSize = ImGui::CalcTextSize(ICON_FA_TIMES_CIRCLE) + style.FramePadding * 2; + bool scrollbar = false; + if (headerWidth > menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3) { + headerHeight += style.ScrollbarSize; + scrollbar = true; + } + if (UIWidgets::Button(ICON_FA_TIMES_CIRCLE, { .size = UIWidgets::Sizes::Inline, .tooltip = "Close Menu (Esc)" })) { + ToggleVisibility(); + } + ImGui::SameLine(); + ImGui::SetNextWindowSizeConstraints({ 0, headerHeight }, { headerWidth, headerHeight }); + ImVec2 headerSelSize = { menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3, headerHeight }; + if (scrollbar) { + headerSelSize.y += style.ScrollbarSize; + } + bool autoFocus = CVarGetInteger("gSettings.SearchAutofocus", 0); + bool headerSearch = !CVarGetInteger("gSettings.SidebarSearch", 0); + ImGui::BeginChild("Header Selection", headerSelSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_HorizontalScrollbar); + for (int i = 0; i < sectionCount; i++) { + auto entry = menuEntries.at(i); + uint8_t nextIndex = i; + UIWidgets::PushStyleButton(menuTheme[menuThemeIndex]); + if (headerIndex != i) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuHeaderEntry(entry.label)) { + if (autoFocus) { + menuSearch.Clear(); + } + CVarSetInteger(headerCvar, i); + CVarSave(); + nextIndex = i; + } + if (headerIndex != i) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (headerIndex == i) { + sidebar = entry.sidebarEntries; + } + if (i + 1 < sectionCount) { + ImGui::SameLine(); + } + if (nextIndex != i) { + headerIndex = nextIndex; + } + } + std::string menuSearchText = ""; + if (headerSearch) { + ImGui::SameLine(); + if (autoFocus && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() && + !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + ImGui::PushStyleColor(ImGuiCol_FrameBg, { 0, 0, 0, 0 }); + menuSearch.Draw("##search", 200.0f); + menuSearchText = menuSearch.InputBuf; + if (menuSearchText.length() < 1) { + ImGui::SameLine(headerWidth - 200.0f + style.ItemSpacing.x); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "Search..."); + } + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::SameLine(menuSize.x - (buttonSize.x * 2) - style.ItemSpacing.x); + if (UIWidgets::Button(ICON_FA_UNDO, { .color = UIWidgets::Colors::Red, + .size = UIWidgets::Sizes::Inline, + .tooltip = "Reset" +#ifdef __APPLE__ + " (Command-R)" +#elif !defined(__SWITCH__) && !defined(__WIIU__) + " (Ctrl+R)" +#else + "" +#endif + })) { + std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) + ->Dispatch("reset"); + } + ImGui::SameLine(); + if (UIWidgets::Button( + ICON_FA_POWER_OFF, + { .color = UIWidgets::Colors::Red, .size = UIWidgets::Sizes::Inline, .tooltip = "Quit 2S2H" })) { + if (!popped) { + ToggleVisibility(); + } + Ship::Context::GetInstance()->GetWindow()->Close(); + } + ImGui::PopStyleVar(); + + pos.y += headerHeight + style.ItemSpacing.y; + pos.x = centerX - menuSize.x / 2 + (style.ItemSpacing.x * (sectionCount + 1)); + window->DrawList->AddRectFilled(pos, pos + ImVec2{ menuSize.x, 4 }, ImGui::GetColorU32({ 255, 255, 255, 255 }), + true, style.WindowRounding); + pos.y += style.ItemSpacing.y; + float sectionHeight = menuSize.y - headerHeight - 4 - style.ItemSpacing.y * 2; + float columnHeight = sectionHeight - style.ItemSpacing.y * 4; + ImGui::SetNextWindowPos(pos + style.ItemSpacing * 2); + float sidebarWidth = 200 - style.ItemSpacing.x; + + const char* sidebarCvar = menuEntries.at(headerIndex).sidebarCvar; + + uint8_t sectionIndex = CVarGetInteger(sidebarCvar, 0); + if (sectionIndex > sidebar.size() - 1) + sectionIndex = sidebar.size() - 1; + if (sectionIndex < 0) + sectionIndex = 0; + float sectionCenterX = pos.x + (sidebarWidth / 2); + float topY = pos.y; + ImGui::SetNextWindowSizeConstraints({ sidebarWidth, 0 }, { sidebarWidth, columnHeight }); + ImGui::BeginChild((menuEntries.at(headerIndex).label + " Section").c_str(), { sidebarWidth, columnHeight * 3 }, + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, ImGuiWindowFlags_NoTitleBar); + for (size_t i = 0; i < sidebar.size(); i++) { + auto sidebarEntry = sidebar.at(i); + uint8_t nextIndex = i; + UIWidgets::PushStyleButton(menuTheme[menuThemeIndex]); + if (sectionIndex != i) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuSidebarEntry(sidebarEntry.label)) { + if (autoFocus) { + menuSearch.Clear(); + } + CVarSetInteger(sidebarCvar, i); + CVarSave(); + nextIndex = i; + } + if (sectionIndex != i) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (nextIndex != i) { + sectionIndex = i; + } + } + ImGui::EndChild(); + + ImGui::PushFont(OTRGlobals::Instance->fontMonoLarger); + pos = ImVec2{ sectionCenterX + (sidebarWidth / 2), topY } + style.ItemSpacing * 2; + window->DrawList->AddRectFilled(pos, pos + ImVec2{ 4, sectionHeight - style.FramePadding.y * 2 }, + ImGui::GetColorU32({ 255, 255, 255, 255 }), true, style.WindowRounding); + pos.x += 4 + style.ItemSpacing.x; + ImGui::SetNextWindowPos(pos + style.ItemSpacing); + float sectionWidth = menuSize.x - sidebarWidth - 4 - style.ItemSpacing.x * 4; + std::string sectionMenuId = sidebar.at(sectionIndex).label + " Settings"; + int columns = sidebar.at(sectionIndex).columnCount; + size_t columnFuncs = sidebar.at(sectionIndex).columnWidgets.size(); + if (windowWidth < 800) { + columns = 1; + } + float columnWidth = (sectionWidth - style.ItemSpacing.x * columns) / columns; + bool useColumns = columns > 1; + if (!useColumns || (headerSearch && menuSearchText.length() > 0)) { + ImGui::SameLine(); + ImGui::SetNextWindowSizeConstraints({ sectionWidth, 0 }, { sectionWidth, columnHeight }); + ImGui::BeginChild(sectionMenuId.c_str(), { sectionWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + if (headerSearch && menuSearchText.length() > 0) { + ImGui::BeginChild("Search Results"); + int searchCount = 0; + for (auto& [menuLabel, menuSidebar, cvar] : menuEntries) { + for (auto& sidebar : menuSidebar) { + for (auto& widgets : sidebar.columnWidgets) { + int column = 1; + for (auto& info : widgets) { + if (info.widgetType == WIDGET_SEPARATOR || info.widgetType == WIDGET_SEPARATOR_TEXT || + info.isHidden) { + continue; + } + std::string widgetStr = std::string(info.widgetName) + std::string(info.widgetTooltip); + std::transform(menuSearchText.begin(), menuSearchText.end(), menuSearchText.begin(), ::tolower); + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), + menuSearchText.end()); + std::transform(widgetStr.begin(), widgetStr.end(), widgetStr.begin(), ::tolower); + widgetStr.erase(std::remove(widgetStr.begin(), widgetStr.end(), ' '), widgetStr.end()); + if (widgetStr.find(menuSearchText) != std::string::npos) { + SearchMenuGetItem(info); + searchCount++; + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::Colors::Gray); + std::string origin = fmt::format(" ({} -> {}, Clmn {})", menuLabel, sidebar.label, column); + ImGui::Text("%s", origin.c_str()); + ImGui::PopStyleColor(); + } + } + column++; + } + } + } + + if (searchCount == 0) { + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("No results found").x) / 2); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "No results found"); + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("Clear Search").x) / 2 - 10.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + if (UIWidgets::Button("Clear Search", { .size = UIWidgets::Sizes::Inline })) { + menuSearch.Clear(); + } + } + + ImGui::EndChild(); + } else { + for (int i = 0; i < columnFuncs; i++) { + std::string sectionId = fmt::format("{} Column {}", sectionMenuId, i); + if (useColumns) { + ImGui::SetNextWindowSizeConstraints({ columnWidth, 0 }, { columnWidth, columnHeight }); + ImGui::BeginChild(sectionId.c_str(), { columnWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + for (auto& entry : sidebar.at(sectionIndex).columnWidgets.at(i)) { + SearchMenuGetItem(entry); + } + if (useColumns) { + ImGui::EndChild(); + } + if (i < columns - 1) { + ImGui::SameLine(); + } + } + } + if (!useColumns || menuSearchText.length() > 0) { + ImGui::EndChild(); + } + ImGui::PopFont(); + ImGui::PopFont(); + + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + if (popout) { + poppedSize = ImGui::GetWindowSize(); + poppedPos = ImGui::GetWindowPos(); + } + ImGui::End(); +} +} // namespace BenGui diff --git a/mm/2s2h/BenGui/Menu.h b/mm/2s2h/BenGui/Menu.h new file mode 100644 index 000000000..ddfb2a57e --- /dev/null +++ b/mm/2s2h/BenGui/Menu.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "UIWidgets.hpp" + +namespace BenGui { +class BenMenu : public Ship::GuiWindow { + public: + using Ship::GuiWindow::GuiWindow; + + BenMenu(const std::string& consoleVariable, const std::string& name); + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + void Draw() override; + + protected: + ImVec2 mOriginalSize; + std::string mName; + uint32_t mWindowFlags; + + private: + bool allowPopout = true; // PortNote: should be set to false on small screen ports + bool popped; + ImVec2 poppedSize; + ImVec2 poppedPos; + float windowHeight; + float windowWidth; +}; +} // namespace BenGui \ No newline at end of file diff --git a/mm/2s2h/BenGui/SearchableMenuItems.h b/mm/2s2h/BenGui/SearchableMenuItems.h new file mode 100644 index 000000000..a8d8c846f --- /dev/null +++ b/mm/2s2h/BenGui/SearchableMenuItems.h @@ -0,0 +1,1705 @@ +#include "2s2h/Enhancements/Enhancements.h" +#include "2s2h/DeveloperTools/DeveloperTools.h" +#include "2s2h/Enhancements/Graphics/3DItemDrops.h" +#include "UIWidgets.hpp" +#include "BenMenuBar.h" +#include "macros.h" +#include "variables.h" +#include +#include + +extern "C" { +#include "functions.h" +extern PlayState* gPlayState; +} + +typedef enum { + COLOR_WHITE, + COLOR_GRAY, + COLOR_DARK_GRAY, + COLOR_INDIGO, + COLOR_RED, + COLOR_DARK_RED, + COLOR_LIGHT_GREEN, + COLOR_GREEN, + COLOR_DARK_GREEN, + COLOR_YELLOW, +} ColorOption; + +typedef enum { + DISABLE_FOR_CAMERAS_OFF, + DISABLE_FOR_DEBUG_CAM_ON, + DISABLE_FOR_DEBUG_CAM_OFF, + DISABLE_FOR_FREE_LOOK_ON, + DISABLE_FOR_FREE_LOOK_OFF, + DISABLE_FOR_AUTO_SAVE_OFF, + DISABLE_FOR_NULL_PLAY_STATE, + DISABLE_FOR_DEBUG_MODE_OFF, + DISABLE_FOR_NO_VSYNC, + DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + DISABLE_FOR_NO_MULTI_VIEWPORT, + DISABLE_FOR_NOT_DIRECTX, + DISABLE_FOR_DIRECTX, + DISABLE_FOR_MATCH_REFRESH_RATE_ON, + DISABLE_FOR_MOTION_BLUR_MODE, + DISABLE_FOR_MOTION_BLUR_OFF, + DISABLE_FOR_FRAME_ADVANCE_OFF, + DISABLE_FOR_WARP_POINT_NOT_SET +} DisableOption; + +struct widgetInfo; +struct disabledInfo; +using VoidFunc = void (*)(); +using DisableInfoFunc = bool (*)(disabledInfo&); +using DisableVec = std::vector; +using WidgetFunc = void (*)(widgetInfo&); +std::string disabledTempTooltip; +const char* disabledTooltip; +bool disabledValue = false; +ColorOption menuThemeIndex = COLOR_INDIGO; +const ImVec4 COLOR_NONE = { 0, 0, 0, 0 }; + +typedef enum { + WIDGET_CHECKBOX, + WIDGET_COMBOBOX, + WIDGET_SLIDER_INT, + WIDGET_SLIDER_FLOAT, + WIDGET_CVAR_CHECKBOX, + WIDGET_CVAR_COMBOBOX, + WIDGET_CVAR_SLIDER_INT, + WIDGET_CVAR_SLIDER_FLOAT, + WIDGET_BUTTON, + WIDGET_COLOR_24, // color picker without alpha + WIDGET_COLOR_32, // color picker with alpha + WIDGET_SEARCH, + WIDGET_SEPARATOR, + WIDGET_SEPARATOR_TEXT, + WIDGET_TEXT, + WIDGET_WINDOW_BUTTON, + WIDGET_AUDIO_BACKEND, // needed because of special operations that can't be handled easily with the normal combobox + // widget + WIDGET_VIDEO_BACKEND // same as above +} WidgetType; + +typedef enum { + MOTION_BLUR_DYNAMIC, + MOTION_BLUR_ALWAYS_OFF, + MOTION_BLUR_ALWAYS_ON, +} MotionBlurOption; + +typedef enum { + DEBUG_LOG_TRACE, + DEBUG_LOG_DEBUG, + DEBUG_LOG_INFO, + DEBUG_LOG_WARN, + DEBUG_LOG_ERROR, + DEBUG_LOG_CRITICAL, + DEBUG_LOG_OFF, +} DebugLogOption; + +// 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; + +// contains various information used to display specific types of widgets +// `min` and `max` are usually only used by sliders +// `defaultVariant` can be used by all widgetTypes, but defaults to 0, "", 0.0f, and white respectively, so only needed +// when you want to change those defaults +// `comboBoxOptions` is the list of dropdown options to be added to a dropdown. +// this needs to be a map of int32_t to const char*, but can also be enum values +// `valuePointer` is used for non-CVar sliders to track current widgetValue +// `size` applies only to buttons, determines whether it stretches to fill its current container or only fits its text +// `color` can be used in any way, depending on the widget. color pickers (COLOR_24 and COLOR_32) will eventually +// use this for storing its current value; WIDGET_SEPARATOR_TEXT uses this to change the text color `windowName` +// is used when using WIDGET_WINDOW_BUTTON, which optionally draws windows, like Stats, in the menu with a button +// to pop the window out of the main menu overlay +// `windowName` is what is displayed and searched for `windowButton` type and window interactions +// `labelPosition` applies to checkbox, combobox and button types. specifies label orientation compared to widget body +// `sameLine` allows widgets to be displayed on the same line as the previously registered widget +// the next three only apply to sliders. `showButtons` shows or hides the +/- buttons on either side of a slider +// `format` specifies how info is displayed within the slider +// `isPercentage` toggles the float slider's value being multiplied by 100 to show percentages instead of direct floats +struct WidgetOptions { + CVarVariant min; + CVarVariant max; + CVarVariant defaultVariant; + std::unordered_map comboBoxOptions; + std::variant valuePointer; + ImVec2 size = UIWidgets::Sizes::Fill; + ImVec4 color = COLOR_NONE; + const char* windowName = ""; + UIWidgets::LabelPosition labelPosition = UIWidgets::LabelPosition::None; + bool sameLine = false; + bool showButtons = true; + const char* format = "%f"; + bool isPercentage = false; +}; + +bool operator==(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b; +} + +bool operator==(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b && l.a == r.a; +} + +bool operator<(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b) || (l.r <= r.r && l.g < r.g && l.b <= r.b) || + (l.r <= r.r && l.g <= r.g && l.b < r.b); +} + +bool operator<(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g < r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b < r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b <= r.b && l.a < r.a); +} + +bool operator>(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b) || (l.r >= r.r && l.g > r.g && l.b >= r.b) || + (l.r >= r.r && l.g >= r.g && l.b > r.b); +} + +bool operator>(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g > r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b > r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b >= r.b && l.a > r.a); +} + +std::unordered_map menuTheme = { { COLOR_WHITE, UIWidgets::Colors::White }, + { COLOR_GRAY, UIWidgets::Colors::Gray }, + { COLOR_DARK_GRAY, UIWidgets::Colors::DarkGray }, + { COLOR_INDIGO, UIWidgets::Colors::Indigo }, + { COLOR_RED, UIWidgets::Colors::Red }, + { COLOR_DARK_RED, UIWidgets::Colors::DarkRed }, + { COLOR_LIGHT_GREEN, UIWidgets::Colors::LightGreen }, + { COLOR_GREEN, UIWidgets::Colors::Green }, + { COLOR_DARK_GREEN, UIWidgets::Colors::DarkGreen }, + { COLOR_YELLOW, UIWidgets::Colors::Yellow } }; + +// All the info needed for display and search of all widgets in the menu. WidgetName is the label displayed, +// `widgetCVar` is the string representation of the CVar used to store the widget value +// `widgetTooltip` is what is displayed when hovering (except when disabled, more on that later) +// `widgetType` is the WidgetType for the widget, which is what determines how the information is used in the draw +// function. all of the preceding are required parts for every widget except for the special widgets (backend dropdowns, +// separators, etc) various parts of widgetOptions are required depending on what widget type you're using +// `widgetCallback` is a lambda used for running code on widget change +// `preFunc` is a lambda called before drawing code starts. It can be used to determine a widget's status, +// whether disabled or hidden, as well as update pointers for non-CVar widget types. +// `postFunc` is a lambda called after all drawing code is finished, for reacting to states other than +// widgets having been changed, like holding buttons. +// All three lambdas accept a `widgetInfo` pointer in case it needs information on the widget for these operations +// `activeDisables` is a vector of DisableOptions for specifying what reasons a widget is disabled, which are displayed +// in the disabledTooltip for the widget. Can display multiple reasons. Handling the reasons is done in `modifierFunc`. +// It is recommended to utilize `disabledInfo`/`DisableReason` to list out all reasons for disabling and isHidden so +// the info can be shown. +// `isHidden` just prevents the widget from being drawn under whatever circumstances you specify in the `modifierFunc` +struct widgetInfo { + std::string widgetName; // Used by all widgets + const char* widgetCVar; // Used by all widgets except + const char* widgetTooltip; + WidgetType widgetType; + WidgetOptions widgetOptions; + WidgetFunc widgetCallback = nullptr; + WidgetFunc preFunc = nullptr; + WidgetFunc postFunc = nullptr; + DisableVec activeDisables = {}; + bool isHidden = false; +}; + +// `disabledInfo` holds information on reasons for hiding or disabling a widget, as well as an evaluation lambda that +// is run once per frame to update its status (this is done to prevent dozens of redundant CVarGets in each frame loop) +// `evaluation` returns a bool which can be determined by whatever code you want that changes its status +// `reason` is the text displayed in the disabledTooltip when a widget is disabled by a particular DisableReason +// `active` is what's referenced when determining disabled status for a widget that uses this This can also be used to +// hold reasons to hide widgets so taht their evaluations are also only run once per frame +struct disabledInfo { + DisableInfoFunc evaluation; + const char* reason; + bool active = false; + int32_t value = 0; +}; + +// Contains the name displayed in the sidebar (label), the number of columns to use in drawing (columnCount; for visual +// separation, 1-3), and nested vectors of the widgets, grouped by column (columnWidgets). The number of widget vectors +// added to the column groups does not need to match the specified columnCount, e.g. you can have one vector added to +// the sidebar, but still separate the window into 3 columns and display only in the first column +struct SidebarEntry { + std::string label; + uint32_t columnCount; + std::vector> columnWidgets; +}; + +// Contains entries for what's listed in the header at the top, including the name displayed on the top bar (label), +// a vector of the SidebarEntries for that header entry, and the name of the cvar used to track what sidebar entry is +// the last viewed for that header. +struct MainMenuEntry { + std::string label; + std::vector sidebarEntries; + const char* sidebarCvar; +}; +extern std::unordered_map warpPointSceneList; +extern void Warp(); + +namespace BenGui { +extern std::shared_ptr> availableWindowBackends; +extern std::unordered_map availableWindowBackendsMap; +extern Ship::WindowBackend configWindowBackend; +extern void UpdateWindowBackendObjects(); + +std::vector menuEntries; +static ImGuiTextFilter menuSearch; +std::vector settingsSidebar; +std::vector enhancementsSidebar; +std::vector devToolsSidebar; +uint8_t searchSidebarIndex = 0; +SidebarEntry searchSidebarEntry = { + "Search", + 1, + { { { "Menu Theme", "", "Searches all menus for the given text, including tooltips.", WIDGET_SEARCH } } } +}; + +static std::map disabledMap = { + { DISABLE_FOR_CAMERAS_OFF, + { [](disabledInfo& info) -> bool { + return !CVarGetInteger("gEnhancements.Camera.DebugCam.Enable", 0) && + !CVarGetInteger("gEnhancements.Camera.FreeLook.Enable", 0); + }, + "Both Debug Camera and Free Look are Disabled" } }, + { DISABLE_FOR_DEBUG_CAM_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger("gEnhancements.Camera.DebugCam.Enable", 0); }, + "Debug Camera is Enabled" } }, + { DISABLE_FOR_DEBUG_CAM_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gEnhancements.Camera.DebugCam.Enable", 0); }, + "Debug Camera is Disabled" } }, + { DISABLE_FOR_FREE_LOOK_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger("gEnhancements.Camera.FreeLook.Enable", 0); }, + "Free Look is Enabled" } }, + { DISABLE_FOR_FREE_LOOK_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gEnhancements.Camera.FreeLook.Enable", 0); }, + "Free Look is Disabled" } }, + { DISABLE_FOR_AUTO_SAVE_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gEnhancements.Saving.Autosave", 0); }, + "AutoSave is Disabled" } }, + { DISABLE_FOR_NULL_PLAY_STATE, + { [](disabledInfo& info) -> bool { return gPlayState == NULL; }, "Save Not Loaded" } }, + { DISABLE_FOR_DEBUG_MODE_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gDeveloperTools.DebugEnabled", 0); }, + "Debug Mode is Disabled" } }, + { DISABLE_FOR_NO_VSYNC, + { [](disabledInfo& info) -> bool { return !Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync(); }, + "Disabling VSync not supported" } }, + { DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen(); + }, + "Windowed Fullscreen not supported" } }, + { DISABLE_FOR_NO_MULTI_VIEWPORT, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports(); + }, + "Multi-viewports not supported" } }, + { DISABLE_FOR_NOT_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Available Only on DirectX" } }, + { DISABLE_FOR_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Not Available on DirectX" } }, + { DISABLE_FOR_MATCH_REFRESH_RATE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger("gMatchRefreshRate", 0); }, + "Match Refresh Rate is Enabled" } }, + { DISABLE_FOR_MOTION_BLUR_MODE, + { [](disabledInfo& info) -> bool { + info.value = CVarGetInteger("gEnhancements.Graphics.MotionBlur.Mode", 0); + return !info.value; + }, + "Motion Blur Mode mismatch" } }, + { DISABLE_FOR_MOTION_BLUR_OFF, + { [](disabledInfo& info) -> bool { return !R_MOTION_BLUR_ENABLED; }, "Motion Blur is Disabled" } }, + { DISABLE_FOR_FRAME_ADVANCE_OFF, + { [](disabledInfo& info) -> bool { return !(gPlayState != nullptr && gPlayState->frameAdvCtx.enabled); }, + "Frame Advance is Disabled" } }, + { DISABLE_FOR_WARP_POINT_NOT_SET, + { [](disabledInfo& info) -> bool { return !CVarGetInteger(WARP_POINT_CVAR "Saved", 0); }, + "Warp Point Not Saved" } } +}; + +std::unordered_map menuThemeOptions = { + { COLOR_WHITE, "White" }, + { COLOR_GRAY, "Gray" }, + { COLOR_DARK_GRAY, "Dark Gray" }, + { COLOR_INDIGO, "Indigo" }, + { COLOR_RED, "Red" }, + { COLOR_DARK_RED, "Dark Red" }, + { COLOR_LIGHT_GREEN, "Light Green" }, + { COLOR_GREEN, "Green" }, + { COLOR_DARK_GREEN, "Dark Green" }, + { COLOR_YELLOW, "Yellow" }, +}; + +static const std::unordered_map alwaysWinDoggyraceOptions = { + { ALWAYS_WIN_DOGGY_RACE_OFF, "Off" }, + { ALWAYS_WIN_DOGGY_RACE_MASKOFTRUTH, "When owning Mask of Truth" }, + { ALWAYS_WIN_DOGGY_RACE_ALWAYS, "Always" }, +}; + +static const std::unordered_map clockTypeOptions = { + { CLOCK_TYPE_ORIGINAL, "Original" }, + { CLOCK_TYPE_3DS, "MM3D style" }, + { CLOCK_TYPE_TEXT_BASED, "Text only" }, +}; + +static const std::unordered_map textureFilteringMap = { + { FILTER_THREE_POINT, "Three-Point" }, + { FILTER_LINEAR, "Linear" }, + { FILTER_NONE, "None" }, +}; + +static const std::unordered_map motionBlurOptions = { + { MOTION_BLUR_DYNAMIC, "Dynamic (default)" }, + { MOTION_BLUR_ALWAYS_OFF, "Always Off" }, + { MOTION_BLUR_ALWAYS_ON, "Always On" }, +}; + +static const std::unordered_map debugSaveOptions = { + { DEBUG_SAVE_INFO_COMPLETE, "100\% save" }, + { DEBUG_SAVE_INFO_VANILLA_DEBUG, "Vanilla debug save" }, + { DEBUG_SAVE_INFO_NONE, "Empty save" }, +}; + +static const std::unordered_map logLevels = { + { DEBUG_LOG_TRACE, "Trace" }, { DEBUG_LOG_DEBUG, "Debug" }, { DEBUG_LOG_INFO, "Info" }, + { DEBUG_LOG_WARN, "Warn" }, { DEBUG_LOG_ERROR, "Error" }, { DEBUG_LOG_CRITICAL, "Critical" }, + { DEBUG_LOG_OFF, "Off" }, +}; + +static const std::unordered_map audioBackendsMap = { + { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, + { Ship::AudioBackend::SDL, "SDL" }, +}; + +static std::unordered_map windowBackendsMap = { + { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, + { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, + { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, +}; + +static const std::unordered_map timeStopOptions = { + { TIME_STOP_OFF, "Off" }, + { TIME_STOP_TEMPLES, "Temples" }, + { TIME_STOP_TEMPLES_DUNGEONS, "Temples + Mini Dungeons" }, +}; + +void FreeLookPitchMinMax() { + f32 maxY = CVarGetFloat("gEnhancements.Camera.FreeLook.MaxPitch", 72.0f); + f32 minY = CVarGetFloat("gEnhancements.Camera.FreeLook.MinPitch", -49.0f); + CVarSetFloat("gEnhancements.Camera.FreeLook.MaxPitch", std::max(maxY, minY)); + CVarSetFloat("gEnhancements.Camera.FreeLook.MinPitch", std::min(maxY, minY)); +} + +void AddSettings() { + // General Settings + settingsSidebar.push_back( + { "General", + 3, + { { + { "Menu Theme", + "gSettings.MenuTheme", + "Changes the Theme of the Menu Widgets.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = menuThemeOptions } }, +#if not defined(__SWITCH__) and not defined(__WIIU__) + { "Menubar Controller Navigation", CVAR_IMGUI_CONTROLLER_NAV, + "Allows controller navigation of the SOH menu bar (Settings, Enhancements,...)\nCAUTION: " + "This will disable game inputs while the menu is visible.\n\nD-pad to move between " + "items, A to select, B to move up in scope. DEV NOTE: SDL is weird currently, pad button only " + "works with menubar open.", + WIDGET_CVAR_CHECKBOX }, + { "Cursor Always Visible", + "gSettings.CursorVisibility", + "Makes the cursor always visible, even in full screen.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetForceCursorVisibility( + CVarGetInteger("gSettings.CursorVisibility", 0)); + } }, +#endif + { "Open App Files Folder", + "", + "Opens the folder that contains the save and mods folders, etc.", + WIDGET_BUTTON, + {}, + [](widgetInfo& info) { + std::string filesPath = Ship::Context::GetInstance()->GetAppDirectoryPath(); + SDL_OpenURL(std::string("file:///" + std::filesystem::absolute(filesPath).string()).c_str()); + } }, + { "Search In Sidebar", + "gSettings.SidebarSearch", + "Displays the Search menu as a sidebar entry in Settings instead of in the header.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { + if (CVarGetInteger("gSettings.SidebarSearch", 0)) { + menuEntries[0].sidebarEntries.insert(menuEntries[0].sidebarEntries.begin() + searchSidebarIndex, + searchSidebarEntry); + CVarSetInteger(menuEntries[0].sidebarCvar, CVarGetInteger(menuEntries[0].sidebarCvar, 0) + 1); + } else { + menuEntries[0].sidebarEntries.erase(menuEntries[0].sidebarEntries.begin() + searchSidebarIndex); + CVarSetInteger(menuEntries[0].sidebarCvar, CVarGetInteger(menuEntries[0].sidebarCvar, 0) - 1); + } + } }, + { "Search Input Autofocus", "gSettings.SearchAutofocus", + "Search input box gets autofocus when visible. Does not affect using other widgets.", + WIDGET_CVAR_CHECKBOX }, + } } }); + // Audio Settings + settingsSidebar.push_back( + { "Audio", + 3, + { { { "Master Volume: %.0f%%", + "gSettings.Audio.MasterVolume", + "Adjust overall sound volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true } }, + { "Main Music Volume: %.0f%%", + "gSettings.Audio.MainMusicVolume", + "Adjust the Background Music volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + AudioSeq_SetPortVolumeScale(SEQ_PLAYER_BGM_MAIN, + CVarGetFloat("gSettings.Audio.MainMusicVolume", 1.0f)); + } }, + { "Sub Music Volume: %.0f%%", + "gSettings.Audio.SubMusicVolume", + "Adjust the Sub Music volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + AudioSeq_SetPortVolumeScale(SEQ_PLAYER_BGM_SUB, + CVarGetFloat("gSettings.Audio.SubMusicVolume", 1.0f)); + } }, + { "Sound Effects Volume: %.0f%%", + "gSettings.Audio.SoundEffectsVolume", + "Adjust the Sound Effects volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + AudioSeq_SetPortVolumeScale(SEQ_PLAYER_SFX, + CVarGetFloat("gSettings.Audio.SoundEffectsVolume", 1.0f)); + } }, + { "Fanfare Volume: %.0f%%", + "gSettings.Audio.FanfareVolume", + "Adjust the Fanfare volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + AudioSeq_SetPortVolumeScale(SEQ_PLAYER_FANFARE, + CVarGetFloat("gSettings.Audio.FanfareVolume", 1.0f)); + } }, + { "Ambience Volume: %.0f%%", + "gSettings.Audio.AmbienceVolume", + "Adjust the Ambient Sound volume.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 0.0f, + .max = 100.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + AudioSeq_SetPortVolumeScale(SEQ_PLAYER_AMBIENCE, + CVarGetFloat("gSettings.Audio.AmbienceVolume", 1.0f)); + } }, + { "Audio API", NULL, "Sets the audio API used by the game. Requires a relaunch to take effect.", + WIDGET_AUDIO_BACKEND } } } }); + // Graphics Settings + static int32_t maxFps; + const char* tooltip = ""; + if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { + maxFps = 360; + tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc.\n\nA higher target " + "FPS than your monitor's refresh rate will waste resources, and might give a worse result."; + } else { + maxFps = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); + tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc."; + } + settingsSidebar.push_back( + { "Graphics", + 3, + { { { "Toggle Fullscreen", + "gSettings.Fullscreen", + "Toggles Fullscreen On/Off.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); } }, +#ifndef __APPLE__ + { "Internal Resolution: %.0f%%", + CVAR_INTERNAL_RESOLUTION, + "Multiplies your output resolution by the value inputted, as a more intensive but effective " + "form of anti-aliasing.", + WIDGET_CVAR_SLIDER_FLOAT, + { .min = 50.0f, + .max = 200.0f, + .defaultVariant = 100.0f, + .showButtons = false, + .format = "", + .isPercentage = true }, + [](widgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier( + CVarGetFloat(CVAR_INTERNAL_RESOLUTION, 1)); + } }, +#endif +#ifndef __WIIU__ + { "Anti-aliasing (MSAA): %d", + CVAR_MSAA_VALUE, + "Activates MSAA (multi-sample anti-aliasing) from 2x up to 8x, to smooth the edges of rendered " + "geometry.\n" + "Higher sample count will result in smoother edges on models, but may reduce performance.", + WIDGET_CVAR_SLIDER_INT, + { 1, 8, 1 }, + [](widgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger(CVAR_MSAA_VALUE, 1)); + } }, +#endif + + { "Current FPS: %d", + "gInterpolationFPS", + tooltip, + WIDGET_CVAR_SLIDER_INT, + { 20, maxFps, 20 }, + [](widgetInfo& info) { + int32_t defaultVariant = std::get(info.widgetOptions.defaultVariant); + if (CVarGetInteger(info.widgetCVar, defaultVariant) == defaultVariant) { + info.widgetName = "Current FPS: Original (%d)"; + } else { + info.widgetName = "Current FPS: %d"; + } + }, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_MATCH_REFRESH_RATE_ON).active) + info.activeDisables.push_back(DISABLE_FOR_MATCH_REFRESH_RATE_ON); + } }, + { "Match Refresh Rate", + "", + "Matches interpolation value to the current game's window refresh rate.", + WIDGET_BUTTON, + {}, + [](widgetInfo& info) { + int hz = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); + if (hz >= 20 && hz <= 360) { + CVarSetInteger("gInterpolationFPS", hz); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NOT_DIRECTX).active; } }, + { "Match Refresh Rate", + "gMatchRefreshRate", + "Matches interpolation value to the current game's window refresh rate.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DIRECTX).active; } }, + { "Jitter fix : >= % d FPS", + "gExtraLatencyThreshold", + "When Interpolation FPS setting is at least this threshold, add one frame of input " + "lag (e.g. 16.6 ms for 60 FPS) in order to avoid jitter. This setting allows the " + "CPU to work on one frame while GPU works on the previous frame.\nThis setting " + "should be used when your computer is too slow to do CPU + GPU work in time.", + WIDGET_CVAR_SLIDER_INT, + { 0, 360, 80 }, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NOT_DIRECTX).active; } }, + { "Renderer API (Needs reload)", NULL, "Sets the renderer API used by the game.", WIDGET_VIDEO_BACKEND }, + { "Enable Vsync", + CVAR_VSYNC_ENABLED, + "Enables Vsync.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NO_VSYNC).active; } }, + { "Windowed Fullscreen", + CVAR_SDL_WINDOWED_FULLSCREEN, + "Enables Windowed Fullscreen Mode.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NO_WINDOWED_FULLSCREEN).active; } }, + { "Allow multi-windows", + CVAR_ENABLE_MULTI_VIEWPORTS, + "Allows multiple windows to be opened at once. Requires a reload to take effect.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NO_MULTI_VIEWPORT).active; } }, + { "Texture Filter (Needs reload)", + CVAR_TEXTURE_FILTER, + "Sets the applied Texture Filtering.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = textureFilteringMap } } } } }); + // Input Editor + settingsSidebar.push_back({ "Input Editor", + 1, + { { { "Popout Input Editor", + "gWindows.BenInputEditor", + "Enables the separate Input Editor window.", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "2S2H Input Editor" } } } } }); +} +int32_t motionBlurStrength; + +void AddEnhancements() { + // Camera Snap Fix + enhancementsSidebar.push_back( + { "Camera", + 3, + { { { .widgetName = "Fixes", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Fix Targetting Camera Snap", + "gEnhancements.Camera.FixTargettingCameraSnap", + "Fixes the camera snap that occurs when you are moving and press the targetting button.", + WIDGET_CVAR_CHECKBOX, + {} } }, + // Camera Enhancements + { { .widgetName = "Cameras", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Free Look", + "gEnhancements.Camera.FreeLook.Enable", + "Enables free look camera control\nNote: You must remap C buttons off of the right " + "stick in the controller config menu, and map the camera stick to the right stick.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterCameraFreeLook(); }, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_DEBUG_CAM_ON).active) + info.activeDisables.push_back(DISABLE_FOR_DEBUG_CAM_ON); + } }, + { "Camera Distance: %d", + "gEnhancements.Camera.FreeLook.MaxCameraDistance", + "Maximum Camera Distance for Free Look.", + WIDGET_CVAR_SLIDER_INT, + { 100, 900, 185 }, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_FREE_LOOK_OFF).active; } }, + { "Camera Transition Speed: %d", + "gEnhancements.Camera.FreeLook.TransitionSpeed", + "Can someone help me?", + WIDGET_CVAR_SLIDER_INT, + { 1, 900, 25 }, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_FREE_LOOK_OFF).active; } }, + { "Max Camera Height Angle: %.0f\xC2\xB0", + "gEnhancements.Camera.FreeLook.MaxPitch", + "Maximum Height of the Camera.", + WIDGET_CVAR_SLIDER_FLOAT, + { -8900.0f, 8900.0f, 7200.0f }, + [](widgetInfo& info) { FreeLookPitchMinMax(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_FREE_LOOK_OFF).active; } }, + { "Min Camera Height Angle: %.0f\xC2\xB0", + "gEnhancements.Camera.FreeLook.MinPitch", + "Minimum Height of the Camera.", + WIDGET_CVAR_SLIDER_FLOAT, + { -8900.0f, 8900.0f, -4900.0f }, + [](widgetInfo& info) { FreeLookPitchMinMax(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_FREE_LOOK_OFF).active; } }, + { "Debug Camera", + "gEnhancements.Camera.DebugCam.Enable", + "Enables free camera control.", + WIDGET_CVAR_CHECKBOX, + {}, + ([](widgetInfo& info) { RegisterDebugCam(); }), + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_FREE_LOOK_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_FREE_LOOK_ON); + } + } }, + { "Invert Camera X Axis", + "gEnhancements.Camera.RightStick.InvertXAxis", + "Inverts the Camera X Axis", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_CAMERAS_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_CAMERAS_OFF); + } + } }, + { "Invert Camera Y Axis", + "gEnhancements.Camera.RightStick.InvertYAxis", + "Inverts the Camera Y Axis", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_CAMERAS_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_CAMERAS_OFF); + } + } }, + { "Third-Person Horizontal Sensitivity: %.0f", + "gEnhancements.Camera.RightStick.CameraSensitivity.X", + "Adjust the Sensitivity of the x axis when in Third Person.", + WIDGET_CVAR_SLIDER_FLOAT, + { 1.0f, 500.0f, 100.0f }, + nullptr, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_CAMERAS_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_CAMERAS_OFF); + } + } }, + { "Third-Person Vertical Sensitivity: %.0f", + "gEnhancements.Camera.RightStick.CameraSensitivity.Y", + "Adjust the Sensitivity of the x axis when in Third Person.", + WIDGET_CVAR_SLIDER_FLOAT, + { 1.0f, 500.0f, 100.0f }, + nullptr, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_CAMERAS_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_CAMERAS_OFF); + } + } }, + { "Enable Roll (6\xC2\xB0 of Freedom)", + "gEnhancements.Camera.DebugCam.6DOF", + "This allows for all six degrees of movement with the camera, NOTE: Yaw will work " + "differently in this system, instead rotating around the focal point" + ", rather than a polar axis.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_CAM_OFF).active; } }, + { "Camera Speed: %.0f", + "gEnhancements.Camera.DebugCam.CameraSpeed", + "Adjusts the speed of the Camera.", + WIDGET_CVAR_SLIDER_FLOAT, + { 10.0f, 300.0f, 50.0f }, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_CAM_OFF).active; } } } } }); + // Cheats + enhancementsSidebar.push_back( + { "Cheats", + 3, + { { { "Infinite Health", "gCheats.InfiniteHealth", "Always have full Hearts.", WIDGET_CVAR_CHECKBOX, {} }, + { "Infinite Magic", "gCheats.InfiniteMagic", "Always have full Magic.", WIDGET_CVAR_CHECKBOX, {} }, + { "Infinite Rupees", "gCheats.InfiniteRupees", "Always have a full Wallet.", WIDGET_CVAR_CHECKBOX, {} }, + { "Infinite Consumables", "gCheats.InfiniteConsumables", + "Always have max Consumables, you must have collected the consumables first.", WIDGET_CVAR_CHECKBOX }, + { "Longer Deku Flower Glide", + "gCheats.LongerFlowerGlide", + "Allows Deku Link to glide longer, no longer dropping after a certain distance.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterLongerFlowerGlide(); } }, + { "No Clip", "gCheats.NoClip", "Allows Link to phase through collision.", WIDGET_CVAR_CHECKBOX }, + { "Unbreakable Razor Sword", "gCheats.UnbreakableRazorSword", + "Allows to Razor Sword to be used indefinitely without dulling its blade.", WIDGET_CVAR_CHECKBOX }, + { "Unrestricted Items", "gCheats.UnrestrictedItems", "Allows all Forms to use all Items.", + WIDGET_CVAR_CHECKBOX }, + { "Moon Jump on L", + "gCheats.MoonJumpOnL", + "Holding L makes you float into the air.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterMoonJumpOnL(); } }, + { "Stop Time in Dungeons", + "gCheats.TempleTimeStop", + "Stops time from advancing in selected areas. Requires a room change to update.\n\n" + "- Off: Vanilla behaviour.\n" + "- Temples: Stops time in Woodfall, Snowhead, Great Bay, and Stone Tower Temples.\n" + "- Temples + Mini Dungeons: In addition to the above temples, stops time in both Spider " + "Houses, Pirate's Fortress, Beneath the Well, Ancient Castle of Ikana, and Secret Shrine.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = timeStopOptions } } } } }); + // Gameplay Enhancements + enhancementsSidebar.push_back( + { "Gameplay", + 3, + { { { .widgetName = "Player", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Fast Deku Flower Launch", + "gEnhancements.Player.FastFlowerLaunch", + "Speeds up the time it takes to be able to get maximum height from launching out of a deku flower", + WIDGET_CVAR_CHECKBOX, + {}, + ([](widgetInfo& info) { RegisterFastFlowerLaunch(); }) }, + { "Instant Putaway", "gEnhancements.Player.InstantPutaway", + "Allows Link to instantly puts away held item without waiting.", WIDGET_CVAR_CHECKBOX }, + { "Climb speed", + "gEnhancements.PlayerMovement.ClimbSpeed", + "Increases the speed at which Link climbs vines and ladders.", + WIDGET_CVAR_SLIDER_INT, + { 1, 5, 1 } }, + { "Dpad Equips", "gEnhancements.Dpad.DpadEquips", "Allows you to equip items to your d-pad", + WIDGET_CVAR_CHECKBOX }, + { "Always Win Doggy Race", + "gEnhancements.Minigames.AlwaysWinDoggyRace", + "Makes the Doggy Race easier to win.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = alwaysWinDoggyraceOptions } }, + { "Fast Magic Arrow Equip Animation", "gEnhancements.Equipment.MagicArrowEquipSpeed", + "Removes the animation for equipping Magic Arrows.", WIDGET_CVAR_CHECKBOX }, + { "Instant Fin Boomerangs Recall", "gEnhancements.PlayerActions.InstantRecall", + "Pressing B will instantly recall the fin boomerang back to Zora Link after they are thrown.", + WIDGET_CVAR_CHECKBOX } }, + { { .widgetName = "Modes", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Play as Kafei", "gModes.PlayAsKafei", "Requires scene reload to take effect.", WIDGET_CVAR_CHECKBOX }, + { "Time Moves when you Move", + "gModes.TimeMovesWhenYouMove", + "Time only moves when Link is not standing still.", + WIDGET_CVAR_CHECKBOX, + {}, + ([](widgetInfo& info) { RegisterTimeMovesWhenYouMove(); }) } }, + { { .widgetName = "Saving", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Persistent Owl Saves", "gEnhancements.Saving.PersistentOwlSaves", + "Continuing a save will not remove the owl save. Playing Song of " + "Time, allowing the moon to crash or finishing the " + "game will remove the owl save and become the new last save.", + WIDGET_CVAR_CHECKBOX }, + { "Pause Menu Save", "gEnhancements.Saving.PauseSave", + "Re-introduce the pause menu save system. Pressing B in the pause menu will give you the " + "option to create a persistent Owl Save from your current location.\n\nWhen loading back " + "into the game, you will be placed either at the entrance of the dungeon you saved in, or " + "in South Clock Town.", + WIDGET_CVAR_CHECKBOX }, + { "Autosave", + "gEnhancements.Saving.Autosave", + "Automatically create a persistent Owl Save on the chosen interval.\n\nWhen loading " + "back into the game, you will be placed either at the entrance of the dungeon you " + "saved in, or in South Clock Town.", + WIDGET_CVAR_CHECKBOX, + {}, + ([](widgetInfo& info) { RegisterAutosave(); }) }, + { "Autosave Interval: %d minutes", + "gEnhancements.Saving.AutosaveInterval", + "Sets the interval between Autosaves.", + WIDGET_CVAR_SLIDER_INT, + { 1, 60, 5 }, + nullptr, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_AUTO_SAVE_OFF).active) { + info.activeDisables.push_back(DISABLE_FOR_AUTO_SAVE_OFF); + } + } }, + { .widgetName = "Time Cycle", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Do not reset Bottle content", "gEnhancements.Cycle.DoNotResetBottleContent", + "Playing the Song Of Time will not reset the bottles' content.", WIDGET_CVAR_CHECKBOX }, + { "Do not reset Consumables", "gEnhancements.Cycle.DoNotResetConsumables", + "Playing the Song Of Time will not reset the consumables.", WIDGET_CVAR_CHECKBOX }, + { "Do not reset Razor Sword", "gEnhancements.Cycle.DoNotResetRazorSword", + "Playing the Song Of Time will not reset the Sword back to Kokiri Sword.", WIDGET_CVAR_CHECKBOX }, + { "Do not reset Rupees", "gEnhancements.Cycle.DoNotResetRupees", + "Playing the Song Of Time will not reset the your rupees.", WIDGET_CVAR_CHECKBOX }, + { .widgetName = "Unstable", + .widgetType = WIDGET_SEPARATOR_TEXT, + .widgetOptions = { .color = UIWidgets::Colors::Yellow } }, + { "Disable Save Delay", "gEnhancements.Saving.DisableSaveDelay", + "Removes the arbitrary 2 second timer for saving from the original game. This is known to " + "cause issues when attempting the 0th Day Glitch", + WIDGET_CVAR_CHECKBOX } } } }); + // Graphics Enhancements + enhancementsSidebar.push_back( + { "Graphics", + 3, + { { { .widgetName = "Clock", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Clock Type", + "gEnhancements.Graphics.ClockType", + "Swaps between Graphical and Text only Clock types.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = clockTypeOptions } }, + { "24 Hours Clock", "gEnhancements.Graphics.24HoursClock", "Changes from a 12 Hour to a 24 Hour Clock", + WIDGET_CVAR_CHECKBOX }, + { .widgetName = "Motion Blur", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Motion Blur Mode", + "gEnhancements.Graphics.MotionBlur.Mode", + "Selects the Mode for Motion Blur.", + WIDGET_CVAR_COMBOBOX, + { .comboBoxOptions = motionBlurOptions, .labelPosition = UIWidgets::LabelPosition::None } }, + { "Interpolate", + "gEnhancements.Graphics.MotionBlur.Interpolate", + "Change motion blur capture to also happen on interpolated frames instead of only on game frames.\n" + "This notably reduces the overall motion blur strength but smooths out the trails.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_MOTION_BLUR_MODE).value == 1; } }, + { "On/Off", + "", + "Enables Motion Blur.", + WIDGET_CHECKBOX, + { .valuePointer = (bool*)&R_MOTION_BLUR_ENABLED }, + nullptr, + [](widgetInfo& info) { + info.widgetOptions.valuePointer = (bool*)&R_MOTION_BLUR_ENABLED; + info.isHidden = disabledMap.at(DISABLE_FOR_MOTION_BLUR_MODE).value != 0; + } }, + { "Strength", + "gEnhancements.Graphics.MotionBlur.Strength", + "Motion Blur strength.", + WIDGET_CVAR_SLIDER_INT, + { 0, 255, 180 }, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_MOTION_BLUR_MODE).value != 2; } }, + { "Strength", + "", + "Motion Blur strength.", + WIDGET_SLIDER_INT, + { 0, 255, 180, {}, &motionBlurStrength }, + [](widgetInfo& info) { R_MOTION_BLUR_ALPHA = motionBlurStrength; }, + [](widgetInfo& info) { + motionBlurStrength = R_MOTION_BLUR_ALPHA; + info.isHidden = disabledMap.at(DISABLE_FOR_MOTION_BLUR_MODE).value != 0 || + disabledMap.at(DISABLE_FOR_MOTION_BLUR_OFF).active; + } }, + { .widgetName = "Other", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "3D Item Drops", + "gEnhancements.Graphics.3DItemDrops", + "Makes item drops 3D", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { Register3DItemDrops(); } }, + { "Authentic Logo", "gEnhancements.Graphics.AuthenticLogo", + "Hide the game version and build details and display the authentic " + "model and texture on the boot logo start screen", + WIDGET_CVAR_CHECKBOX }, + { "Bow Reticle", "gEnhancements.Graphics.BowReticle", "Gives the bow a reticle when you draw an arrow.", + WIDGET_CVAR_CHECKBOX }, + { "Disable Black Bar Letterboxes", "gEnhancements.Graphics.DisableBlackBars", + "Disables Black Bar Letterboxes during cutscenes and Z-targeting\nNote: there may be " + "minor visual glitches that were covered up by the black bars\nPlease disable this " + "setting before reporting a bug.", + WIDGET_CVAR_CHECKBOX }, + { .widgetName = "Unstable", + .widgetType = WIDGET_SEPARATOR_TEXT, + .widgetOptions = { .color = UIWidgets::Colors::Yellow } }, + { "Disable Scene Geometry Distance Check", "gEnhancements.Graphics.DisableSceneGeometryDistanceCheck", + "Disables the distance check for scene geometry, allowing it to be drawn no matter how far " + "away it is from the player. This may have unintended side effects.", + WIDGET_CVAR_CHECKBOX }, + { "Widescreen Actor Culling", "gEnhancements.Graphics.ActorCullingAccountsForWidescreen", + "Adjusts the culling planes to account for widescreen resolutions. " + "This may have unintended side effects.", + WIDGET_CVAR_CHECKBOX }, + { "Increase Actor Draw Distance: %dx", + "gEnhancements.Graphics.IncreaseActorDrawDistance", + "Increase the range in which Actors are drawn. This may have unintended side effects.", + WIDGET_CVAR_SLIDER_INT, + { 1, 5, 1 }, + [](widgetInfo& info) { + CVarSetInteger("gEnhancements.Graphics.IncreaseActorUpdateDistance", + MIN(CVarGetInteger("gEnhancements.Graphics.IncreaseActorDrawDistance", 1), + CVarGetInteger("gEnhancements.Graphics.IncreaseActorUpdateDistance", 1))); + } }, + { "Increase Actor Update Distance: %dx", + "gEnhancements.Graphics.IncreaseActorUpdateDistance", + "Increase the range in which Actors are updated. This may have unintended side effects.", + WIDGET_CVAR_SLIDER_INT, + { 1, 5, 1 }, + [](widgetInfo& info) { + CVarSetInteger("gEnhancements.Graphics.IncreaseActorDrawDistance", + MAX(CVarGetInteger("gEnhancements.Graphics.IncreaseActorDrawDistance", 1), + CVarGetInteger("gEnhancements.Graphics.IncreaseActorUpdateDistance", 1))); + } } } } }); + enhancementsSidebar.push_back( + { "Items/Songs", + 3, + { // Mask Enhancements + { { .widgetName = "Masks", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Blast Mask has Powder Keg Force", "gEnhancements.Masks.BlastMaskKeg", + "Blast Mask can also destroy objects only the Powder Keg can.", WIDGET_CVAR_CHECKBOX }, + { "Fast Transformation", "gEnhancements.Masks.FastTransformation", + "Removes the delay when using transormation masks.", WIDGET_CVAR_CHECKBOX }, + { "Fierce Deity's Mask Anywhere", "gEnhancements.Masks.FierceDeitysAnywhere", + "Allow using Fierce Deity's mask outside of boss rooms.", WIDGET_CVAR_CHECKBOX }, + { "Persistent Bunny Hood", + "gEnhancements.Masks.PersistentBunnyHood.Enabled", + "Permanantly toggle a speed boost from the bunny hood by pressing " + "'A' on it in the mask menu.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { UpdatePersistentMasksState(); } }, + { "No Blast Mask Cooldown", "gEnhancements.Masks.NoBlastMaskCooldown", + "Eliminates the Cooldown between Blast Mask usage.", WIDGET_CVAR_CHECKBOX } }, + // Song Enhancements + { { .widgetName = "Ocarina", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Enable Sun's Song", "gEnhancements.Songs.EnableSunsSong", + "Enables the partially implemented Sun's Song. RIGHT-DOWN-UP-RIGHT-DOWN-UP to play it. " + "This song will make time move very fast until either Link moves to a different scene, " + "or when the time switches to a new time period.", + WIDGET_CVAR_CHECKBOX }, + { "Dpad Ocarina", "gEnhancements.Playback.DpadOcarina", "Enables using the Dpad for Ocarina playback.", + WIDGET_CVAR_CHECKBOX }, + { "Pause Owl Warp", "gEnhancements.Songs.PauseOwlWarp", + "Allows the player to use the pause menu map to owl warp instead of " + "having to play the Song of Soaring.", + WIDGET_CVAR_CHECKBOX }, + { "Zora Eggs For Bossa Nova", + "gEnhancements.Songs.ZoraEggCount", + "The number of eggs required to unlock new wave bossa nova.", + WIDGET_CVAR_SLIDER_INT, + { 1, 7, 7 } }, + { "Prevent Dropped Ocarina Inputs", "gEnhancements.Playback.NoDropOcarinaInput", + "Prevent dropping inputs when playing the ocarina quickly.", WIDGET_CVAR_CHECKBOX } } } }); + enhancementsSidebar.push_back( + { "Time Savers", + 3, + { // Cutscene Skips + { { .widgetName = "Cutscenes", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Hide Title Cards", "gEnhancements.Cutscenes.HideTitleCards", "Hides Title Cards when entering areas.", + WIDGET_CVAR_CHECKBOX }, + { "Skip Entrance Cutscenes", "gEnhancements.Cutscenes.SkipEntranceCutscenes", + "Skip cutscenes that occur when first entering a new area.", WIDGET_CVAR_CHECKBOX }, + { "Skip to File Select", "gEnhancements.Cutscenes.SkipToFileSelect", + "Skip the opening title sequence and go straight to the file select menu after boot.", + WIDGET_CVAR_CHECKBOX }, + { "Skip Intro Sequence", "gEnhancements.Cutscenes.SkipIntroSequence", + "When starting a game you will be taken straight to South Clock Town as Deku Link.", + WIDGET_CVAR_CHECKBOX }, + { "Skip Story Cutscenes", "gEnhancements.Cutscenes.SkipStoryCutscenes", + "Disclaimer: This doesn't do much yet, we will be progressively adding more skips over time.", + WIDGET_CVAR_CHECKBOX }, + { "Skip Misc Interactions", "gEnhancements.Cutscenes.SkipMiscInteractions", + "Disclaimer: This doesn't do much yet, we will be progressively adding more skips over time.", + WIDGET_CVAR_CHECKBOX } }, + // Dialogue Enhancements + { { .widgetName = "Dialogue", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Fast Bank Selection", "gEnhancements.Dialogue.FastBankSelection", + "Pressing the Z or R buttons while the Deposit/Withdrawl Rupees dialogue is open will set " + "the Rupees to Links current Rupees or 0 respectively.", + WIDGET_CVAR_CHECKBOX }, + { "Fast Text", "gEnhancements.Dialogue.FastText", + "Speeds up text rendering, and enables holding of B progress to next message.", + WIDGET_CVAR_CHECKBOX } } } }); + enhancementsSidebar.push_back( + { "Fixes", + 3, + { // Fixes + { { .widgetName = "Fixes", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Fix Ammo Count Color", "gFixes.FixAmmoCountEnvColor", + "Fixes a missing gDPSetEnvColor, which causes the ammo count to be " + "the wrong color prior to obtaining magic or other conditions.", + WIDGET_CVAR_CHECKBOX }, + { "Fix Fierce Deity Z-Target movement", "gEnhancements.Fixes.FierceDeityZTargetMovement", + "Fixes Fierce Deity movement being choppy when Z-targeting", WIDGET_CVAR_CHECKBOX }, + { "Fix Hess and Weirdshot Crash", "gEnhancements.Fixes.HessCrash", + "Fixes a crash that can occur when performing a HESS or Weirdshot.", WIDGET_CVAR_CHECKBOX }, + { "Fix Text Control Characters", "gEnhancements.Fixes.ControlCharacters", + "Fixes certain control characters not functioning properly " + "depending on their position within the text.", + WIDGET_CVAR_CHECKBOX } } } }); + enhancementsSidebar.push_back( + { "Restorations", + 3, + { // Restorations + { { .widgetName = "Restorations", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Constant Distance Backflips and Sidehops", "gEnhancements.Restorations.ConstantFlipsHops", + "Backflips and Sidehops travel a constant distance as they did in OoT.", WIDGET_CVAR_CHECKBOX }, + { "Power Crouch Stab", "gEnhancements.Restorations.PowerCrouchStab", + "Crouch stabs will use the power of Link's previous melee attack, as is in MM JP 1.0 and OoT.", + WIDGET_CVAR_CHECKBOX }, + { "Side Rolls", "gEnhancements.Restorations.SideRoll", "Restores side rolling from OoT.", + WIDGET_CVAR_CHECKBOX }, + { "Tatl ISG", "gEnhancements.Restorations.TatlISG", "Restores Navi ISG from OoT, but now with Tatl.", + WIDGET_CVAR_CHECKBOX }, + { "Woodfall Mountain Appearance", + "gEnhancements.Restorations.WoodfallMountainAppearance", + "Restores the appearance of Woodfall mountain to not look poisoned " + "when viewed from Termina Field after clearing Woodfall Temple\n\n" + "Requires a scene reload to take effect", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterWoodfallMountainAppearance(); } } } } }); + enhancementsSidebar.push_back({ "HUD Editor", + 1, + { // HUD Editor + { { "Popout HUD Editor", + "gWindows.HudEditor", + "Enables the HUD Editor window, allowing you to modify your HUD", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "HUD Editor" } } } } }); +} + +void AddDevTools() { + devToolsSidebar.push_back( + { "General", + 3, + { { { "Popout Menu", "gSettings.Menu.Popout", "Changes the menu display from overlay to windowed.", + WIDGET_CVAR_CHECKBOX }, + { "Set Warp Point", + "", + "Creates warp point that you can teleport to later", + WIDGET_BUTTON, + {}, + [](widgetInfo& info) { + Player* player = GET_PLAYER(gPlayState); + + CVarSetInteger(WARP_POINT_CVAR "Entrance", gSaveContext.save.entrance); + CVarSetInteger(WARP_POINT_CVAR "Room", gPlayState->roomCtx.curRoom.num); + CVarSetFloat(WARP_POINT_CVAR "X", player->actor.world.pos.x); + CVarSetFloat(WARP_POINT_CVAR "Y", player->actor.world.pos.y); + CVarSetFloat(WARP_POINT_CVAR "Z", player->actor.world.pos.z); + CVarSetFloat(WARP_POINT_CVAR "Rotation", player->actor.shape.rot.y); + CVarSetInteger(WARP_POINT_CVAR "Saved", 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_NULL_PLAY_STATE).active; } }, + { "Scene Room ID", + "", + "", + WIDGET_TEXT, + {}, + nullptr, + [](widgetInfo& info) { + u32 sceneId = Entrance_GetSceneIdAbsolute( + CVarGetInteger(WARP_POINT_CVAR "Entrance", ENTRANCE(SOUTH_CLOCK_TOWN, 0))); + info.widgetName = fmt::format("{} Room {}", warpPointSceneList[sceneId], + CVarGetInteger(WARP_POINT_CVAR "Room", 0)); + info.isHidden = disabledMap.at(DISABLE_FOR_NULL_PLAY_STATE).active || + disabledMap.at(DISABLE_FOR_WARP_POINT_NOT_SET).active; + } }, + { ICON_FA_TIMES, + "", + "Clear warp point", + WIDGET_BUTTON, + { .size = UIWidgets::Sizes::Inline, .sameLine = true }, + [](widgetInfo& info) { + CVarClear(WARP_POINT_CVAR "Entrance"); + CVarClear(WARP_POINT_CVAR "Room"); + CVarClear(WARP_POINT_CVAR "X"); + CVarClear(WARP_POINT_CVAR "Y"); + CVarClear(WARP_POINT_CVAR "Z"); + CVarClear(WARP_POINT_CVAR "Rotation"); + CVarClear(WARP_POINT_CVAR "Saved"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + }, + [](widgetInfo& info) { + info.isHidden = disabledMap.at(DISABLE_FOR_NULL_PLAY_STATE).active || + disabledMap.at(DISABLE_FOR_WARP_POINT_NOT_SET).active; + } }, + { "Warp", + "", + "Teleport to the set warp point", + WIDGET_BUTTON, + { .size = UIWidgets::Sizes::Inline, .sameLine = true }, + [](widgetInfo& info) { Warp(); }, + [](widgetInfo& info) { + info.isHidden = disabledMap.at(DISABLE_FOR_NULL_PLAY_STATE).active || + disabledMap.at(DISABLE_FOR_WARP_POINT_NOT_SET).active; + } } }, + { { "Debug Mode", + "gDeveloperTools.DebugEnabled", + "Enables Debug Mode, allowing you to select maps with L + R + Z.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { + if (!CVarGetInteger("gDeveloperTools.DebugEnabled", 0)) { + CVarClear("gDeveloperTools.DebugSaveFileMode"); + CVarClear("gDeveloperTools.PreventActorUpdate"); + CVarClear("gDeveloperTools.PreventActorDraw"); + CVarClear("gDeveloperTools.PreventActorInit"); + CVarClear("gDeveloperTools.DisableObjectDependency"); + if (gPlayState != NULL) { + gPlayState->frameAdvCtx.enabled = false; + } + RegisterDebugSaveCreate(); + RegisterPreventActorUpdateHooks(); + RegisterPreventActorDrawHooks(); + RegisterPreventActorInitHooks(); + } + } }, + { "Better Map Select", + "gDeveloperTools.BetterMapSelect.Enabled", + "Overrides the original map select with a translated, more user-friendly version.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Debug Save File Mode", + "gDeveloperTools.DebugSaveFileMode", + "Change the behavior of creating saves while debug mode is enabled:\n\n" + "- Empty Save: The default 3 heart save file in first cycle\n" + "- Vanilla Debug Save: Uses the title screen save info (8 hearts, all items and masks)\n" + "- 100\% Save: All items, equipment, mask, quast status and bombers notebook complete", + WIDGET_CVAR_COMBOBOX, + { 0, 0, 0, debugSaveOptions }, + [](widgetInfo& info) { RegisterDebugSaveCreate(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Prevent Actor Update", + "gDeveloperTools.PreventActorUpdate", + "Prevents Actors from updating.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterPreventActorUpdateHooks(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Prevent Actor Draw", + "gDeveloperTools.PreventActorDraw", + "Prevents Actors from drawing.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterPreventActorDrawHooks(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Prevent Actor Init", + "gDeveloperTools.PreventActorInit", + "Prevents Actors from initializing.", + WIDGET_CVAR_CHECKBOX, + {}, + [](widgetInfo& info) { RegisterPreventActorInitHooks(); }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Disable Object Dependency", + "gDeveloperTools.DisableObjectDependency", + "Disables dependencies when loading objects.", + WIDGET_CVAR_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Log Level", + "gDeveloperTools.LogLevel", + "The log level determines which messages are printed to the " + "console. This does not affect the log file output", + WIDGET_CVAR_COMBOBOX, + { 0, 0, 0, logLevels }, + [](widgetInfo& info) { + Ship::Context::GetInstance()->GetLogger()->set_level( + (spdlog::level::level_enum)CVarGetInteger("gDeveloperTools.LogLevel", 1)); + }, + [](widgetInfo& info) { info.isHidden = disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; } }, + { "Frame Advance", + "", + "This allows you to advance through the game one frame at a time on command. " + "To advance a frame, hold Z and tap R on the second controller. Holding Z " + "and R will advance a frame every half second. You can also use the buttons below.", + WIDGET_CHECKBOX, + {}, + nullptr, + [](widgetInfo& info) { + info.isHidden = disabledMap.at(DISABLE_FOR_NULL_PLAY_STATE).active || + disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; + if (gPlayState != nullptr) { + info.widgetOptions.valuePointer = (bool*)&gPlayState->frameAdvCtx.enabled; + } else { + info.widgetOptions.valuePointer = (bool*)nullptr; + } + } }, + { "Advance 1", + "", + "Advance 1 frame.", + WIDGET_BUTTON, + { .size = UIWidgets::Sizes::Inline }, + [](widgetInfo& info) { CVarSetInteger("gDeveloperTools.FrameAdvanceTick", 1); }, + [](widgetInfo& info) { + info.isHidden = disabledMap.at(DISABLE_FOR_FRAME_ADVANCE_OFF).active || + disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; + } }, + { "Advance (Hold)", + "", + "Advance frames while the button is held.", + WIDGET_BUTTON, + { .size = UIWidgets::Sizes::Inline, .sameLine = true }, + nullptr, + [](widgetInfo& info) { + info.isHidden = disabledMap.at(DISABLE_FOR_FRAME_ADVANCE_OFF).active || + disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; + }, + [](widgetInfo& info) { + if (ImGui::IsItemActive()) { + CVarSetInteger("gDeveloperTools.FrameAdvanceTick", 1); + } + } } } } }); + // dev tools windows + devToolsSidebar.push_back({ "Collision Viewer", + 1, + { { { "Popout Collision Viewer", + "gWindows.CollisionViewer", + "Makes collision visible on screen", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Collision Viewer" } } } } }); + devToolsSidebar.push_back( + { "Stats", + 1, + { { { "Popout Stats", + "gOpenWindows.Stats", + "Shows the stats window, with your FPS and frametimes, and the OS you're playing on", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Stats" } } } } }); + devToolsSidebar.push_back( + { "Console", + 1, + { { { "Popout Console", + "gOpenWindows.Console", + "Enables the console window, allowing you to input commands. Type help for some examples", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Console" } } } } }); + devToolsSidebar.push_back( + { "Gfx Debugger", + 1, + { { { "Popout Gfx Debugger", + "gOpenWindows.GfxDebugger", + "Enables the Gfx Debugger window, allowing you to input commands, type help for some examples", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "GfxDebuggerWindow" } } } } }); + devToolsSidebar.push_back({ "Save Editor", + 1, + { { { "Popout Save Editor", + "gWindows.SaveEditor", + "Enables the Save Editor window, allowing you to edit your save file", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Save Editor" } } } } }); + devToolsSidebar.push_back({ "Actor Viewer", + 1, + { { { "Popout Actor Viewer", + "gWindows.ActorViewer", + "Enables the Actor Viewer window, allowing you to view actors in the world.", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Actor Viewer" } } } } }); + devToolsSidebar.push_back({ "Event Log", + 1, + { { { "Popout Event Log", + "gWindows.EventLog", + "Enables the event log window", + WIDGET_WINDOW_BUTTON, + { .size = UIWidgets::Sizes::Inline, .windowName = "Event Log" } } } } }); +} + +void SearchMenuGetItem(widgetInfo& widget) { + disabledTempTooltip = "This setting is disabled because: \n\n"; + disabledValue = false; + disabledTooltip = " "; + + if (widget.preFunc != nullptr) { + widget.activeDisables.clear(); + widget.isHidden = false; + widget.preFunc(widget); + if (widget.isHidden) { + return; + } + if (!widget.activeDisables.empty()) { + disabledValue = true; + for (auto option : widget.activeDisables) { + disabledTempTooltip += std::string("- ") + disabledMap.at(option).reason + std::string("\n"); + } + disabledTooltip = disabledTempTooltip.c_str(); + } + } + + if (widget.widgetOptions.sameLine) { + ImGui::SameLine(); + } + + try { + switch (widget.widgetType) { + case WIDGET_CHECKBOX: { + bool* pointer = std::get(widget.widgetOptions.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Checkbox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + if (UIWidgets::Checkbox( + widget.widgetName.c_str(), pointer, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .labelPosition = widget.widgetOptions.labelPosition == UIWidgets::LabelPosition::None + ? UIWidgets::LabelPosition::Near + : widget.widgetOptions.labelPosition })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + } break; + case WIDGET_CVAR_CHECKBOX: + if (UIWidgets::CVarCheckbox( + widget.widgetName.c_str(), widget.widgetCVar, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .defaultValue = static_cast(std::get(widget.widgetOptions.defaultVariant)), + .labelPosition = widget.widgetOptions.labelPosition == UIWidgets::LabelPosition::None + ? UIWidgets::LabelPosition::Near + : widget.widgetOptions.labelPosition })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + }; + break; + case WIDGET_AUDIO_BACKEND: { + auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetAudioBackend(); + if (UIWidgets::Combobox( + "Audio API", ¤tAudioBackend, audioBackendsMap, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = + Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1, + .disabledTooltip = "Only one audio API is available on this platform." })) { + Ship::Context::GetInstance()->GetAudio()->SetAudioBackend(currentAudioBackend); + } + } break; + case WIDGET_VIDEO_BACKEND: { + if (UIWidgets::Combobox( + "Renderer API (Needs reload)", &configWindowBackend, availableWindowBackendsMap, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = availableWindowBackends->size() <= 1, + .disabledTooltip = "Only one renderer API is available on this platform." })) { + Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", + static_cast(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", + windowBackendsMap.at(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->Save(); + UpdateWindowBackendObjects(); + } + } break; + case WIDGET_SEPARATOR: + ImGui::Separator(); + break; + case WIDGET_SEPARATOR_TEXT: + if (widget.widgetOptions.color != COLOR_NONE) { + ImGui::PushStyleColor(ImGuiCol_Text, widget.widgetOptions.color); + } + ImGui::SeparatorText(widget.widgetName.c_str()); + if (widget.widgetOptions.color != COLOR_NONE) { + ImGui::PopStyleColor(); + } + break; + case WIDGET_TEXT: + ImGui::AlignTextToFramePadding(); + ImGui::Text(widget.widgetName.c_str()); + break; + case WIDGET_COMBOBOX: { + int32_t* pointer = std::get(widget.widgetOptions.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Combobox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + if (UIWidgets::Combobox( + widget.widgetName.c_str(), pointer, widget.widgetOptions.comboBoxOptions, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .labelPosition = widget.widgetOptions.labelPosition == UIWidgets::LabelPosition::None + ? UIWidgets::LabelPosition::Above + : widget.widgetOptions.labelPosition })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + }; + } break; + case WIDGET_CVAR_COMBOBOX: + if (UIWidgets::CVarCombobox( + widget.widgetName.c_str(), widget.widgetCVar, widget.widgetOptions.comboBoxOptions, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .defaultIndex = static_cast(std::get(widget.widgetOptions.defaultVariant)), + .labelPosition = widget.widgetOptions.labelPosition == UIWidgets::LabelPosition::None + ? UIWidgets::LabelPosition::Above + : widget.widgetOptions.labelPosition })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + break; + case WIDGET_SLIDER_INT: { + int32_t* pointer = std::get(widget.widgetOptions.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("int32 Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + if (UIWidgets::SliderInt(widget.widgetName.c_str(), pointer, + std::get(widget.widgetOptions.min), + std::get(widget.widgetOptions.max), + { + .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + }; + } break; + case WIDGET_SLIDER_FLOAT: { + float floatMin = (std::get(widget.widgetOptions.min) / 100); + float floatMax = (std::get(widget.widgetOptions.max) / 100); + float floatDefault = (std::get(widget.widgetOptions.defaultVariant) / 100); + float* pointer = std::get(widget.widgetOptions.valuePointer); + + if (pointer == nullptr) { + SPDLOG_ERROR("float Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + if (UIWidgets::SliderFloat(widget.widgetName.c_str(), pointer, floatMin, floatMax, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .showButtons = widget.widgetOptions.showButtons, + .format = widget.widgetOptions.format, + .isPercentage = widget.widgetOptions.isPercentage })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + } break; + case WIDGET_CVAR_SLIDER_INT: + if (UIWidgets::CVarSliderInt(widget.widgetName.c_str(), widget.widgetCVar, + std::get(widget.widgetOptions.min), + std::get(widget.widgetOptions.max), + std::get(widget.widgetOptions.defaultVariant), + { + .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + }; + break; + case WIDGET_CVAR_SLIDER_FLOAT: { + float floatMin = (std::get(widget.widgetOptions.min) / 100); + float floatMax = (std::get(widget.widgetOptions.max) / 100); + float floatDefault = (std::get(widget.widgetOptions.defaultVariant) / 100); + if (UIWidgets::CVarSliderFloat(widget.widgetName.c_str(), widget.widgetCVar, floatMin, floatMax, + floatDefault, + { .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .showButtons = widget.widgetOptions.showButtons, + .format = widget.widgetOptions.format, + .isPercentage = widget.widgetOptions.isPercentage })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + } break; + case WIDGET_BUTTON: + if (UIWidgets::Button(widget.widgetName.c_str(), + { menuTheme[menuThemeIndex], widget.widgetOptions.size, widget.widgetTooltip, + disabledValue, disabledTooltip })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + break; + case WIDGET_WINDOW_BUTTON: { + if (widget.widgetOptions.windowName == nullptr || widget.widgetOptions.windowName[0] == '\0') { + std::string msg = + fmt::format("Error drawing window contents for {}: windowName not defined", widget.widgetName); + SPDLOG_ERROR(msg.c_str()); + break; + } + auto window = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow(widget.widgetOptions.windowName); + if (!window) { + std::string msg = fmt::format("Error drawing window contents: windowName {} does not exist", + widget.widgetOptions.windowName); + SPDLOG_ERROR(msg.c_str()); + break; + } + UIWidgets::WindowButton(widget.widgetName.c_str(), widget.widgetCVar, window, + { .size = widget.widgetOptions.size, .tooltip = widget.widgetTooltip }); + if (!window->IsVisible()) { + window->DrawElement(); + } + } break; + case WIDGET_SEARCH: { + if (ImGui::Button("Clear")) { + menuSearch.Clear(); + } + ImGui::SameLine(); + if (CVarGetInteger("gSettings.SearchAutofocus", 0) && + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() && + !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + menuSearch.Draw(); + std::string menuSearchText(menuSearch.InputBuf); + + if (menuSearchText == "") { + ImGui::Text("Start typing to see results."); + return; + } + ImGui::BeginChild("Search Results"); + for (auto& [menuLabel, menuSidebar, cvar] : menuEntries) { + for (auto& sidebar : menuSidebar) { + for (auto& widgets : sidebar.columnWidgets) { + int column = 1; + for (auto& info : widgets) { + if (info.widgetType == WIDGET_SEARCH || info.widgetType == WIDGET_SEPARATOR || + info.widgetType == WIDGET_SEPARATOR_TEXT || info.isHidden) { + continue; + } + std::string widgetStr = std::string(info.widgetName) + std::string(info.widgetTooltip); + std::transform(menuSearchText.begin(), menuSearchText.end(), menuSearchText.begin(), + ::tolower); + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), + menuSearchText.end()); + std::transform(widgetStr.begin(), widgetStr.end(), widgetStr.begin(), ::tolower); + widgetStr.erase(std::remove(widgetStr.begin(), widgetStr.end(), ' '), widgetStr.end()); + if (widgetStr.find(menuSearchText) != std::string::npos) { + SearchMenuGetItem(info); + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::Colors::Gray); + std::string origin = + fmt::format(" ({} -> {}, Clmn {})", menuLabel, sidebar.label, column); + ImGui::Text("%s", origin.c_str()); + ImGui::PopStyleColor(); + } + } + column++; + } + } + } + ImGui::EndChild(); + } break; + default: + break; + } + if (widget.postFunc != nullptr) { + widget.postFunc(widget); + } + } catch (const std::bad_variant_access& e) { + SPDLOG_ERROR("Failed to draw menu item \"{}\" due to: {}", widget.widgetName, e.what()); + assert(false); + } +} +} // namespace BenGui diff --git a/mm/2s2h/BenGui/UIWidgets.cpp b/mm/2s2h/BenGui/UIWidgets.cpp index c50aceae7..632aac6b0 100644 --- a/mm/2s2h/BenGui/UIWidgets.cpp +++ b/mm/2s2h/BenGui/UIWidgets.cpp @@ -1,6 +1,7 @@ #include "UIWidgets.hpp" #define IMGUI_DEFINE_MATH_OPERATORS #include +#include #include #include #include @@ -389,6 +390,46 @@ bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t return dirty; } +void ClampFloat(float* value, float min, float max, float step) { + int ticks = 0; + float increment = 1.0f; + if (step < 1.0f) { + ticks++; + increment = 0.1f; + } + if (step < 0.1f) { + ticks++; + increment = 0.01f; + } + if (step < 0.01f) { + ticks++; + increment = 0.001f; + } + if (step < 0.001f) { + ticks++; + increment = 0.0001f; + } + if (step < 0.0001f) { + ticks++; + increment = 0.00001f; + } + if (step < 0.00001f) { + ticks++; + increment = 0.000001f; + } + int factor = 1 * std::pow(10, ticks); + if (*value < min) { + *value = min; + } else if (*value > max) { + *value = max; + } else { + *value = std::round(*value * factor) / factor; + std::stringstream ss; + ss << std::setprecision(ticks) << std::setiosflags(std::ios_base::fixed) << *value; + *value = std::stof(ss.str()); + } +} + bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); @@ -414,8 +455,7 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl if (options.showButtons) { if (Button("-", { .color = options.color, .size = Sizes::Inline }) && *value > min) { *value -= options.step; - if (*value < min) - *value = min; + ClampFloat(value, min, max, options.step); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); dirty = true; } @@ -427,6 +467,7 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_Float, &valueToDisplay, &minToDisplay, &maxToDisplay, options.format, options.flags)) { *value = options.isPercentage ? valueToDisplay / 100.0f : valueToDisplay; + ClampFloat(value, min, max, options.step); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); dirty = true; } @@ -435,8 +476,7 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (Button("+", { .color = options.color, .size = Sizes::Inline }) && *value < max) { *value += options.step; - if (*value > max) - *value = max; + ClampFloat(value, min, max, options.step); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); dirty = true; } diff --git a/mm/2s2h/BenGui/UIWidgets.hpp b/mm/2s2h/BenGui/UIWidgets.hpp index 506d10912..90357360d 100644 --- a/mm/2s2h/BenGui/UIWidgets.hpp +++ b/mm/2s2h/BenGui/UIWidgets.hpp @@ -12,6 +12,8 @@ namespace UIWidgets { + using SectionFunc = void(*)(); + struct TextFilters { static int FilterNumbers(ImGuiInputTextCallbackData* data) { if (data->EventChar < 256 && strchr("1234567890", (char)data->EventChar)) { @@ -97,6 +99,7 @@ namespace UIWidgets { void PushStyleCheckbox(const ImVec4& color = Colors::Indigo); void PopStyleCheckbox(); + void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash); bool Checkbox(const char* label, bool* v, const CheckboxOptions& options = {}); bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options = {}); diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index 57dd9fb65..26113a1d2 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -15,6 +15,7 @@ #include "z64animation.h" #include "z64bgcheck.h" #include +#include #ifdef _WIN32 #include #else @@ -95,6 +96,8 @@ CrowdControl* CrowdControl::Instance; #include "2s2h/resource/importer/BackgroundFactory.h" #include "2s2h/resource/importer/TextureAnimationFactory.h" #include "2s2h/resource/importer/KeyFrameFactory.h" +#include "window/gui/resource/Font.h" +#include "window/gui/resource/FontFactory.h" OTRGlobals* OTRGlobals::Instance; GameInteractor* GameInteractor::Instance; @@ -279,6 +282,14 @@ OTRGlobals::OTRGlobals() { } } #endif + + fontMono = CreateFontWithSize(16.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLarger = CreateFontWithSize(20.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLargest = CreateFontWithSize(24.0f, "fonts/Inconsolata-Regular.ttf"); + fontStandard = CreateFontWithSize(16.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLarger = CreateFontWithSize(20.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLargest = CreateFontWithSize(24.0f, "fonts/Montserrat-Regular.ttf"); + ImGui::GetIO().FontDefault = fontMono; } OTRGlobals::~OTRGlobals() { @@ -310,6 +321,37 @@ struct ExtensionEntry { std::string ext; }; +ImFont* OTRGlobals::CreateFontWithSize(float size, std::string fontPath) { + auto mImGuiIo = &ImGui::GetIO(); + ImFont* font; + if (fontPath == "") { + ImFontConfig fontCfg = ImFontConfig(); + fontCfg.OversampleH = fontCfg.OversampleV = 1; + fontCfg.PixelSnapH = true; + fontCfg.SizePixels = size; + font = mImGuiIo->Fonts->AddFontDefault(&fontCfg); + } else { + auto initData = std::make_shared(); + initData->Format = RESOURCE_FORMAT_BINARY; + initData->Type = static_cast(RESOURCE_TYPE_FONT); + initData->ResourceVersion = 0; + initData->Path = fontPath; + std::shared_ptr fontData = std::static_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResource(fontPath, false, initData)); + font = mImGuiIo->Fonts->AddFontFromMemoryTTF(fontData->Data, fontData->DataSize, size); + } + // FontAwesome fonts need to have their sizes reduced by 2.0f/3.0f in order to align correctly + float iconFontSize = size * 2.0f / 3.0f; + static const ImWchar sIconsRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; + ImFontConfig iconsConfig; + iconsConfig.MergeMode = true; + iconsConfig.PixelSnapH = true; + iconsConfig.GlyphMinAdvanceX = iconFontSize; + mImGuiIo->Fonts->AddFontFromMemoryCompressedBase85TTF(fontawesome_compressed_data_base85, iconFontSize, + &iconsConfig, sIconsRanges); + return font; +} + extern "C" void OTRMessage_Init(); extern "C" void AudioMgr_CreateNextAudioBuffer(s16* samples, u32 num_samples); extern "C" void AudioPlayer_Play(const uint8_t* buf, uint32_t len); @@ -1475,9 +1517,9 @@ extern "C" void OTRControllerCallback(uint8_t rumble) { Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(0)->GetLED()->SetLEDColor( GetColorForControllerLED()); - static std::shared_ptr controllerConfigWindow = nullptr; + static std::shared_ptr controllerConfigWindow = nullptr; if (controllerConfigWindow == nullptr) { - controllerConfigWindow = std::dynamic_pointer_cast( + controllerConfigWindow = std::dynamic_pointer_cast( Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Input Editor")); // TODO: Add SoH Controller Config window rumble testing to upstream LUS config window // note: the current implementation may not be desired in LUS, as "true" rumble support diff --git a/mm/2s2h/BenPort.h b/mm/2s2h/BenPort.h index 81b5975b3..1bad7f7af 100644 --- a/mm/2s2h/BenPort.h +++ b/mm/2s2h/BenPort.h @@ -24,6 +24,13 @@ class OTRGlobals { public: static OTRGlobals* Instance; + ImFont* fontStandard; + ImFont* fontStandardLarger; + ImFont* fontStandardLargest; + ImFont* fontMono; + ImFont* fontMonoLarger; + ImFont* fontMonoLargest; + std::shared_ptr context; OTRGlobals(); @@ -35,6 +42,7 @@ class OTRGlobals { std::shared_ptr> ListFiles(std::string path); private: + ImFont* CreateFontWithSize(float size, std::string fontPath = ""); void CheckSaveFile(size_t sramSize) const; bool hasMasterQuest; bool hasOriginal; diff --git a/mm/2s2h/DeveloperTools/ActorViewer.cpp b/mm/2s2h/DeveloperTools/ActorViewer.cpp index 3a514785c..a3542960e 100644 --- a/mm/2s2h/DeveloperTools/ActorViewer.cpp +++ b/mm/2s2h/DeveloperTools/ActorViewer.cpp @@ -177,12 +177,6 @@ void ActorViewerWindow::UpdateElement() { } void ActorViewerWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Actor Viewer", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::End(); - return; - } - if (gPlayState != nullptr) { if (lastSceneId != gPlayState->sceneId) { ResetVariables(); @@ -442,7 +436,6 @@ void ActorViewerWindow::DrawElement() { } else { ImGui::Text("Playstate needed for actors!"); } - ImGui::End(); } void ActorViewerWindow::InitElement() { diff --git a/mm/2s2h/DeveloperTools/CollisionViewer.cpp b/mm/2s2h/DeveloperTools/CollisionViewer.cpp index 758ade313..efbc9f77a 100644 --- a/mm/2s2h/DeveloperTools/CollisionViewer.cpp +++ b/mm/2s2h/DeveloperTools/CollisionViewer.cpp @@ -42,12 +42,6 @@ static std::vector sphereVtx; // Draws the ImGui window for the collision viewer void CollisionViewerWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(390, 475), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Collision Viewer", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::End(); - return; - } - UIWidgets::CVarCheckbox("Enabled", "gCollisionViewer.Enabled"); ImGui::SameLine(); @@ -113,8 +107,6 @@ void CollisionViewerWindow::DrawElement() { { 192, 0, 192, 255 }); ImGui::EndDisabled(); - - ImGui::End(); } // Calculates the normal for a triangle at the 3 specified points diff --git a/mm/2s2h/DeveloperTools/DeveloperTools.h b/mm/2s2h/DeveloperTools/DeveloperTools.h index 93c361b99..ffcac49f5 100644 --- a/mm/2s2h/DeveloperTools/DeveloperTools.h +++ b/mm/2s2h/DeveloperTools/DeveloperTools.h @@ -1,6 +1,8 @@ #ifndef DEVELOPER_TOOLS_H #define DEVELOPER_TOOLS_H +#define WARP_POINT_CVAR "gDeveloperTools.WarpPoint." + enum DebugSaveInfo { DEBUG_SAVE_INFO_NONE, DEBUG_SAVE_INFO_VANILLA_DEBUG, diff --git a/mm/2s2h/DeveloperTools/EventLog.cpp b/mm/2s2h/DeveloperTools/EventLog.cpp index f4dd1772d..2cd60f733 100644 --- a/mm/2s2h/DeveloperTools/EventLog.cpp +++ b/mm/2s2h/DeveloperTools/EventLog.cpp @@ -259,12 +259,6 @@ void RegisterEventLogHooks() { } void EventLogWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Event Log", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::End(); - return; - } - if (UIWidgets::CVarCheckbox("Enable", "gEventLog.Enabled")) { RegisterEventLogHooks(); } @@ -384,8 +378,6 @@ void EventLogWindow::DrawElement() { ImGui::EndTable(); } - - ImGui::End(); } void EventLogWindow::InitElement() { diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp index e8aa2ddfa..8d6811628 100644 --- a/mm/2s2h/DeveloperTools/SaveEditor.cpp +++ b/mm/2s2h/DeveloperTools/SaveEditor.cpp @@ -1949,12 +1949,6 @@ void DrawFlagsTab() { } void SaveEditorWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(480, 600), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Save Editor", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::End(); - return; - } - if (ImGui::BeginTabBar("SaveContextTabBar", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { if (ImGui::BeginTabItem("General")) { DrawGeneralTab(); @@ -1998,8 +1992,6 @@ void SaveEditorWindow::DrawElement() { ImGui::EndTabBar(); } - - ImGui::End(); } const char* textureLoad[8] = { gDungeonStrayFairyWoodfallIconTex, diff --git a/mm/2s2h/DeveloperTools/WarpPoint.cpp b/mm/2s2h/DeveloperTools/WarpPoint.cpp index 619d2ccfd..bb556eeb6 100644 --- a/mm/2s2h/DeveloperTools/WarpPoint.cpp +++ b/mm/2s2h/DeveloperTools/WarpPoint.cpp @@ -2,6 +2,7 @@ #include "2s2h/BenGui/UIWidgets.hpp" #include "window/gui/IconsFontAwesome4.h" #include "2s2h/Enhancements/GameInteractor/GameInteractor.h" +#include "2s2h/DeveloperTools/DeveloperTools.h" extern "C" { #include "z64.h" @@ -13,8 +14,6 @@ extern SaveContext gSaveContext; extern GameState* gGameState; } -#define CV "gDeveloperTools.WarpPoint." - // 2S2H Added columns to scene table: entranceSceneId, betterMapSelectIndex, humanName #define DEFINE_SCENE(_name, enumValue, _textId, _drawConfig, _restrictionFlags, _persistentCycleFlags, \ _entranceSceneId, _betterMapSelectIndex, humanName) \ @@ -29,8 +28,9 @@ std::unordered_map warpPointSceneList = { #undef DEFINE_SCENE_UNSET void Warp() { - Vec3f pos = { CVarGetFloat(CV "X", 0.0f), CVarGetFloat(CV "Y", 0.0f), CVarGetFloat(CV "Z", 0.0f) }; - s32 entrance = CVarGetInteger(CV "Entrance", ENTRANCE(SOUTH_CLOCK_TOWN, 0)); + Vec3f pos = { CVarGetFloat(WARP_POINT_CVAR "X", 0.0f), CVarGetFloat(WARP_POINT_CVAR "Y", 0.0f), + CVarGetFloat(WARP_POINT_CVAR "Z", 0.0f) }; + s32 entrance = CVarGetInteger(WARP_POINT_CVAR "Entrance", ENTRANCE(SOUTH_CLOCK_TOWN, 0)); if (gPlayState == NULL) { // If gPlayState is NULL, it means the the user opted into BootToWarpPoint and the game is starting up. This is @@ -47,7 +47,7 @@ void Warp() { gSaveContext.save.playerForm = PLAYER_FORM_HUMAN; gSaveContext.save.linkAge = 0; gSaveContext.fileNum = 0xFF; - MapSelect_LoadGame((MapSelectState*)gGameState, CVarGetInteger(CV "Entrance", 0), 0); + MapSelect_LoadGame((MapSelectState*)gGameState, CVarGetInteger(WARP_POINT_CVAR "Entrance", 0), 0); } else { // The else case, and the rest of this function is primarly relevant code copied from Play_SetRespawnData and // func_80169EFC, minus the parts that copy scene flags to scene we are warping to (this is obviously @@ -57,9 +57,9 @@ void Warp() { gPlayState->transitionType = TRANS_TYPE_INSTANT; } gSaveContext.respawn[RESPAWN_MODE_DOWN].entrance = Entrance_Create(entrance >> 9, 0, entrance & 0xF); - gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = CVarGetInteger(CV "Room", 0); + gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = CVarGetInteger(WARP_POINT_CVAR "Room", 0); gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = pos; - gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = CVarGetFloat(CV "Rotation", 0.0f); + gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = CVarGetFloat(WARP_POINT_CVAR "Rotation", 0.0f); gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = PLAYER_PARAMS(0xFF, PLAYER_INITMODE_D); gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST; gSaveContext.respawnFlag = -8; @@ -67,8 +67,8 @@ void Warp() { void RegisterWarpPoint() { GameInteractor::Instance->RegisterGameHook([]() { - if (!CVarGetInteger("gEnhancements.Cutscenes.SkipToFileSelect", 0) && CVarGetInteger(CV "BootToWarpPoint", 0) && - CVarGetInteger(CV "Saved", 0)) { + if (!CVarGetInteger("gEnhancements.Cutscenes.SkipToFileSelect", 0) && + CVarGetInteger(WARP_POINT_CVAR "BootToWarpPoint", 0) && CVarGetInteger(WARP_POINT_CVAR "Saved", 0)) { // Normally called on console logo screen gSaveContext.seqId = (u8)NA_BGM_DISABLED; gSaveContext.ambienceId = AMBIENCE_ID_DISABLED; @@ -85,28 +85,29 @@ void RenderWarpPointSection() { if (UIWidgets::Button("Set Warp Point")) { Player* player = GET_PLAYER(gPlayState); - CVarSetInteger(CV "Entrance", gSaveContext.save.entrance); - CVarSetInteger(CV "Room", gPlayState->roomCtx.curRoom.num); - CVarSetFloat(CV "X", player->actor.world.pos.x); - CVarSetFloat(CV "Y", player->actor.world.pos.y); - CVarSetFloat(CV "Z", player->actor.world.pos.z); - CVarSetFloat(CV "Rotation", player->actor.shape.rot.y); - CVarSetInteger(CV "Saved", 1); + CVarSetInteger(WARP_POINT_CVAR "Entrance", gSaveContext.save.entrance); + CVarSetInteger(WARP_POINT_CVAR "Room", gPlayState->roomCtx.curRoom.num); + CVarSetFloat(WARP_POINT_CVAR "X", player->actor.world.pos.x); + CVarSetFloat(WARP_POINT_CVAR "Y", player->actor.world.pos.y); + CVarSetFloat(WARP_POINT_CVAR "Z", player->actor.world.pos.z); + CVarSetFloat(WARP_POINT_CVAR "Rotation", player->actor.shape.rot.y); + CVarSetInteger(WARP_POINT_CVAR "Saved", 1); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } - if (CVarGetInteger(CV "Saved", 0)) { - u32 sceneId = Entrance_GetSceneIdAbsolute(CVarGetInteger(CV "Entrance", ENTRANCE(SOUTH_CLOCK_TOWN, 0))); + if (CVarGetInteger(WARP_POINT_CVAR "Saved", 0)) { + u32 sceneId = + Entrance_GetSceneIdAbsolute(CVarGetInteger(WARP_POINT_CVAR "Entrance", ENTRANCE(SOUTH_CLOCK_TOWN, 0))); ImGui::AlignTextToFramePadding(); - ImGui::Text("%s Room %d", warpPointSceneList[sceneId], CVarGetInteger(CV "Room", 0)); + ImGui::Text("%s Room %d", warpPointSceneList[sceneId], CVarGetInteger(WARP_POINT_CVAR "Room", 0)); ImGui::SameLine(); if (UIWidgets::Button(ICON_FA_TIMES, { .size = UIWidgets::Sizes::Inline })) { - CVarClear(CV "Entrance"); - CVarClear(CV "Room"); - CVarClear(CV "X"); - CVarClear(CV "Y"); - CVarClear(CV "Z"); - CVarClear(CV "Rotation"); - CVarClear(CV "Saved"); + CVarClear(WARP_POINT_CVAR "Entrance"); + CVarClear(WARP_POINT_CVAR "Room"); + CVarClear(WARP_POINT_CVAR "X"); + CVarClear(WARP_POINT_CVAR "Y"); + CVarClear(WARP_POINT_CVAR "Z"); + CVarClear(WARP_POINT_CVAR "Rotation"); + CVarClear(WARP_POINT_CVAR "Saved"); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } ImGui::SameLine(); diff --git a/mm/assets/custom/fonts/Inconsolata-Regular.ttf b/mm/assets/custom/fonts/Inconsolata-Regular.ttf new file mode 100644 index 000000000..d1241516b Binary files /dev/null and b/mm/assets/custom/fonts/Inconsolata-Regular.ttf differ diff --git a/mm/assets/custom/fonts/Montserrat-Regular.ttf b/mm/assets/custom/fonts/Montserrat-Regular.ttf new file mode 100644 index 000000000..f4a266dd3 Binary files /dev/null and b/mm/assets/custom/fonts/Montserrat-Regular.ttf differ