From 996ff0053e07c455f0b19b58df97a90824c07056 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 24 Apr 2026 18:06:24 +0200 Subject: [PATCH] =?UTF-8?q?fix(input):=20dedup=20de=20mandos,=20auto-assig?= =?UTF-8?q?n=20per=20ordre=20i=20cycles=20sense=20col=C2=B7lisi=C3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/input/gamepad_manager.cpp | 36 ++++++++++++++++++++ source/core/input/gamepad_manager.hpp | 13 ++++++++ source/game/options.cpp | 35 +++++++++++++++++--- source/game/ui/service_menu.cpp | 47 ++++++++++++++++++--------- 4 files changed, 111 insertions(+), 20 deletions(-) diff --git a/source/core/input/gamepad_manager.cpp b/source/core/input/gamepad_manager.cpp index f9d6d2e..1b84f06 100644 --- a/source/core/input/gamepad_manager.cpp +++ b/source/core/input/gamepad_manager.cpp @@ -79,6 +79,17 @@ auto GamepadManager::uniqueName(const std::string& base) const -> std::string { } void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) { + // Dedup per `instance_id`: SDL encua un SDL_EVENT_GAMEPAD_ADDED per + // cada mando ja connectat quan s'inicialitza el subsistema, a més + // dels que ja hem obert a `init()`. Sense aquesta guarda, el mateix + // mando s'afegiria dues vegades (silenciós + notificat amb sufix + // "#2") i qualsevol referència via `getPrimary`/`entries_` seria + // ambigua. + const bool ALREADY_KNOWN = std::ranges::any_of( + entries_, + [jid](const Entry& e) { return e.instance_id == jid; }); + if (ALREADY_KNOWN) { return; } + installWebStandardMapping(jid); if (!SDL_IsGamepad(jid)) { return; } SDL_Gamepad* pad = SDL_OpenGamepad(jid); @@ -257,3 +268,28 @@ void GamepadManager::unassignFromPlayer(int player_id) { if (player_id < 0 || player_id >= MAX_PLAYERS) { return; } assignments_[static_cast(player_id)] = Assignment{}; } + +auto GamepadManager::autoAssignFreeSlots() -> std::vector { + std::vector assigned; + for (int pid = 0; pid < MAX_PLAYERS; ++pid) { + auto& slot = assignments_[static_cast(pid)]; + if (slot.pad != nullptr) { continue; } + // Primer mando que cap altre slot no haja reclamat. Així respectem + // qualsevol assignació persistida que ja haja vinculat un pad + // concret i no el reassignem a un jugador diferent. + for (const auto& e : entries_) { + const bool TAKEN = std::ranges::any_of( + assignments_, + [&](const Assignment& other) { return other.pad == e.pad; }); + if (TAKEN) { continue; } + slot.pad = e.pad; + slot.name = e.pretty_name; + slot.path = e.path; + assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path}); + std::cout << "GamepadManager: auto-assigned " << e.pretty_name + << " → P" << (pid + 1) << '\n'; + break; + } + } + return assigned; +} diff --git a/source/core/input/gamepad_manager.hpp b/source/core/input/gamepad_manager.hpp index 9ab6c40..a040560 100644 --- a/source/core/input/gamepad_manager.hpp +++ b/source/core/input/gamepad_manager.hpp @@ -80,6 +80,19 @@ class GamepadManager { // Esborra l'assignació (per si l'usuari trie "Cap" al menú). void unassignFromPlayer(int player_id); + // Per a cada slot buit, hi vincula el primer mando connectat que no + // estigui reclamat per un altre slot. Equivalent al patró de + // `coffee_crisis_arcade_edition`: les assignacions persistides tenen + // prioritat; els slots que queden buits després s'emplenen per + // ordre. Retorna la llista de parells (player_id, name, path) que + // s'han vinculat, per a que Options pugui persistir-los. + struct AutoAssigned { + int player_id; + std::string name; + std::string path; + }; + auto autoAssignFreeSlots() -> std::vector; + private: struct Entry { SDL_Gamepad* pad{nullptr}; diff --git a/source/game/options.cpp b/source/game/options.cpp index 9749a41..a775bf9 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -179,10 +179,24 @@ namespace { // Aplica les assignacions persistides al GamepadManager. Cridat // després de `loadFromFile` perquè l'ordre és: GamepadManager::init // (obre tots els mandos físics) → Options::loadFromFile (llig YAML) - // → aquesta funció (casa mandos↔jugadors). + // → aquesta funció (casa mandos↔jugadors) → auto-assign per ordre + // els slots que hagen quedat buits (equivalent al patró de + // `coffee_crisis_arcade_edition`). void applyControlsToGamepadManager() { - GamepadManager::get().assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path); - GamepadManager::get().assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path); + auto& gm = GamepadManager::get(); + gm.assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path); + gm.assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path); + const auto AUTO = gm.autoAssignFreeSlots(); + if (AUTO.empty()) { return; } + // Sincronitzem Options amb el que el manager ha vinculat per + // ordre, per persistir-ho al config.yaml. No cridem els setters + // individuals perquè cada un dispararia un saveToFile; preferim + // fer un sol save al final. + for (const auto& a : AUTO) { + auto& slot = (a.player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2; + slot.name = a.name; + slot.path = a.path; + } } void parseAudio(const fkyaml::node& yaml) { @@ -557,7 +571,11 @@ namespace Options { const std::string CONTENT = readFileOrEmpty(g_config_file_path); if (CONTENT.empty()) { - // Primera execució: creem el fitxer amb els valors per defecte. + // Primera execució: fem l'auto-assign inicial dels mandos + // (per a que P1 obtingui el primer mando connectat sense que + // l'usuari haja de passar pel menú) i després escrivim el + // fitxer amb els valors resultants. + applyControlsToGamepadManager(); return saveToFile(); } @@ -584,7 +602,16 @@ namespace Options { parseVideo(yaml); parseAudio(yaml); parseControls(yaml); + // Captura les assignacions inicials per detectar si + // l'auto-assign (dins d'`applyControls...`) les ha tocat i + // cal reescriure el fitxer amb els valors nous. + const auto BEFORE_P1 = g_controls.gamepad_p1; + const auto BEFORE_P2 = g_controls.gamepad_p2; applyControlsToGamepadManager(); + const bool CHANGED = + (BEFORE_P1.name != g_controls.gamepad_p1.name || BEFORE_P1.path != g_controls.gamepad_p1.path || + BEFORE_P2.name != g_controls.gamepad_p2.name || BEFORE_P2.path != g_controls.gamepad_p2.path); + if (CHANGED) { saveToFile(); } return true; } catch (const std::exception& e) { std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n'; diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index 9495de6..aa5931f 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -755,12 +755,31 @@ auto ServiceMenu::buildKeyboardControls() -> Page { namespace { - // Índex del cycle per a un jugador donat. 0 = "Cap" (desassignat o - // sense coincidència amb cap mando connectat); 1..N = mando `idx-1` - // de la llista actual de `GamepadManager::getAll()`. La coincidència - // es fa primer per ruta (estable entre sessions) i si no, per nom. + // Mandos visibles al cycle d'un jugador: tots els connectats + // EXCEPTE els reclamats per l'altre jugador. Així l'usuari mai pot + // assignar el mateix mando a dos jugadors des de la UI — per + // reassignar-lo ha d'alliberar-lo primer posant-lo a "Cap" a l'altre + // slot. L'ordre de la llista manté el de `getAll()`. + auto visiblePads(int player_id) -> std::vector { + const auto ALL = GamepadManager::get().getAll(); + const auto& other = (player_id == 0) ? Options::controls().gamepad_p2 + : Options::controls().gamepad_p1; + std::vector out; + out.reserve(ALL.size()); + for (const auto& p : ALL) { + const bool OWNED_BY_OTHER = + (!other.path.empty() && p.path == other.path) || + (other.path.empty() && !other.name.empty() && p.name == other.name); + if (!OWNED_BY_OTHER) { out.push_back(p); } + } + return out; + } + + // Índex del cycle dins de la llista `visiblePads(player_id)`. + // 0 = "Cap" (desassignat o sense coincidència amb cap mando visible); + // 1..N = mando `idx-1`. Primer match per ruta, fallback per nom. auto currentPadIndex(int player_id) -> std::size_t { - const auto PADS = GamepadManager::get().getAll(); + const auto PADS = visiblePads(player_id); const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1 : Options::controls().gamepad_p2; if (slot.name.empty() && slot.path.empty()) { return 0; } @@ -773,29 +792,25 @@ namespace { return 0; } - // Text visible per a cada índex del cycle. 0 → "Cap"; altrament - // el nom del mando. Si l'índex queda fora de rang (p.ex. un mando - // desenchufat just abans que es repinte el label) mostrem el nom - // persistit amb un sufix "(desconnectat)" per no deixar l'usuari - // sense feedback. auto padLabel(int player_id, std::size_t idx) -> std::string { if (idx == 0) { return Locale::get().text("menu.values.gamepad_none"); } - const auto PADS = GamepadManager::get().getAll(); + const auto PADS = visiblePads(player_id); const std::size_t POS = idx - 1; if (POS < PADS.size()) { return PADS[POS].name; } + // Fora de rang: l'assignació guardada no és visible ara (p. ex. + // el mando s'acaba de desconnectar). Mostrem el nom persistit + // amb un sufix per no deixar l'usuari sense feedback. const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1 : Options::controls().gamepad_p2; return slot.name + " (" + Locale::get().text("menu.values.gamepad_disconnected") + ')'; } - // Aplica la selecció del cycle: 0 desassigna, altres índexs vinculen - // el mando corresponent. Options persisteix i crida el GamepadManager. void applyPadSelection(int player_id, std::size_t idx) { if (idx == 0) { Options::setGamepadAssignment(player_id, "", ""); return; } - const auto PADS = GamepadManager::get().getAll(); + const auto PADS = visiblePads(player_id); const std::size_t POS = idx - 1; if (POS >= PADS.size()) { return; } Options::setGamepadAssignment(player_id, PADS[POS].name, PADS[POS].path); @@ -814,14 +829,14 @@ auto ServiceMenu::buildGamepadControls() -> Page { page.items.push_back(makeCycle( "menu.items.gamepad_p1", []() { return currentPadIndex(0); }, - []() { return GamepadManager::get().getAll().size() + 1; }, + []() { return visiblePads(0).size() + 1; }, [](std::size_t idx) { applyPadSelection(0, idx); }, [](std::size_t idx) { return padLabel(0, idx); })); page.items.push_back(makeCycle( "menu.items.gamepad_p2", []() { return currentPadIndex(1); }, - []() { return GamepadManager::get().getAll().size() + 1; }, + []() { return visiblePads(1).size() + 1; }, [](std::size_t idx) { applyPadSelection(1, idx); }, [](std::size_t idx) { return padLabel(1, idx); }));