overlay: small tweaks to auto light matching (#575)

## Link to GitHub Issue or related Pull Request, if one exists
#574 

## Description of change

* Add binds for Nostroller
* Update matching logic to ignore common device-side output names like
"Button" "Light" and "LED" (e.g., `Button 1` from a device now matches
on `P1 1`)
* For RGB matching, in addition to `Light R` on the device also try
`LightR` (Nostroller needs this)
* Add a confirmation dialog for `Clear All`
* Address minor bugs

## Testing
Seems to work on arcin, and Nostroller. Still need to retest Faucetwo.
Hopefully I didn't break Phoenixwan.
This commit is contained in:
bicarus
2026-03-15 19:04:36 -07:00
committed by GitHub
parent 44befb7e9a
commit 9ec62a61ac
3 changed files with 292 additions and 188 deletions
+228 -173
View File
@@ -2416,6 +2416,18 @@ namespace overlay::windows {
// clear all // clear all
ImGui::BeginDisabled(bound.empty()); ImGui::BeginDisabled(bound.empty());
if (ImGui::Button("Clear All")) { if (ImGui::Button("Clear All")) {
ImGui::OpenPopup("Clear all light bindings");
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
ImGui::SetTooltip("Unbind all lights.");
}
if (ImGui::BeginPopupModal("Clear all light bindings", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextUnformatted("Are you sure? This can't be undone.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Yes")) {
if (lights_testing) { if (lights_testing) {
if (lights_test_current >= 0 && lights_test_current < (int) bound.size()) { if (lights_test_current >= 0 && lights_test_current < (int) bound.size()) {
auto &cur = (*lights)[bound[lights_test_current]]; auto &cur = (*lights)[bound[lights_test_current]];
@@ -2434,10 +2446,13 @@ namespace overlay::windows {
clear_light(&light.getAlternatives()[ai], ai + 1); clear_light(&light.getAlternatives()[ai], ai + 1);
} }
} }
ImGui::CloseCurrentPopup();
} }
ImGui::EndDisabled(); ImGui::SameLine();
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) { if (ImGui::Button("Cancel")) {
ImGui::SetTooltip("Unbind all lights."); ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
} }
// test all // test all
@@ -2992,186 +3007,18 @@ namespace overlay::windows {
device = auto_match_devices[auto_match_device_selected]; device = auto_match_devices[auto_match_device_selected];
} }
struct MatchEntry {
std::string game_name;
std::string device_name;
int light_index;
int control_index;
bool soft;
};
std::vector<MatchEntry> matched; std::vector<MatchEntry> matched;
std::vector<MatchEntry> unmatched;
for (int li = 0; li < (int) lights->size(); li++) { for (int li = 0; li < (int) lights->size(); li++) {
matched.push_back({ matched.push_back({
(*lights)[li].getName(), "", li, -1, false (*lights)[li].getName(), "", li, -1, false
}); });
} }
std::vector<MatchEntry> unmatched;
std::vector<std::string> raw_names; std::vector<std::string> raw_names;
std::string detected_controller; std::string detected_controller;
if (has_device) { if (has_device) {
// build output name list detected_controller = match_lights(device, lights, raw_names, matched, unmatched);
switch (device->type) {
case rawinput::HID: {
for (auto &n : device->hidInfo->button_output_caps_names) {
raw_names.push_back(n);
}
for (auto &n : device->hidInfo->value_output_caps_names) {
raw_names.push_back(n);
}
break;
}
case rawinput::SEXTET_OUTPUT: {
for (int i = 0; i < rawinput::SextetDevice::LIGHT_COUNT; i++) {
raw_names.emplace_back(
rawinput::SextetDevice::LIGHT_NAMES[i]);
}
break;
}
case rawinput::PIUIO_DEVICE: {
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS; i++) {
raw_names.emplace_back(
rawinput::PIUIO::LIGHT_NAMES[i]);
}
break;
}
case rawinput::SMX_STAGE: {
for (int i = 0; i < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT; i++) {
raw_names.push_back(
rawinput::SmxStageDevice::GetLightNameByIndex(i));
}
break;
}
case rawinput::SMX_DEDICAB: {
for (int i = 0; i < rawinput::SmxDedicabDevice::LIGHTS_COUNT; i++) {
raw_names.push_back(
rawinput::SmxDedicabDevice::GetLightNameByIndex(i));
}
break;
}
default:
break;
}
// match by name (hard match, case insensitive, with P1/P2 prefix fallback)
std::vector<bool> device_matched(raw_names.size(), false);
std::string player_prefix = auto_match_p2 ? "P2 " : "P1 ";
auto player_prefix_lower = strtolower(player_prefix);
for (auto &entry : matched) {
auto game_lower = strtolower(entry.game_name);
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
auto dev_lower = strtolower(raw_names[ci]);
if (game_lower == dev_lower || game_lower == player_prefix_lower + dev_lower) {
entry.device_name = raw_names[ci];
entry.control_index = ci;
device_matched[ci] = true;
break;
}
}
}
// soft matching
bool is_valkyrie_mode = games::sdvx::is_valkyrie_model();
static const char *RGB[] = {" R", " G", " B"};
// try to match a game light name, first as-is, then with P1/P2 prefix
auto try_match = [&](const std::string &game_target, int ci, const char *controller) -> bool {
auto target = strtolower(game_target);
auto target_player = strtolower(player_prefix + game_target);
for (auto &entry : matched) {
if (entry.control_index >= 0) {
continue;
}
auto entry_lower = strtolower(entry.game_name);
if (entry_lower != target && entry_lower != target_player) {
continue;
}
entry.device_name = raw_names[ci];
entry.control_index = ci;
entry.soft = true;
device_matched[ci] = true;
if (detected_controller.empty()) {
detected_controller = controller;
}
return true;
}
return false;
};
for (int ri = 0; ri < LIGHT_MATCH_MAP_COUNT; ri++) {
auto &rule = LIGHT_MATCH_MAP[ri];
if (*rule.game && this->games_selected_name != rule.game) {
continue;
}
if (rule.vid && device->hidInfo
&& (device->hidInfo->attributes.VendorID != rule.vid
|| device->hidInfo->attributes.ProductID != rule.pid)) {
continue;
}
if (*rule.address) {
// address mode: match device light at specific index
int ci = (int) strtol(rule.address, nullptr, 0);
if (ci >= (int) raw_names.size() || device_matched[ci]) {
continue;
}
if (strtolower(raw_names[ci]) != strtolower(std::string(rule.device_light))) {
continue;
}
try_match(rule.game_light, ci, rule.controller);
} else if (rule.rgb) {
// RGB mode: expand device_light + " R/G/B" + device_suffix
const char *game_base = (is_valkyrie_mode && *rule.game_light_alt)
? rule.game_light_alt : rule.game_light;
if (!*game_base) {
continue;
}
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
auto dev_lower = strtolower(raw_names[ci]);
for (int rgb = 0; rgb < 3; rgb++) {
auto expected = strtolower(
std::string(rule.device_light) + RGB[rgb] + rule.device_suffix);
if (dev_lower != expected) {
continue;
}
try_match(std::string(game_base) + RGB[rgb], ci, rule.controller);
break;
}
}
} else {
// name mode: direct 1:1 device_light → game_light
const char *game_base = (is_valkyrie_mode && *rule.game_light_alt)
? rule.game_light_alt : rule.game_light;
if (!*game_base) {
continue;
}
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
if (strtolower(raw_names[ci]) != strtolower(std::string(rule.device_light))) {
continue;
}
if (try_match(game_base, ci, rule.controller)) {
break;
}
}
}
}
// unmatched device lights
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (!device_matched[ci] && strtolower(raw_names[ci]) != "null") {
unmatched.push_back({"", raw_names[ci], -1, ci, false});
}
}
} }
// partition: matches first, then hard before soft // partition: matches first, then hard before soft
@@ -3553,6 +3400,214 @@ namespace overlay::windows {
ImGui::EndPopup(); ImGui::EndPopup();
} }
std::string Config::match_lights(
rawinput::Device *device,
std::vector<Light> *lights,
std::vector<std::string> &raw_names,
std::vector<MatchEntry> &matched,
std::vector<MatchEntry> &unmatched) {
std::string detected_controller;
// build output name list
switch (device->type) {
case rawinput::HID: {
for (auto &n : device->hidInfo->button_output_caps_names) {
raw_names.push_back(n);
}
for (auto &n : device->hidInfo->value_output_caps_names) {
raw_names.push_back(n);
}
break;
}
case rawinput::SEXTET_OUTPUT: {
for (int i = 0; i < rawinput::SextetDevice::LIGHT_COUNT; i++) {
raw_names.emplace_back(
rawinput::SextetDevice::LIGHT_NAMES[i]);
}
break;
}
case rawinput::PIUIO_DEVICE: {
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS; i++) {
raw_names.emplace_back(
rawinput::PIUIO::LIGHT_NAMES[i]);
}
break;
}
case rawinput::SMX_STAGE: {
for (int i = 0; i < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT; i++) {
raw_names.push_back(
rawinput::SmxStageDevice::GetLightNameByIndex(i));
}
break;
}
case rawinput::SMX_DEDICAB: {
for (int i = 0; i < rawinput::SmxDedicabDevice::LIGHTS_COUNT; i++) {
raw_names.push_back(
rawinput::SmxDedicabDevice::GetLightNameByIndex(i));
}
break;
}
default:
break;
}
// match by name (hard match, case insensitive, with P1/P2 prefix fallback)
std::vector<bool> device_matched(raw_names.size(), false);
const std::string player_prefix = auto_match_p2 ? "p2 " : "p1 ";
for (auto &entry : matched) {
const auto game_lower = strtolower(entry.game_name);
// match on exact name
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
const auto dev_lower = strtolower(raw_names[ci]);
if (game_lower == dev_lower ||
game_lower == player_prefix + dev_lower) {
entry.device_name = raw_names[ci];
entry.control_index = ci;
device_matched[ci] = true;
break;
}
}
// raw device name, but stripped of a few common strings, and spaces
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
auto dev_stripped = strtolower(raw_names[ci]);;
strreplace(dev_stripped, "button", "");
strreplace(dev_stripped, "led", "");
strreplace(dev_stripped, "light", "");
strreplace(dev_stripped, " ", "");
if (game_lower == dev_stripped ||
game_lower == player_prefix + dev_stripped) {
entry.device_name = raw_names[ci];
entry.control_index = ci;
device_matched[ci] = true;
break;
}
}
}
// soft matching
const bool is_valkyrie_mode = games::sdvx::is_valkyrie_model();
static const char *RGB[] = {"R", "G", "B"};
// try to match a game light name, first as-is, then with P1/P2 prefix
auto try_match = [&](const std::string &game_target, int ci, const char *controller) -> bool {
auto target = strtolower(game_target);
auto target_player = strtolower(player_prefix + game_target);
for (auto &entry : matched) {
if (entry.control_index >= 0) {
continue;
}
auto entry_lower = strtolower(entry.game_name);
if (entry_lower != target && entry_lower != target_player) {
continue;
}
entry.device_name = raw_names[ci];
entry.control_index = ci;
entry.soft = true;
device_matched[ci] = true;
if (detected_controller.empty()) {
detected_controller = controller;
}
return true;
}
return false;
};
for (int ri = 0; ri < LIGHT_MATCH_MAP_COUNT; ri++) {
auto &rule = LIGHT_MATCH_MAP[ri];
if (*rule.game && this->games_selected_name != rule.game) {
continue;
}
if (rule.vid && device->hidInfo
&& (device->hidInfo->attributes.VendorID != rule.vid
|| device->hidInfo->attributes.ProductID != rule.pid)) {
continue;
}
if (*rule.address) {
// address mode: match device light at specific index
int ci = (int) strtol(rule.address, nullptr, 0);
if (ci >= (int) raw_names.size() || device_matched[ci]) {
continue;
}
if (strtolower(raw_names[ci]) != strtolower(std::string(rule.device_light))) {
continue;
}
try_match(rule.game_light, ci, rule.controller);
} else if (rule.rgb) {
// RGB mode: expand device_light + " R/G/B" + device_suffix
const char *game_base = (is_valkyrie_mode && *rule.game_light_alt)
? rule.game_light_alt : rule.game_light;
if (!*game_base) {
continue;
}
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
auto dev_lower = strtolower(raw_names[ci]);
for (int rgb = 0; rgb < 3; rgb++) {
// try light name, plus space, plus R/G/B
auto expected = strtolower(
std::string(rule.device_light) + " " + RGB[rgb] + rule.device_suffix);
if (dev_lower == expected) {
try_match(std::string(game_base) + " " + RGB[rgb], ci, rule.controller);
break;
}
// try light name, plus R/G/B
auto expected2 = strtolower(
std::string(rule.device_light) + RGB[rgb] + rule.device_suffix);
if (dev_lower == expected2) {
try_match(std::string(game_base) + " " + RGB[rgb], ci, rule.controller);
break;
}
}
}
} else {
// name mode: direct 1:1 device_light → game_light
const char *game_base = (is_valkyrie_mode && *rule.game_light_alt)
? rule.game_light_alt : rule.game_light;
if (!*game_base) {
continue;
}
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (device_matched[ci]) {
continue;
}
if (strtolower(raw_names[ci]) != strtolower(std::string(rule.device_light))) {
continue;
}
if (try_match(game_base, ci, rule.controller)) {
break;
}
}
}
}
// unmatched device lights
for (int ci = 0; ci < (int) raw_names.size(); ci++) {
if (!device_matched[ci] && strtolower(raw_names[ci]) != "null") {
unmatched.push_back({"", raw_names[ci], -1, ci, false});
}
}
return detected_controller;
}
void Config::build_cards() { void Config::build_cards() {
// early quit // early quit
+15
View File
@@ -26,6 +26,14 @@ namespace overlay::windows {
CONFIG_TAB_SEARCH, CONFIG_TAB_SEARCH,
}; };
struct MatchEntry {
std::string game_name;
std::string device_name;
int light_index;
int control_index;
bool soft;
};
class Config : public Window { class Config : public Window {
private: private:
@@ -129,6 +137,13 @@ namespace overlay::windows {
void edit_light_popup(Light &primary_light, Light *light, const int alt_index); void edit_light_popup(Light &primary_light, Light *light, const int alt_index);
void auto_match_lights_popup(std::vector<Light> *lights); void auto_match_lights_popup(std::vector<Light> *lights);
std::string match_lights(
rawinput::Device *device,
std::vector<Light> *lights,
std::vector<std::string> &raw_names,
std::vector<MatchEntry> &matched,
std::vector<MatchEntry> &unmatched);
void build_cards(); void build_cards();
std::string build_option_value_picker_title(const OptionDefinition& option); std::string build_option_value_picker_title(const OptionDefinition& option);
void build_option_value_picker(Option& option); void build_option_value_picker(Option& option);
@@ -40,6 +40,40 @@ namespace overlay::windows {
{"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x04", false, "5", ""}, {"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x04", false, "5", ""},
{"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x05", false, "6", ""}, {"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x05", false, "6", ""},
{"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x06", false, "7", ""}, {"PHOENIXWAN", "Beatmania IIDX", 0x034C, 0x0368, "Generic Indicator", "", "0x06", false, "7", ""},
// Nostroller VID_0E8F&PID_1212&MI_03 (Lights 01-14) (RGB)
// The lights are named "LED_01R" "LED_01G" "LED_01B" and so on.
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_01", "", "", true, "Key 1", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_02", "", "", true, "Key 2", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_03", "", "", true, "Key 3", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_04", "", "", true, "Key 4", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_05", "", "", true, "Key 5", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_06", "", "", true, "Key 6", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_07", "", "", true, "Key 7", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_08", "", "", true, "Key 8", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_09", "", "", true, "Key 9", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_10", "", "", true, "Key 10", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_11", "", "", true, "Key 11", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_12", "", "", true, "Key 12", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_13", "", "", true, "Key 13", ""},
{"Nostroller (Lights 01-14)", "Nostalgia", 0, 0, "LED_14", "", "", true, "Key 14", ""},
// Nostroller VID_0E8F&PID_1212&MI_02 (Lights 15-28) (RGB)
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_15", "", "", true, "Key 15", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_16", "", "", true, "Key 16", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_17", "", "", true, "Key 17", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_18", "", "", true, "Key 18", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_19", "", "", true, "Key 19", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_20", "", "", true, "Key 20", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_21", "", "", true, "Key 21", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_22", "", "", true, "Key 22", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_23", "", "", true, "Key 23", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_24", "", "", true, "Key 24", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_25", "", "", true, "Key 25", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_26", "", "", true, "Key 26", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_27", "", "", true, "Key 27", ""},
{"Nostroller (Lights 15-28)", "Nostalgia", 0, 0, "LED_28", "", "", true, "Key 28", ""},
}; };
static const int LIGHT_MATCH_MAP_COUNT = static const int LIGHT_MATCH_MAP_COUNT =