Files
aee-arcade/source/core/input/gamepad_manager.cpp
T

296 lines
11 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.
const int ADDED = SDL_AddGamepadMappingsFromFile("data/input/gamecontrollerdb.txt");
if (ADDED > 0) {
std::cout << "GamepadManager: " << ADDED << " mappings de gamecontrollerdb.txt\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);
}
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;
}
}
}
}
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;
}