feat: Make gamepad status lighting optional (#1705)

* feat: Status Lighting option

* wip: rename to "Enable Controller LED"

* wip: setting for all four ports individually

* wip: fix detection + code cleanup

* wip: increase led speed for dusk preset

* wip: use aurora API

* bump aurora to 2b07d55

* wip: remove isSupported + fix namespace comment

* wip: add value override to BoolButton

* Style nit & remove unused

* Undo change

---------

Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Ash
2026-05-28 08:04:23 +02:00
committed by GitHub
parent 6592d95199
commit 71961dae85
8 changed files with 153 additions and 82 deletions
+4 -1
View File
@@ -3,6 +3,9 @@
#ifndef GAMEPAD_COLOR_H
#define GAMEPAD_COLOR_H
void handleGamepadColor();
namespace dusk::input {
void handleGamepadColor();
bool pad_has_led(int port) noexcept;
}
#endif
+1
View File
@@ -209,6 +209,7 @@ struct UserSettings {
ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
std::array<ConfigVar<bool>, 4> enableLED;
ConfigVar<bool> swapDirectSelect;
// Cheats
+100 -78
View File
@@ -5,102 +5,124 @@
#include <d/actor/d_a_alink.h>
#include <dusk/gamepad_color.h>
cXyz currentGamepadColor = {0, 0, 0};
cXyz finalGamepadColor = {0, 0, 0};
cXyz additionalGamepadColor = {0, 0, 0};
#include "ui/controller_config.hpp"
float lerpSpeed = 0.0f;
#include <pad.h>
const cXyz duskColor = {50, 50, -50};
const cXyz noColor = {0, 0, 0};
namespace dusk::input {
cXyz LerpColor(cXyz a, cXyz b, float t) {
return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)};
}
namespace {
cXyz currentColor = {0, 0, 0};
float lerpSpeed = 0.0f;
void FadeLED(cXyz newColor, float speed) {
finalGamepadColor = newColor;
lerpSpeed = speed / 30.0f;
}
enum ColorVariations : u8 {
NO_COLOR = 0,
DUSK_COLOR = 1,
ZHINT = 2,
LINK_WOLF = 3,
LINK_CASUAL = 4,
LINK_KOKIRI = 5,
LINK_ZORA = 6,
LINK_MAGIC = 7,
LINK_MAGIC_HEAVY = 8,
};
void SetLED(cXyz newColor) {
currentGamepadColor = newColor;
finalGamepadColor = newColor;
}
struct ColorSetting {
cXyz color;
float speed;
};
void SetGamepadAdditionalColor(cXyz addColor) {
additionalGamepadColor.x = addColor.x;
additionalGamepadColor.y = addColor.y;
additionalGamepadColor.z = addColor.z;
}
const ColorSetting kColorTable[] = {
/* 0: NO_COLOR */ { {0, 0, 0}, 2.0f },
/* 1: DUSK_COLOR */ { {50, 50, -50}, 2.0f },
/* 2: ZHINT */ { {50, 50, 175}, 2.0f },
/* 3: LINK_WOLF */ { {115, 115, 75}, 5.0f },
/* 4: LINK_CASUAL */ { {235, 230, 115}, 5.0f },
/* 5: LINK_KOKIRI */ { {0, 100, 0}, 5.0f },
/* 6: LINK_ZORA */ { {0, 0, 100}, 5.0f },
/* 7: LINK_MAGIC */ { {100, 0, 5}, 5.0f },
/* 8: LINK_MAGIC_HEAVY */ { {5, 100, 100}, 5.0f },
};
void handleGamepadColor() {
bool setColor = false;
fopAc_ac_c* zhint = dComIfGp_att_getZHint();
if (zhint != NULL) {
FadeLED({50, 50, 175}, 2.0f);
setColor = true;
cXyz LerpColor(const cXyz a, const cXyz b, const float t) {
return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)};
}
daPy_py_c* player = daPy_getPlayerActorClass();
daAlink_c* link = daAlink_getAlinkActorClass();
void clamp(cXyz& color) {
color.x = std::clamp(color.x, 0.0f, 255.0f);
color.y = std::clamp(color.y, 0.0f, 255.0f);
color.z = std::clamp(color.z, 0.0f, 255.0f);
}
ColorSetting getColorSetting() {
const fopAc_ac_c* zHint = dComIfGp_att_getZHint();
if (zHint != nullptr) {
return kColorTable[ZHINT];
}
const daAlink_c* link = daAlink_getAlinkActorClass();
if (link == nullptr)
return kColorTable[DUSK_COLOR];
if (link != nullptr && !setColor) {
if (link->checkWolf()) {
FadeLED({115, 115, 75}, 5.0f);
setColor = true;
} else {
switch (dComIfGs_getSelectEquipClothes()) {
case dItemNo_WEAR_KOKIRI_e:
FadeLED({0, 100, 0}, 5.0f);
setColor = true;
break;
case dItemNo_WEAR_ZORA_e:
FadeLED({0, 0, 100}, 5.0f);
setColor = true;
break;
case dItemNo_ARMOR_e:
if (link->checkMagicArmorHeavy()) {
FadeLED({5, 100, 100}, 5.0f);
} else {
FadeLED({100, 0, 5}, 5.0f);
}
setColor = true;
break;
case dItemNo_WEAR_CASUAL_e:
FadeLED({235, 230, 115}, 5.0f);
setColor = true;
break;
return kColorTable[LINK_WOLF];
}
switch (dComIfGs_getSelectEquipClothes()) {
case dItemNo_WEAR_KOKIRI_e:
return kColorTable[LINK_KOKIRI];
case dItemNo_WEAR_ZORA_e:
return kColorTable[LINK_ZORA];
case dItemNo_ARMOR_e:
if (link->checkMagicArmorHeavy()) {
return kColorTable[LINK_MAGIC_HEAVY];
}
return kColorTable[LINK_MAGIC];
case dItemNo_WEAR_CASUAL_e:
return kColorTable[LINK_CASUAL];
default:
return kColorTable[LINK_KOKIRI];
}
}
if (dKy_darkworld_check()) {
SetGamepadAdditionalColor(duskColor);
} else {
SetGamepadAdditionalColor(noColor);
cXyz getAdditionalColor() {
if (dKy_darkworld_check()) {
return kColorTable[DUSK_COLOR].color;
}
return kColorTable[NO_COLOR].color;
}
f32 finalRed = finalGamepadColor.x + additionalGamepadColor.x;
f32 finalGreen = finalGamepadColor.y + additionalGamepadColor.y;
f32 finalBlue = finalGamepadColor.z + additionalGamepadColor.z;
} // namespace
if (finalRed > 255)
finalRed = 255;
if (finalRed < 0)
finalRed = 0;
bool pad_has_led(const int port) noexcept {
if (port > PAD_MAX_CONTROLLERS || port < 0)
return false;
if (finalGreen > 255)
finalGreen = 255;
if (finalGreen < 0)
finalGreen = 0;
return PADHasLED(port);
}
if (finalBlue > 255)
finalBlue = 255;
if (finalBlue < 0)
finalBlue = 0;
void handleGamepadColor() {
auto [color, speed] = getColorSetting();
const cXyz additionalColor = getAdditionalColor();
cXyz finalColor = color + additionalColor;
lerpSpeed = speed / 30.0f;
currentGamepadColor = LerpColor(currentGamepadColor, cXyz{finalRed, finalGreen, finalBlue}, lerpSpeed);
PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z);
}
clamp(finalColor);
currentColor = LerpColor(currentColor, finalColor, lerpSpeed);
for (int i = 0; i < 4; i++) {
if (pad_has_led(i) && getSettings().game.enableLED[i]) {
PADSetColor(
i,
static_cast<u8>(currentColor.x),
static_cast<u8>(currentColor.y),
static_cast<u8>(currentColor.z)
);
}
}
}
} // namespace dusk::input
+10
View File
@@ -97,6 +97,12 @@ UserSettings g_userSettings = {
.debugFlyCam {"game.debugFlyCam", false},
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
.allowBackgroundInput {"game.allowBackgroundInput", true},
.enableLED {
ConfigVar<bool>{"game.enableLED_port0", true},
ConfigVar<bool>{"game.enableLED_port1", true},
ConfigVar<bool>{"game.enableLED_port2", true},
ConfigVar<bool>{"game.enableLED_port3", true},
},
.swapDirectSelect {"game.swapDirectSelect", false},
// Cheats
@@ -289,6 +295,10 @@ void registerSettings() {
Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.game.debugFlyCamLockEvents);
Register(g_userSettings.game.allowBackgroundInput);
Register(g_userSettings.game.enableLED[0]);
Register(g_userSettings.game.enableLED[1]);
Register(g_userSettings.game.enableLED[2]);
Register(g_userSettings.game.enableLED[3]);
Register(g_userSettings.game.swapDirectSelect);
Register(g_userSettings.backend.isoPath);
+8 -1
View File
@@ -12,7 +12,8 @@ BoolButton::BoolButton(Rml::Element* parent, Props props)
.icon = std::move(props.icon),
}),
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)),
mValueOverride(std::move(props.valueOverride)) {}
bool BoolButton::modified() const {
if (mIsModified) {
@@ -29,6 +30,12 @@ bool BoolButton::disabled() const {
}
Rml::String BoolButton::format_value() {
if (mValueOverride) {
if (std::string value = mValueOverride(); !value.empty()) {
return value;
}
}
return mGetValue() ? "On" : "Off";
}
+2
View File
@@ -12,6 +12,7 @@ public:
std::function<void(bool)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
std::function<Rml::String()> valueOverride;
};
BoolButton(Rml::Element* parent, Props props);
@@ -28,6 +29,7 @@ private:
std::function<void(int)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
std::function<Rml::String()> mValueOverride;
};
} // namespace dusk::ui
+27 -1
View File
@@ -17,6 +17,7 @@
#include "dusk/action_bindings.h"
#include "dusk/config.hpp"
#include "dusk/gamepad_color.h"
namespace dusk::ui {
namespace {
@@ -254,7 +255,9 @@ ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
event.StopPropagation();
}
},
true);
true
);
if (auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr) {
if (auto* root = context->GetRootElement()) {
mListeners.emplace_back(std::make_unique<ScopedEventListener>(
@@ -312,6 +315,28 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
addPageButton(Page::Actions, "Custom Action Bindings", [] {return Rml::String(">"); }, [] { return false; });
leftPane.add_section("Options");
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Enable LED Status",
.getValue =
[port] {
return getSettings().game.enableLED[port].getValue();
},
.setValue =
[port](const bool value) {
getSettings().game.enableLED[port].setValue(value);
},
.isDisabled = [port] {
return !input::pad_has_led(port);
},
.valueOverride = [port] {
if (!input::pad_has_led(port))
return "Not Supported";
return "";
}}),
rightPane, [](Pane& pane) {
pane.add_text("Sets the controller's lighting color based on the game's state.");
});
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Enable Dead Zones",
.getValue =
@@ -379,6 +404,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
PADSerializeMappings();
ClearAllActionBindings(port);
refresh_controller_page();
});
pane.add_button({
+1 -1
View File
@@ -743,7 +743,7 @@ static void fapGm_AfterRecord() {
BOOL isRecording = false;
static void duskExecute() {
handleGamepadColor();
dusk::input::handleGamepadColor();
updateAutoSave();
if (dusk::getSettings().game.recordingMode) {