From 9ec62a61acde34dace301c11ea17de538cdc2f8b Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:04:36 -0700 Subject: [PATCH] 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. --- src/spice2x/overlay/windows/config.cpp | 431 ++++++++++-------- src/spice2x/overlay/windows/config.h | 15 + src/spice2x/overlay/windows/light_match_map.h | 34 ++ 3 files changed, 292 insertions(+), 188 deletions(-) diff --git a/src/spice2x/overlay/windows/config.cpp b/src/spice2x/overlay/windows/config.cpp index 671112b..c2ede51 100644 --- a/src/spice2x/overlay/windows/config.cpp +++ b/src/spice2x/overlay/windows/config.cpp @@ -2416,29 +2416,44 @@ namespace overlay::windows { // clear all ImGui::BeginDisabled(bound.empty()); if (ImGui::Button("Clear All")) { - if (lights_testing) { - if (lights_test_current >= 0 && lights_test_current < (int) bound.size()) { - auto &cur = (*lights)[bound[lights_test_current]]; - GameAPI::Lights::writeLight(RI_MGR, cur, 0.f); - RI_MGR->devices_flush_output(); - } - lights_testing = false; - lights_test_current = -1; - } - for (auto &light : *lights) { - if (!light.isSet()) { - continue; - } - clear_light(&light, 0); - for (int ai = 0; ai < (int) light.getAlternatives().size(); ai++) { - clear_light(&light.getAlternatives()[ai], ai + 1); - } - } + 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_test_current >= 0 && lights_test_current < (int) bound.size()) { + auto &cur = (*lights)[bound[lights_test_current]]; + GameAPI::Lights::writeLight(RI_MGR, cur, 0.f); + RI_MGR->devices_flush_output(); + } + lights_testing = false; + lights_test_current = -1; + } + for (auto &light : *lights) { + if (!light.isSet()) { + continue; + } + clear_light(&light, 0); + for (int ai = 0; ai < (int) light.getAlternatives().size(); ai++) { + clear_light(&light.getAlternatives()[ai], ai + 1); + } + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } // test all ImGui::SameLine(); @@ -2992,186 +3007,18 @@ namespace overlay::windows { 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 matched; - std::vector unmatched; - for (int li = 0; li < (int) lights->size(); li++) { matched.push_back({ (*lights)[li].getName(), "", li, -1, false }); } + std::vector unmatched; std::vector raw_names; std::string detected_controller; if (has_device) { - // 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 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}); - } - } + detected_controller = match_lights(device, lights, raw_names, matched, unmatched); } // partition: matches first, then hard before soft @@ -3553,6 +3400,214 @@ namespace overlay::windows { ImGui::EndPopup(); } + std::string Config::match_lights( + rawinput::Device *device, + std::vector *lights, + std::vector &raw_names, + std::vector &matched, + std::vector &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 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() { // early quit diff --git a/src/spice2x/overlay/windows/config.h b/src/spice2x/overlay/windows/config.h index 6a11d89..5b4b905 100644 --- a/src/spice2x/overlay/windows/config.h +++ b/src/spice2x/overlay/windows/config.h @@ -26,6 +26,14 @@ namespace overlay::windows { 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 { private: @@ -129,6 +137,13 @@ namespace overlay::windows { void edit_light_popup(Light &primary_light, Light *light, const int alt_index); void auto_match_lights_popup(std::vector *lights); + std::string match_lights( + rawinput::Device *device, + std::vector *lights, + std::vector &raw_names, + std::vector &matched, + std::vector &unmatched); + void build_cards(); std::string build_option_value_picker_title(const OptionDefinition& option); void build_option_value_picker(Option& option); diff --git a/src/spice2x/overlay/windows/light_match_map.h b/src/spice2x/overlay/windows/light_match_map.h index f085a3d..fd880a5 100644 --- a/src/spice2x/overlay/windows/light_match_map.h +++ b/src/spice2x/overlay/windows/light_match_map.h @@ -40,6 +40,40 @@ namespace overlay::windows { {"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", "", "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 =