diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt
index 1bc5895..7f504b0 100644
--- a/src/spice2x/CMakeLists.txt
+++ b/src/spice2x/CMakeLists.txt
@@ -352,6 +352,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
cfg/configurator.cpp
cfg/configurator_wnd.cpp
cfg/screen_resize.cpp
+ cfg/controller_presets.cpp
# easrv
easrv/easrv.cpp
@@ -565,6 +566,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/screen_resize.cpp
overlay/windows/sdvx_sub.cpp
overlay/windows/config.cpp
+ overlay/windows/controller_presets.cpp
overlay/windows/control.cpp
overlay/windows/eadev.cpp
overlay/windows/fps.cpp
diff --git a/src/spice2x/build/controller_presets.xml b/src/spice2x/build/controller_presets.xml
new file mode 100644
index 0000000..c447639
--- /dev/null
+++ b/src/spice2x/build/controller_presets.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/spice2x/cfg/Win32D.rc b/src/spice2x/cfg/Win32D.rc
index cec5bfd..a94301d 100644
--- a/src/spice2x/cfg/Win32D.rc
+++ b/src/spice2x/cfg/Win32D.rc
@@ -13,5 +13,6 @@ IDR_LICENSES RCDATA "../licenses.txt"
IDR_PATCHES RCDATA "../build/patches.json"
IDR_README RCDATA "../readme.txt"
IDR_DSEGFONT RCDATA "../external/dseg/DSEG14Classic-Italic.ttf"
+IDR_CONTROLLER_PRESETS_BUILTIN RCDATA "../build/controller_presets.xml"
#endif
diff --git a/src/spice2x/cfg/button.h b/src/spice2x/cfg/button.h
index 9c52bcb..1a2fc0d 100644
--- a/src/spice2x/cfg/button.h
+++ b/src/spice2x/cfg/button.h
@@ -63,10 +63,10 @@ private:
float last_velocity = 0.f;
unsigned short velocity_threshold = 0;
- std::string getVKeyString();
std::string getMidiNoteString();
public:
+ std::string getVKeyString();
// overrides
bool override_enabled = false;
diff --git a/src/spice2x/cfg/controller_presets.cpp b/src/spice2x/cfg/controller_presets.cpp
new file mode 100644
index 0000000..4e9500b
--- /dev/null
+++ b/src/spice2x/cfg/controller_presets.cpp
@@ -0,0 +1,442 @@
+#include "overlay/windows/controller_presets.h"
+
+#include
+
+#include "cfg/resource.h"
+#include "external/tinyxml2/tinyxml2.h"
+#include "util/fileutils.h"
+#include "util/logging.h"
+#include "util/resutils.h"
+
+namespace overlay::windows {
+
+ static std::filesystem::path get_templates_path() {
+ return fileutils::get_config_file_path("ControllerPresets", "spicetools_presets.xml");
+ }
+
+ static void write_button_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
+ const std::string &name, const TemplateButtonEntry &entry) {
+ auto *el = doc.NewElement("button");
+ el->SetAttribute("name", name.c_str());
+ el->SetAttribute("vkey", entry.vKey);
+ el->SetAttribute("analogtype", (int)entry.analog_type);
+ el->SetAttribute("devid", entry.device_identifier.c_str());
+ el->SetAttribute("invert", entry.invert);
+ el->SetAttribute("debounce_up", entry.debounce_up);
+ el->SetAttribute("debounce_down", entry.debounce_down);
+ el->SetAttribute("velocity_threshold", entry.velocity_threshold);
+ parent->InsertEndChild(el);
+ }
+
+ static TemplateButtonEntry read_button_entry(tinyxml2::XMLElement *el) {
+ TemplateButtonEntry entry;
+ int vkey = INVALID_VKEY;
+ el->QueryIntAttribute("vkey", &vkey);
+ entry.vKey = (unsigned short)vkey;
+
+ int atype = 0;
+ el->QueryIntAttribute("analogtype", &atype);
+ entry.analog_type = (ButtonAnalogType)atype;
+
+ const char *devid = el->Attribute("devid");
+ entry.device_identifier = devid ? devid : "";
+
+ el->QueryBoolAttribute("invert", &entry.invert);
+ el->QueryDoubleAttribute("debounce_up", &entry.debounce_up);
+ el->QueryDoubleAttribute("debounce_down", &entry.debounce_down);
+
+ int vel = 0;
+ el->QueryIntAttribute("velocity_threshold", &vel);
+ entry.velocity_threshold = (unsigned short)vel;
+
+ return entry;
+ }
+
+ static void write_analog(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
+ const TemplateAnalogBinding &analog) {
+ auto *el = doc.NewElement("analog");
+ el->SetAttribute("name", analog.name.c_str());
+ el->SetAttribute("devid", analog.device_identifier.c_str());
+ el->SetAttribute("index", analog.index);
+ el->SetAttribute("sensivity", analog.sensitivity); // keep typo for compat
+ el->SetAttribute("deadzone", analog.deadzone);
+ el->SetAttribute("deadzone_mirror", analog.deadzone_mirror);
+ el->SetAttribute("invert", analog.invert);
+ el->SetAttribute("smoothing", analog.smoothing);
+ el->SetAttribute("multiplier", analog.multiplier);
+ el->SetAttribute("relative", analog.relative_mode);
+ el->SetAttribute("delay", analog.delay_buffer_depth);
+ parent->InsertEndChild(el);
+ }
+
+ static TemplateAnalogBinding read_analog(tinyxml2::XMLElement *el) {
+ TemplateAnalogBinding a;
+ const char *name = el->Attribute("name");
+ a.name = name ? name : "";
+
+ const char *devid = el->Attribute("devid");
+ a.device_identifier = devid ? devid : "";
+
+ int idx = 0xFF;
+ el->QueryIntAttribute("index", &idx);
+ a.index = (unsigned short)idx;
+
+ el->QueryFloatAttribute("sensivity", &a.sensitivity); // keep typo
+ el->QueryFloatAttribute("deadzone", &a.deadzone);
+ el->QueryBoolAttribute("deadzone_mirror", &a.deadzone_mirror);
+ el->QueryBoolAttribute("invert", &a.invert);
+ el->QueryBoolAttribute("smoothing", &a.smoothing);
+ el->QueryIntAttribute("multiplier", &a.multiplier);
+ el->QueryBoolAttribute("relative", &a.relative_mode);
+ el->QueryIntAttribute("delay", &a.delay_buffer_depth);
+
+ return a;
+ }
+
+ static void write_light_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
+ const std::string &name, const TemplateLightEntry &entry) {
+ auto *el = doc.NewElement("light");
+ el->SetAttribute("name", name.c_str());
+ el->SetAttribute("devid", entry.device_identifier.c_str());
+ el->SetAttribute("index", entry.index);
+ parent->InsertEndChild(el);
+ }
+
+ static TemplateLightEntry read_light_entry(tinyxml2::XMLElement *el) {
+ TemplateLightEntry entry;
+ const char *devid = el->Attribute("devid");
+ entry.device_identifier = devid ? devid : "";
+
+ int idx = 0;
+ el->QueryIntAttribute("index", &idx);
+ entry.index = (unsigned int)idx;
+
+ return entry;
+ }
+
+ // Parse all templates from an already-loaded XMLDocument.
+ // is_builtin controls the is_builtin flag on each returned template.
+ static std::vector load_templates_from_doc(
+ tinyxml2::XMLDocument &doc, bool is_builtin) {
+ std::vector templates;
+
+ auto *root = doc.RootElement();
+ if (!root) return templates;
+
+ auto *tmpl_el = root->FirstChildElement("template");
+ while (tmpl_el) {
+ ControllerTemplate tmpl;
+ const char *name = tmpl_el->Attribute("name");
+ const char *game = tmpl_el->Attribute("game");
+ tmpl.name = name ? name : "";
+ tmpl.game_name = game ? game : "";
+ tmpl.is_builtin = is_builtin;
+
+ // buttons
+ auto *buttons_el = tmpl_el->FirstChildElement("buttons");
+ if (buttons_el) {
+ auto *btn_el = buttons_el->FirstChildElement("button");
+ while (btn_el) {
+ const char *btn_name = btn_el->Attribute("name");
+ std::string btn_name_str = btn_name ? btn_name : "";
+
+ TemplateButtonBinding *binding = nullptr;
+ for (auto &b : tmpl.buttons) {
+ if (b.name == btn_name_str) { binding = &b; break; }
+ }
+ if (!binding) {
+ tmpl.buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
+ } else {
+ binding->alternatives.push_back(read_button_entry(btn_el));
+ }
+ btn_el = btn_el->NextSiblingElement("button");
+ }
+ }
+
+ // keypad buttons
+ auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
+ if (kp_el) {
+ auto *btn_el = kp_el->FirstChildElement("button");
+ while (btn_el) {
+ const char *btn_name = btn_el->Attribute("name");
+ std::string btn_name_str = btn_name ? btn_name : "";
+
+ TemplateButtonBinding *binding = nullptr;
+ for (auto &b : tmpl.keypad_buttons) {
+ if (b.name == btn_name_str) { binding = &b; break; }
+ }
+ if (!binding) {
+ tmpl.keypad_buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
+ } else {
+ binding->alternatives.push_back(read_button_entry(btn_el));
+ }
+ btn_el = btn_el->NextSiblingElement("button");
+ }
+ }
+
+ // analogs
+ auto *analogs_el = tmpl_el->FirstChildElement("analogs");
+ if (analogs_el) {
+ auto *analog_el = analogs_el->FirstChildElement("analog");
+ while (analog_el) {
+ tmpl.analogs.push_back(read_analog(analog_el));
+ analog_el = analog_el->NextSiblingElement("analog");
+ }
+ }
+
+ // lights
+ auto *lights_el = tmpl_el->FirstChildElement("lights");
+ if (lights_el) {
+ auto *light_el = lights_el->FirstChildElement("light");
+ while (light_el) {
+ const char *light_name = light_el->Attribute("name");
+ std::string light_name_str = light_name ? light_name : "";
+
+ TemplateLightBinding *binding = nullptr;
+ for (auto &l : tmpl.lights) {
+ if (l.name == light_name_str) { binding = &l; break; }
+ }
+ if (!binding) {
+ tmpl.lights.push_back({light_name_str, read_light_entry(light_el), {}});
+ } else {
+ binding->alternatives.push_back(read_light_entry(light_el));
+ }
+ light_el = light_el->NextSiblingElement("light");
+ }
+ }
+
+ templates.push_back(std::move(tmpl));
+ tmpl_el = tmpl_el->NextSiblingElement("template");
+ }
+
+ return templates;
+ }
+
+ static std::vector load_builtin_templates() {
+ auto xml = resutil::load_file_string(IDR_CONTROLLER_PRESETS_BUILTIN);
+ tinyxml2::XMLDocument doc;
+ auto err = doc.Parse(xml.c_str());
+ if (err != tinyxml2::XML_SUCCESS) {
+ log_warning("templates", "failed to parse builtin templates XML: {}", (int)err);
+ return {};
+ }
+ return load_templates_from_doc(doc, true);
+ }
+
+ std::vector load_user_templates() {
+ std::vector templates;
+ auto path = get_templates_path();
+
+ tinyxml2::XMLDocument doc;
+ if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
+ return templates;
+ }
+ return load_templates_from_doc(doc, false);
+ }
+
+ bool save_user_template(const ControllerTemplate &tmpl) {
+ auto path = get_templates_path();
+ auto path_tmp = path;
+ path_tmp.replace_extension(L"tmp");
+
+ tinyxml2::XMLDocument doc;
+ if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
+ auto *decl = doc.NewDeclaration();
+ doc.InsertFirstChild(decl);
+ auto *root = doc.NewElement("templates");
+ doc.InsertEndChild(root);
+ }
+
+ auto *root = doc.RootElement();
+ if (!root) {
+ root = doc.NewElement("templates");
+ doc.InsertEndChild(root);
+ }
+
+ // remove existing template with same name and game
+ auto *existing = root->FirstChildElement("template");
+ while (existing) {
+ auto *next = existing->NextSiblingElement("template");
+ const char *ename = existing->Attribute("name");
+ const char *egame = existing->Attribute("game");
+ if (ename && egame &&
+ std::string(ename) == tmpl.name &&
+ std::string(egame) == tmpl.game_name) {
+ root->DeleteChild(existing);
+ }
+ existing = next;
+ }
+
+ auto *tmpl_el = doc.NewElement("template");
+ tmpl_el->SetAttribute("name", tmpl.name.c_str());
+ tmpl_el->SetAttribute("game", tmpl.game_name.c_str());
+
+ // buttons — skip unbound entries
+ auto *buttons_el = doc.NewElement("buttons");
+ for (auto &btn : tmpl.buttons) {
+ if (!btn.primary.is_unbound()) {
+ write_button_entry(doc, buttons_el, btn.name, btn.primary);
+ }
+ for (auto &alt : btn.alternatives) {
+ if (!alt.is_unbound()) {
+ write_button_entry(doc, buttons_el, btn.name, alt);
+ }
+ }
+ }
+ tmpl_el->InsertEndChild(buttons_el);
+
+ // keypad buttons — skip unbound entries
+ if (!tmpl.keypad_buttons.empty()) {
+ auto *kp_el = doc.NewElement("keypad_buttons");
+ for (auto &btn : tmpl.keypad_buttons) {
+ if (!btn.primary.is_unbound()) {
+ write_button_entry(doc, kp_el, btn.name, btn.primary);
+ }
+ for (auto &alt : btn.alternatives) {
+ if (!alt.is_unbound()) {
+ write_button_entry(doc, kp_el, btn.name, alt);
+ }
+ }
+ }
+ tmpl_el->InsertEndChild(kp_el);
+ }
+
+ // analogs — skip unbound entries
+ auto *analogs_el = doc.NewElement("analogs");
+ for (auto &a : tmpl.analogs) {
+ if (!a.is_unbound()) {
+ write_analog(doc, analogs_el, a);
+ }
+ }
+ tmpl_el->InsertEndChild(analogs_el);
+
+ // lights — skip unbound entries
+ auto *lights_el = doc.NewElement("lights");
+ for (auto &l : tmpl.lights) {
+ if (!l.primary.is_unbound()) {
+ write_light_entry(doc, lights_el, l.name, l.primary);
+ }
+ for (auto &alt : l.alternatives) {
+ if (!alt.is_unbound()) {
+ write_light_entry(doc, lights_el, l.name, alt);
+ }
+ }
+ }
+ tmpl_el->InsertEndChild(lights_el);
+
+ root->InsertEndChild(tmpl_el);
+
+ // ensure directory exists
+ if (!path.parent_path().empty() && !std::filesystem::exists(path.parent_path())) {
+ fileutils::dir_create_recursive(path.parent_path());
+ }
+
+ // save atomically via temp file
+ auto save_err = doc.SaveFile(path_tmp.c_str());
+ if (save_err != tinyxml2::XML_SUCCESS) {
+ log_warning("templates", "failed to save templates file: {}", (int)save_err);
+ return false;
+ }
+
+ std::error_code ec;
+ std::filesystem::rename(path_tmp, path, ec);
+ if (ec) {
+ log_warning("templates", "failed to rename templates file: {}", ec.message());
+ return false;
+ }
+
+ return true;
+ }
+
+ bool delete_user_template(const std::string &game_name, const std::string &template_name) {
+ auto path = get_templates_path();
+
+ tinyxml2::XMLDocument doc;
+ if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
+
+ auto *root = doc.RootElement();
+ if (!root) return false;
+
+ bool found = false;
+ auto *tmpl_el = root->FirstChildElement("template");
+ while (tmpl_el) {
+ auto *next = tmpl_el->NextSiblingElement("template");
+ const char *ename = tmpl_el->Attribute("name");
+ const char *egame = tmpl_el->Attribute("game");
+ if (ename && egame &&
+ std::string(ename) == template_name &&
+ std::string(egame) == game_name) {
+ root->DeleteChild(tmpl_el);
+ found = true;
+ }
+ tmpl_el = next;
+ }
+
+ if (found) {
+ auto path_tmp = path;
+ path_tmp.replace_extension(L"tmp");
+ if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
+ std::error_code ec;
+ std::filesystem::rename(path_tmp, path, ec);
+ }
+ }
+
+ return found;
+ }
+
+ bool rename_user_template(const std::string &game_name,
+ const std::string &old_name, const std::string &new_name) {
+ auto path = get_templates_path();
+
+ tinyxml2::XMLDocument doc;
+ if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
+
+ auto *root = doc.RootElement();
+ if (!root) return false;
+
+ bool found = false;
+ auto *tmpl_el = root->FirstChildElement("template");
+ while (tmpl_el) {
+ const char *ename = tmpl_el->Attribute("name");
+ const char *egame = tmpl_el->Attribute("game");
+ if (ename && egame &&
+ std::string(ename) == old_name &&
+ std::string(egame) == game_name) {
+ tmpl_el->SetAttribute("name", new_name.c_str());
+ found = true;
+ break;
+ }
+ tmpl_el = tmpl_el->NextSiblingElement("template");
+ }
+
+ if (found) {
+ auto path_tmp = path;
+ path_tmp.replace_extension(L"tmp");
+ if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
+ std::error_code ec;
+ std::filesystem::rename(path_tmp, path, ec);
+ }
+ }
+
+ return found;
+ }
+
+ std::vector get_templates_for_game(const std::string &game_name) {
+ std::vector result;
+
+ for (auto &t : load_builtin_templates()) {
+ if (t.game_name == game_name) {
+ result.push_back(std::move(t));
+ }
+ }
+
+ for (auto &t : load_user_templates()) {
+ if (t.game_name == game_name) {
+ result.push_back(std::move(t));
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/spice2x/cfg/resource.h b/src/spice2x/cfg/resource.h
index 1c73163..3d2482f 100644
--- a/src/spice2x/cfg/resource.h
+++ b/src/spice2x/cfg/resource.h
@@ -4,3 +4,4 @@
#define IDR_PATCHES 132
#define IDR_README 133
#define IDR_DSEGFONT 134
+#define IDR_CONTROLLER_PRESETS_BUILTIN 135
diff --git a/src/spice2x/overlay/windows/config.cpp b/src/spice2x/overlay/windows/config.cpp
index 5e6ded7..fe4e353 100644
--- a/src/spice2x/overlay/windows/config.cpp
+++ b/src/spice2x/overlay/windows/config.cpp
@@ -257,6 +257,7 @@ namespace overlay::windows {
// selected game changed
if (previous_games_selected != this->games_selected) {
read_card();
+ templates_cache_dirty = true;
}
// tab selection
@@ -353,6 +354,14 @@ namespace overlay::windows {
ImGui::EndChild();
ImGui::EndTabItem();
}
+ if (ImGui::BeginTabItem("Presets")) {
+ tab_selected_new = ConfigTab::CONFIG_TAB_PRESETS;
+ ImGui::BeginChild("Presets", ImVec2(
+ 0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
+ this->build_presets();
+ ImGui::EndChild();
+ ImGui::EndTabItem();
+ }
if (ImGui::BeginTabItem("Cards")) {
tab_selected_new = ConfigTab::CONFIG_TAB_CARDS;
ImGui::BeginChild("Cards", ImVec2(
@@ -573,6 +582,7 @@ namespace overlay::windows {
this->tab_selected = tab_selected_new;
buttons_many_active = false;
buttons_many_index = -1;
+ templates_cache_dirty = true;
ImGui::CloseCurrentPopup();
}
@@ -4680,6 +4690,838 @@ namespace overlay::windows {
}
}
+ //////////////////
+ /// Presets Tab //
+ //////////////////
+
+ ControllerTemplate Config::capture_current_bindings(const std::string &name) {
+ ControllerTemplate tmpl;
+ tmpl.name = name;
+ tmpl.game_name = this->games_selected_name;
+ tmpl.is_builtin = false;
+
+ // capture buttons
+ auto *buttons = games::get_buttons(this->games_selected_name);
+ if (buttons) {
+ for (auto &btn : *buttons) {
+ tmpl.buttons.emplace_back(btn);
+ }
+ }
+
+ // capture keypad buttons
+ auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
+ if (keypad_buttons) {
+ for (auto &btn : *keypad_buttons) {
+ tmpl.keypad_buttons.emplace_back(btn);
+ }
+ }
+
+ // capture analogs
+ auto *analogs = games::get_analogs(this->games_selected_name);
+ if (analogs) {
+ for (auto &a : *analogs) {
+ tmpl.analogs.emplace_back(a);
+ }
+ }
+
+ // capture lights
+ auto *lights = games::get_lights(this->games_selected_name);
+ if (lights) {
+ for (auto &l : *lights) {
+ TemplateLightBinding tl;
+ tl.name = l.getName();
+ tl.primary.device_identifier = l.getDeviceIdentifier();
+ tl.primary.index = l.getIndex();
+
+ for (auto &alt : l.getAlternatives()) {
+ TemplateLightEntry le;
+ le.device_identifier = alt.getDeviceIdentifier();
+ le.index = alt.getIndex();
+ tl.alternatives.push_back(le);
+ }
+
+ tmpl.lights.push_back(std::move(tl));
+ }
+ }
+
+ return tmpl;
+ }
+
+ void Config::clear_all_bindings() {
+ const auto &game = games_list[games_selected];
+
+ // clear all game buttons (NOT overlay buttons)
+ auto *buttons = games::get_buttons(this->games_selected_name);
+ if (buttons) {
+ for (auto &btn : *buttons) {
+ for (int ai = (int)btn.getAlternatives().size() - 1; ai >= 0; ai--) {
+ clear_button(&btn.getAlternatives()[ai], ai + 1);
+ }
+ // clear primary
+ clear_button(&btn, -1);
+ }
+ }
+
+ // clear all keypad buttons
+ auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
+ if (keypad_buttons) {
+ for (auto &btn : *keypad_buttons) {
+ for (int ai = (int)btn.getAlternatives().size() - 1; ai >= 0; ai--) {
+ clear_button(&btn.getAlternatives()[ai], ai + 1);
+ }
+ clear_button(&btn, -1);
+ }
+ }
+
+ // clear all analogs
+ auto *analogs = games::get_analogs(this->games_selected_name);
+ if (analogs) {
+ for (auto &a : *analogs) {
+ a.clearBindings();
+ ::Config::getInstance().updateBinding(game, a);
+ }
+ }
+
+ // clear all lights
+ auto *lights = games::get_lights(this->games_selected_name);
+ if (lights) {
+ for (auto &l : *lights) {
+ for (int ai = (int)l.getAlternatives().size() - 1; ai >= 0; ai--) {
+ clear_light(&l.getAlternatives()[ai], ai + 1);
+ }
+ clear_light(&l, -1);
+ }
+ }
+ }
+
+ // apply bindings from a template filtered by source, remapping to target_device
+ void Config::apply_template_source(const ControllerTemplate &tmpl,
+ const std::string &source_filter,
+ const std::string &target_device) {
+ const auto &game = games_list[games_selected];
+ bool source_is_naive = (source_filter == "Naive");
+ bool target_is_naive = (target_device == "Naive");
+
+ // helper: check if a button entry matches the source filter
+ auto entry_matches = [&](const TemplateButtonEntry &e) -> bool {
+ if (source_is_naive) {
+ return e.is_naive();
+ }
+ return e.is_device() && e.device_identifier == source_filter;
+ };
+
+ // helper: remap device identifier from source to target
+ auto remap_devid = [&]() -> std::string {
+ if (target_is_naive) {
+ return "";
+ }
+ return target_device;
+ };
+
+ // helper: apply template button bindings to a game button vector
+ auto apply_buttons = [&](const std::vector &tmpl_btns,
+ std::vector