311 lines
12 KiB
C++
311 lines
12 KiB
C++
#include "core/input/gamepad_manager.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/rendering/notification_presets.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) {
|
|
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
|
|
<< ") ha fallat: " << SDL_GetError() << '\n';
|
|
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 : "";
|
|
std::cout << "GamepadManager: connectat " << name
|
|
<< (path.empty() ? "" : " [" + path + "]") << '\n';
|
|
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); }
|
|
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
|
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)) {
|
|
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
|
|
<< SDL_GetError() << '\n';
|
|
return;
|
|
}
|
|
// `gamecontrollerdb.txt` és opcional. Si no existeix, SDL retorna -1 i
|
|
// continuem amb les definicions internes del sistema. Sempre informem
|
|
// del resultat per facilitar el diagnòstic de mandos no reconeguts.
|
|
static constexpr const char* DB_PATH = "data/input/gamecontrollerdb.txt";
|
|
const int ADDED = SDL_AddGamepadMappingsFromFile(DB_PATH);
|
|
if (ADDED < 0) {
|
|
std::cout << "GamepadManager: no s'ha pogut carregar " << DB_PATH
|
|
<< ": " << SDL_GetError() << '\n';
|
|
} else {
|
|
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << DB_PATH << '\n';
|
|
}
|
|
|
|
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);
|
|
}
|
|
std::cout << "GamepadManager: " << entries_.size() << " mando(s) detectat(s) a l'inici\n";
|
|
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<Info> {
|
|
std::vector<Info> 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<std::size_t>(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<std::size_t>(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) {
|
|
std::cout << "GamepadManager: mando " << a.name
|
|
<< " reassignat (last-wins)\n";
|
|
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) {
|
|
std::cout << "GamepadManager: assignat " << a.name
|
|
<< " → P" << (player_id + 1) << '\n';
|
|
} else {
|
|
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent ("
|
|
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
|
|
}
|
|
}
|
|
|
|
void GamepadManager::unassignFromPlayer(int player_id) {
|
|
if (player_id < 0 || player_id >= MAX_PLAYERS) { return; }
|
|
assignments_[static_cast<std::size_t>(player_id)] = Assignment{};
|
|
}
|
|
|
|
auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
|
|
std::vector<AutoAssigned> assigned;
|
|
for (int pid = 0; pid < MAX_PLAYERS; ++pid) {
|
|
auto& slot = assignments_[static_cast<std::size_t>(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;
|
|
}
|