#include "core/input/gamepad_manager.hpp" #include #include #include #include #include "core/locale/locale.hpp" #include "core/rendering/notification_presets.hpp" #include "utils/log.hpp" namespace { #ifdef __EMSCRIPTEN__ // A Emscripten, SDL 3.4+ no casa el GUID dels mandos web: Chrome/Android // no aporten Vendor/Product ID i el parser de SDL extreu dades brutes. // El W3C Gamepad API, però, garanteix un layout estàndard quan el mapping // reporta "standard", així que injectem un mapping SDL explícit amb eixe // layout per al GUID del joystick abans que SDL intenti obrir-lo com a // gamepad. Fora d'Emscripten és un no-op. void installWebStandardMapping(SDL_JoystickID jid) { SDL_GUID guid = SDL_GetJoystickGUIDForID(jid); char guid_str[33]; SDL_GUIDToString(guid, guid_str, sizeof(guid_str)); const char* name = SDL_GetJoystickNameForID(jid); if (name == nullptr || *name == '\0') { name = "Standard Gamepad"; } char mapping[512]; SDL_snprintf(mapping, sizeof(mapping), "%s,%s," "a:b0,b:b1,x:b2,y:b3," "leftshoulder:b4,rightshoulder:b5," "lefttrigger:b6,righttrigger:b7," "back:b8,start:b9," "leftstick:b10,rightstick:b11," "dpup:b12,dpdown:b13,dpleft:b14,dpright:b15," "guide:b16," "leftx:a0,lefty:a1,rightx:a2,righty:a3," "platform:Emscripten", guid_str, name); SDL_AddGamepadMapping(mapping); } #else void installWebStandardMapping(SDL_JoystickID /*jid*/) {} #endif } // namespace auto GamepadManager::get() -> GamepadManager& { static GamepadManager instance_; return instance_; } auto GamepadManager::prettyName(const char* raw) -> std::string { std::string name = (raw != nullptr && *raw != '\0') ? std::string(raw) : std::string("Gamepad"); const auto POS = name.find_first_of("(["); if (POS != std::string::npos) { name.erase(POS); } while (!name.empty() && name.back() == ' ') { name.pop_back(); } if (name.empty()) { name = "Gamepad"; } return name; } auto GamepadManager::uniqueName(const std::string& base) const -> std::string { const bool COLLISION = std::ranges::any_of( entries_, [&](const Entry& e) { return e.pretty_name == base; }); if (!COLLISION) { return base; } // Format " #N" començant a #2 — la primera ocurrència manté el nom net. for (int i = 2; i < 100; ++i) { std::string candidate = base + " #" + std::to_string(i); const bool MATCH = std::ranges::any_of( entries_, [&](const Entry& e) { return e.pretty_name == candidate; }); if (!MATCH) { return candidate; } } return base; // cas ultra-improbable: 99 mandos idèntics connectats } 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); if (pad == nullptr) { Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError()); return; } std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad))); // `SDL_GetGamepadPath` pot tornar null a backends que no exposen ruta // física (Emscripten, alguns drivers). Buit com a fallback; la // reconnexió es resoldrà per nom. const char* raw_path = SDL_GetGamepadPath(pad); std::string path = (raw_path != nullptr) ? raw_path : ""; Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + path + "]"); entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path}); // Hot-plug: si alguna assignació pendent encaixa amb aquest mando, // la vinculem ara. Primer intentem per path exacte, després per nom. for (auto& a : assignments_) { if (a.pad != nullptr) { continue; } if (a.name.empty() && a.path.empty()) { continue; } rebindAssignment(a); } if (notify) { Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_connected")); } } void GamepadManager::removeGamepad(SDL_JoystickID jid) { auto it = std::ranges::find_if( entries_, [jid](const Entry& e) { return e.instance_id == jid; }); if (it == entries_.end()) { return; } std::string name = std::move(it->pretty_name); SDL_Gamepad* pad = it->pad; if (pad != nullptr) { SDL_CloseGamepad(pad); } Log::info("GamepadManager", "desconnectat {}", name); entries_.erase(it); // Qualsevol assignació que apuntava a aquest mando queda amb `pad` // a null però conservem nom/ruta: si el mando es torna a connectar, // el hot-plug el re-vincularà automàticament. for (auto& a : assignments_) { if (a.pad == pad) { a.pad = nullptr; } } Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_disconnected")); } void GamepadManager::init() { if (initialized_) { return; } if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError()); return; } // `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte // (no a `data/`) perquè SDL_AddGamepadMappingsFromFile només sap // llegir del filesystem real, no del pack. CMake el copia al costat // del binari a cada build perquè SDL_GetBasePath el trobi tant en // dev com en release. Sempre informem del resultat per facilitar el // diagnòstic de mandos no reconeguts. std::string db_path = "gamecontrollerdb.txt"; if (const char* base = SDL_GetBasePath()) { db_path = std::string(base) + "gamecontrollerdb.txt"; } const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str()); if (ADDED < 0) { Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError()); } else { Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path); } int count = 0; SDL_JoystickID* ids = SDL_GetJoysticks(&count); if (ids != nullptr) { for (int i = 0; i < count; ++i) { addGamepad(ids[i], /*notify=*/false); } SDL_free(ids); } Log::info("GamepadManager", "{} mando(s) detectat(s) a l'inici", entries_.size()); initialized_ = true; } void GamepadManager::shutdown() { for (auto& entry : entries_) { if (entry.pad != nullptr) { SDL_CloseGamepad(entry.pad); } } entries_.clear(); if (initialized_) { SDL_QuitSubSystem(SDL_INIT_GAMEPAD); initialized_ = false; } } void GamepadManager::handleEvent(const SDL_Event& ev) { if (ev.type == SDL_EVENT_GAMEPAD_ADDED) { addGamepad(ev.gdevice.which, /*notify=*/true); } else if (ev.type == SDL_EVENT_GAMEPAD_REMOVED) { removeGamepad(ev.gdevice.which); } } auto GamepadManager::getPrimary() const -> SDL_Gamepad* { return entries_.empty() ? nullptr : entries_.front().pad; } auto GamepadManager::getCount() const -> std::size_t { return entries_.size(); } auto GamepadManager::getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad* { return idx < entries_.size() ? entries_[idx].pad : nullptr; } auto GamepadManager::getAll() const -> std::vector { std::vector result; result.reserve(entries_.size()); std::ranges::transform(entries_, std::back_inserter(result), [](const Entry& e) { return Info{.pad = e.pad, .name = e.pretty_name, .path = e.path}; }); return result; } auto GamepadManager::getForPlayer(int player_id) const -> SDL_Gamepad* { if (player_id < 0 || player_id >= MAX_PLAYERS) { return nullptr; } return assignments_[static_cast(player_id)].pad; } void GamepadManager::rebindAssignment(Assignment& a) { // Primera passada: coincidència exacta per path. És el cas estable // (mateix mando, mateix port) i no canvia res de l'assignació. if (!a.path.empty()) { auto it = std::ranges::find_if( entries_, [&](const Entry& e) { return !e.path.empty() && e.path == a.path; }); if (it != entries_.end()) { a.pad = it->pad; return; } } // Segona passada: coincidència per nom (mando canviat de port USB). // Acceptem la primera entrada que no estigui ja vinculada a una altra // assignació — així dos mandos amb el mateix nom no es pisen. if (!a.name.empty()) { for (const auto& e : entries_) { if (e.pretty_name != a.name) { continue; } const bool TAKEN = std::ranges::any_of( assignments_, [&](const Assignment& other) { return &other != &a && other.pad == e.pad; }); if (TAKEN) { continue; } a.pad = e.pad; a.path = e.path; // actualitzem ruta persistida return; } } a.pad = nullptr; } void GamepadManager::assignToPlayer(int player_id, const std::string& name, const std::string& path) { if (player_id < 0 || player_id >= MAX_PLAYERS) { return; } auto& a = assignments_[static_cast(player_id)]; a.name = name; a.path = path; a.pad = nullptr; if (name.empty() && path.empty()) { return; } rebindAssignment(a); // Last-wins: si l'altre jugador tenia el mateix mando vinculat, el // desvinculem (conservant el seu nom/path perquè el menú pugui // mostrar l'estat "pendent" i oferir re-selecció). if (a.pad != nullptr) { for (auto& other : assignments_) { if (&other != &a && other.pad == a.pad) { Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name); other.pad = nullptr; } } } // Traç de l'assignació persistida (vinculada o pendent) perquè quede // visible a la consola igual que les autoassignacions. if (a.pad != nullptr) { Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1); } else { Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name); } } 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}); Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1); break; } } return assigned; }