diff --git a/soh/include/z64.h b/soh/include/z64.h index 7fa24a1b6..8dea2a4d7 100644 --- a/soh/include/z64.h +++ b/soh/include/z64.h @@ -2232,6 +2232,13 @@ typedef enum { /* 0x02 */ PAUSE_ANY_CURSOR_ALWAYS_OFF, } PauseCursorAnySlotOptions; +typedef enum { + LED_SOURCE_TUNIC_ORIGINAL, + LED_SOURCE_TUNIC_COSMETICS, + LED_SOURCE_HEALTH, + LED_SOURCE_CUSTOM +} LEDColorSource; + #define ROM_FILE(name) \ { 0, 0, #name } diff --git a/soh/soh/Enhancements/controls/GameControlEditor.cpp b/soh/soh/Enhancements/controls/GameControlEditor.cpp index 5b2d42902..025b2693c 100644 --- a/soh/soh/Enhancements/controls/GameControlEditor.cpp +++ b/soh/soh/Enhancements/controls/GameControlEditor.cpp @@ -10,10 +10,11 @@ #include #include #include -#include +#include #include #include +#include "Window.h" #include "../../UIWidgets.hpp" namespace GameControlEditor { @@ -88,7 +89,7 @@ namespace GameControlEditor { void DrawUI(bool&); void Init() { - LUS::AddWindow("Enhancements", "Game Control Editor", DrawUI, CVarGetInteger("gGameControlEditorEnabled", 0)); + LUS::AddWindow("Enhancements", "Additional Controller Options", DrawUI, CVarGetInteger("gControllerOptionsEnabled", 0)); addButtonName(BTN_A, "A"); addButtonName(BTN_B, "B"); @@ -325,13 +326,46 @@ namespace GameControlEditor { UIWidgets::PaddedEnhancementCheckbox("Answer Navi Prompt with L Button", "gNaviOnL"); DrawHelpIcon("Speak to Navi with L but enter first-person camera with C-Up"); LUS::EndGroupPanel(); + } + void DrawLEDControlPanel() { + LUS::BeginGroupPanel("LED Colors", ImGui::GetContentRegionAvail()); + static const char* ledSources[4] = { "Original Tunic Colors", "Cosmetics Tunic Colors", "Health Colors", "Custom" }; + UIWidgets::PaddedText("Source"); + UIWidgets::EnhancementCombobox("gLedColorSource", ledSources, LED_SOURCE_TUNIC_ORIGINAL); + DrawHelpIcon("Health\n- Red when health critical (13-20% depending on max health)\n- Yellow when health < 40%. Green otherwise.\n\n" \ + "Tunics: colors will mirror currently equipped tunic, whether original or the current values in Cosmetics Editor.\n\n" \ + "Custom: single, solid color"); + if (CVarGetInteger("gLedColorSource", 1) == 3) { + UIWidgets::Spacer(3); + auto port1Color = CVarGetColor24("gLedPort1Color", { 255, 255, 255 }); + ImVec4 colorVec = { port1Color.r / 255.0f, port1Color.g / 255.0f, port1Color.b / 255.0f, 1.0f }; + if (ImGui::ColorEdit3("", (float*)&colorVec, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + Color_RGB8 color; + color.r = colorVec.x * 255.0; + color.g = colorVec.y * 255.0; + color.b = colorVec.z * 255.0; + + CVarSetColor24("gLedPort1Color", color); + LUS::RequestCvarSaveOnNextTick(); + } + ImGui::SameLine(); + ImGui::Text("Custom Color"); + } + UIWidgets::PaddedEnhancementSliderFloat("Brightness: %d%%", "##LED_Brightness", "gLedBrightness", + 0.0f, 1.0f, "", 1.0f, true, true); + DrawHelpIcon("Sets the brightness of controller LEDs. 0% brightness = LEDs off."); + UIWidgets::PaddedEnhancementCheckbox("Critical Health Override", "gLedCriticalOverride", true, true, + CVarGetInteger("gLedColorSource", LED_SOURCE_TUNIC_ORIGINAL) == LED_SOURCE_HEALTH, "Override redundant for health source.", + UIWidgets::CheckboxGraphics::Cross, true); + DrawHelpIcon("Shows red color when health is critical, otherwise displays according to color source."); + LUS::EndGroupPanel(); } void DrawUI(bool& open) { if (!open) { - if (CVarGetInteger("gGameControlEditorEnabled", 0)) { - CVarClear("gGameControlEditorEnabled"); + if (CVarGetInteger("gControllerOptionsEnabled", 0)) { + CVarClear("gControllerOptionsEnabled"); LUS::RequestCvarSaveOnNextTick(); } return; @@ -361,6 +395,9 @@ namespace GameControlEditor { DrawMiscControlPanel(); } else { DrawCustomButtons(); + if (CurrentPort == 1 && LUS::Context::GetInstance()->GetControlDeck()->GetDeviceFromPortIndex(0)->CanSetLed()) { + DrawLEDControlPanel(); + } } } ImGui::End(); diff --git a/soh/soh/GameMenuBar.cpp b/soh/soh/GameMenuBar.cpp index e3090861f..185258390 100644 --- a/soh/soh/GameMenuBar.cpp +++ b/soh/soh/GameMenuBar.cpp @@ -163,7 +163,7 @@ namespace GameMenuBar { ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.22f, 0.38f, 0.56f, 1.0f)); - if (ImGui::Button(GetWindowButtonText("Controller Configuration", CVarGetInteger("gControllerConfigurationEnabled", 0)).c_str(), ImVec2 (-1.0f, 0.0f))) + if (ImGui::Button(GetWindowButtonText("Controller Mapping", CVarGetInteger("gControllerConfigurationEnabled", 0)).c_str(), ImVec2 (-1.0f, 0.0f))) { if (CVarGetInteger("gControllerConfigurationEnabled", 0)) { CVarClear("gControllerConfigurationEnabled"); @@ -173,6 +173,16 @@ namespace GameMenuBar { LUS::RequestCvarSaveOnNextTick(); LUS::ToggleInputEditorWindow(CVarGetInteger("gControllerConfigurationEnabled", 0)); } + if (ImGui::Button(GetWindowButtonText("Additional Controller Options", CVarGetInteger("gControllerOptionsEnabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) + { + if (CVarGetInteger("gControllerOptionsEnabled", 0)) { + CVarClear("gControllerOptionsEnabled"); + } else { + CVarSetInteger("gControllerOptionsEnabled", 1); + } + LUS::RequestCvarSaveOnNextTick(); + LUS::EnableWindow("Additional Controller Options", CVarGetInteger("gControllerOptionsEnabled", 0)); + } UIWidgets::PaddedSeparator(); ImGui::PopStyleColor(1); ImGui::PopStyleVar(3); @@ -186,7 +196,6 @@ namespace GameMenuBar { UIWidgets::Tooltip("Sets the on screen size of the displayed inputs from the Show Inputs setting"); UIWidgets::PaddedEnhancementSliderInt("Simulated Input Lag: %d frames", "##SimulatedInputLag", "gSimulatedInputLag", 0, 6, "", 0, true, true, false); UIWidgets::Tooltip("Buffers your inputs to be executed a specified amount of frames later"); - ImGui::EndMenu(); } @@ -990,16 +999,6 @@ namespace GameMenuBar { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.22f, 0.38f, 0.56f, 1.0f)); - if (ImGui::Button(GetWindowButtonText("Customize Game Controls", CVarGetInteger("gGameControlEditorEnabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) - { - if (CVarGetInteger("gGameControlEditorEnabled", 0)) { - CVarClear("gGameControlEditorEnabled"); - } else { - CVarSetInteger("gGameControlEditorEnabled", 1); - } - LUS::RequestCvarSaveOnNextTick(); - LUS::EnableWindow("Game Control Editor", CVarGetInteger("gGameControlEditorEnabled", 0)); - } if (ImGui::Button(GetWindowButtonText("Cosmetics Editor", CVarGetInteger("gCosmeticsEditorEnabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) { if (CVarGetInteger("gCosmeticsEditorEnabled", 0)) { diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 7b3515a39..b17fd3db2 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -122,6 +122,10 @@ SpeechSynthesizer* SpeechSynthesizer::Instance; extern "C" char** cameraStrings; std::vector> cameraStdStrings; +Color_RGB8 kokiriColor = { 0x1E, 0x69, 0x1B }; +Color_RGB8 goronColor = { 0x64, 0x14, 0x00 }; +Color_RGB8 zoraColor = { 0x00, 0xEC, 0x64 }; + // OTRTODO: A lot of these left in Japanese are used by the mempak manager. LUS does not currently support mempaks. Ignore unused ones. const char* constCameraStrings[] = { "INSUFFICIENT", @@ -1590,28 +1594,63 @@ extern "C" uint32_t OTRGetCurrentHeight() { return OTRGlobals::Instance->context->GetWindow()->GetCurrentHeight(); } -extern "C" void OTRControllerCallback(uint8_t rumble, uint8_t ledColor) { - auto controlDeck = LUS::Context::GetInstance()->GetControlDeck(); - - for (int i = 0; i < controlDeck->GetNumConnectedPorts(); ++i) { - auto physicalDevice = controlDeck->GetDeviceFromPortIndex(i); - switch (ledColor) { - case 0: - physicalDevice->SetLedColor(i, {255, 0, 0}); - break; - case 1: - physicalDevice->SetLedColor(i, {0x1E, 0x69, 0x1B}); - break; - case 2: - physicalDevice->SetLedColor(i, {0x64, 0x14, 0x00}); - break; - case 3: - physicalDevice->SetLedColor(i, {0x00, 0x3C, 0x64}); - break; +Color_RGB8 GetColorForControllerLED() { + auto brightness = CVarGetFloat("gLedBrightness", 1.0f) / 1.0f; + Color_RGB8 color = { 0, 0, 0 }; + if (brightness > 0.0f) { + LEDColorSource source = static_cast(CVarGetInteger("gLedColorSource", LED_SOURCE_TUNIC_ORIGINAL)); + bool criticalOverride = CVarGetInteger("gLedCriticalOverride", 1); + if (gPlayState && (source == LED_SOURCE_TUNIC_ORIGINAL || source == LED_SOURCE_TUNIC_COSMETICS)) { + switch (CUR_EQUIP_VALUE(EQUIP_TUNIC) - 1) { + case PLAYER_TUNIC_KOKIRI: + color = source == LED_SOURCE_TUNIC_COSMETICS + ? CVarGetColor24("gCosmetics.Link_KokiriTunic.Value", kokiriColor) + : kokiriColor; + break; + case PLAYER_TUNIC_GORON: + color = source == LED_SOURCE_TUNIC_COSMETICS + ? CVarGetColor24("gCosmetics.Link_GoronTunic.Value", goronColor) + : goronColor; + break; + case PLAYER_TUNIC_ZORA: + color = source == LED_SOURCE_TUNIC_COSMETICS + ? CVarGetColor24("gCosmetics.Link_ZoraTunic.Value", zoraColor) + : zoraColor; + break; + } } - - physicalDevice->SetRumble(i, rumble); + if (source == LED_SOURCE_CUSTOM) { + color = CVarGetColor24("gLedPort1Color", { 255, 255, 255 }); + } + if (criticalOverride || source == LED_SOURCE_HEALTH) { + if (HealthMeter_IsCritical()) { + color = { 0xFF, 0, 0 }; + } else if (source == LED_SOURCE_HEALTH) { + if (gSaveContext.health / gSaveContext.healthCapacity <= 0.4f) { + color = { 0xFF, 0xFF, 0 }; + } else { + color = { 0, 0xFF, 0 }; + } + } + } + color.r = color.r * brightness; + color.g = color.g * brightness; + color.b = color.b * brightness; } + + return color; +} + +extern "C" void OTRControllerCallback(uint8_t rumble) { + auto physicalDevice = LUS::Context::GetInstance()->GetControlDeck()->GetDeviceFromPortIndex(0); + + if (physicalDevice->CanSetLed()) { + // We call this every tick, SDL accounts for this use and prevents driver spam + // https://github.com/libsdl-org/SDL/blob/f17058b562c8a1090c0c996b42982721ace90903/src/joystick/SDL_joystick.c#L1114-L1144 + physicalDevice->SetLedColor(0, GetColorForControllerLED()); + } + + physicalDevice->SetRumble(0, rumble); } extern "C" float OTRGetAspectRatio() { diff --git a/soh/src/code/padmgr.c b/soh/src/code/padmgr.c index ac0bfc2c6..d98d23e4a 100644 --- a/soh/src/code/padmgr.c +++ b/soh/src/code/padmgr.c @@ -295,25 +295,7 @@ void PadMgr_ProcessInputs(PadMgr* padMgr) { } uint8_t rumble = (padMgr->rumbleEnable[0] > 0); - uint8_t ledColor = 1; - - if (HealthMeter_IsCritical()) { - ledColor = 0; - } else if (gPlayState) { - switch (CUR_EQUIP_VALUE(EQUIP_TUNIC) - 1) { - case PLAYER_TUNIC_KOKIRI: - ledColor = 1; - break; - case PLAYER_TUNIC_GORON: - ledColor = 2; - break; - case PLAYER_TUNIC_ZORA: - ledColor = 3; - break; - } - } - - OTRControllerCallback(rumble, ledColor); + OTRControllerCallback(rumble); if (CVarGetInteger("gPauseBufferBlockInputFrame", 0)) { Controller_BlockGameInput();