#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("bat_threshold", entry.bat_threshold); el->SetAttribute("velocity_threshold", entry.velocity_threshold); el->SetAttribute("modifiers", entry.modifier_mask); 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; int bat = 0; el->QueryIntAttribute("bat_threshold", &bat); entry.bat_threshold = bat; unsigned int modifier_mask = 0; el->QueryUnsignedAttribute("modifiers", &modifier_mask); entry.modifier_mask = static_cast(modifier_mask & UINT8_C(0x0F)); 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_ms", analog.delay_ms); 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->QueryUnsignedAttribute("delay_ms", &a.delay_ms); 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"); } } // modifier buttons auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons"); if (mod_el) { auto *btn_el = mod_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.modifier_buttons) { if (b.name == btn_name_str) { binding = &b; break; } } if (!binding) { tmpl.modifier_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, const bool save_buttons, const bool save_modifiers, const bool save_keypads, const bool save_analogs, const bool save_lights) { 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"); if (save_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); // modifier buttons - skip unbound entries if (!tmpl.modifier_buttons.empty()) { auto *mod_el = doc.NewElement("modifier_buttons"); if (save_modifiers) { for (auto &btn : tmpl.modifier_buttons) { if (!btn.primary.is_unbound()) { write_button_entry(doc, mod_el, btn.name, btn.primary); } for (auto &alt : btn.alternatives) { if (!alt.is_unbound()) { write_button_entry(doc, mod_el, btn.name, alt); } } } } tmpl_el->InsertEndChild(mod_el); } // keypad buttons — skip unbound entries if (!tmpl.keypad_buttons.empty()) { auto *kp_el = doc.NewElement("keypad_buttons"); if (save_keypads) { 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"); if (save_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"); if (save_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; } if (MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) { log_warning("templates", "failed to rename templates file: 0x{:08x}", GetLastError()); return false; } log_info("templates", "templates file saved successfully"); 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) { MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); } } 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) { MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); } } return found; } std::vector get_templates_for_game(const std::string &game_name) { std::vector result; log_misc("templates", "loading built-in templates for {}", game_name); for (auto &t : load_builtin_templates()) { if (t.game_name == game_name) { result.push_back(std::move(t)); } } log_misc("templates", "loading user templates for {}", game_name); for (auto &t : load_user_templates()) { if (t.game_name == game_name) { result.push_back(std::move(t)); } } return result; } }