diff --git a/source/core/input/gamepad_manager.cpp b/source/core/input/gamepad_manager.cpp index a3c8fe1..f9d6d2e 100644 --- a/source/core/input/gamepad_manager.cpp +++ b/source/core/input/gamepad_manager.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -87,8 +88,23 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) { return; } std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad))); - std::cout << "GamepadManager: connectat " << name << '\n'; - entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name}); + // `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")); } @@ -100,9 +116,18 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) { [jid](const Entry& e) { return e.instance_id == jid; }); if (it == entries_.end()) { return; } std::string name = std::move(it->pretty_name); - if (it->pad != nullptr) { SDL_CloseGamepad(it->pad); } + 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")); } @@ -161,3 +186,74 @@ auto GamepadManager::getCount() const -> std::size_t { 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) { + 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(player_id)] = Assignment{}; +} diff --git a/source/core/input/gamepad_manager.hpp b/source/core/input/gamepad_manager.hpp index 781b2b4..9ab6c40 100644 --- a/source/core/input/gamepad_manager.hpp +++ b/source/core/input/gamepad_manager.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -41,22 +42,70 @@ class GamepadManager { void handleEvent(const SDL_Event& ev); // Primary = primer gamepad al vector. `nullptr` si no n'hi ha cap. - // Ús típic: Player 1 i navegació del ServiceMenu. + // Ús típic: navegació del ServiceMenu. Per al gameplay els jugadors + // usen `getForPlayer(id)`. [[nodiscard]] auto getPrimary() const -> SDL_Gamepad*; [[nodiscard]] auto getCount() const -> std::size_t; - // Retorna el mand del jugador `idx`-èsim (0-indexed) o `nullptr` si - // encara no hi ha prou mandos connectats. Reservat per a multi-jugador. + // Retorna el mand del jugador `idx`-èsim (0-indexed) en l'ordre de + // connexió. Per al gameplay, `getForPlayer` fa servir l'assignació + // persistida, no l'ordre de connexió. [[nodiscard]] auto getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad*; + // Informació exposada al menú de servei (llista de mandos + // disponibles). `path` pot estar buit si SDL no l'exposa (p. ex. a + // alguns backends web). + struct Info { + SDL_Gamepad* pad{nullptr}; + std::string name; + std::string path; + }; + [[nodiscard]] auto getAll() const -> std::vector; + + // Retorna el mand assignat al jugador `player_id` (0 = P1, 1 = P2) + // o `nullptr` si no n'hi ha cap. L'assignació la fixa + // `assignToPlayer` (cridat per Options en carregar `config.yaml` o + // des del ServiceMenu quan l'usuari tria un mando). + [[nodiscard]] auto getForPlayer(int player_id) const -> SDL_Gamepad*; + + // Assigna un mando per nom + ruta física. Si ja hi ha un mando + // connectat que coincideix per ruta (o per nom com a fallback), el + // vincula immediatament; si no, l'assignació queda pendent i + // s'aplicarà si el mando es connecta més tard (hot-plug). + // Política last-wins: si l'altre jugador tenia el mateix mando + // vinculat, se'l desvincula silenciosament. + void assignToPlayer(int player_id, const std::string& name, const std::string& path); + + // Esborra l'assignació (per si l'usuari trie "Cap" al menú). + void unassignFromPlayer(int player_id); + private: struct Entry { SDL_Gamepad* pad{nullptr}; SDL_JoystickID instance_id{0}; std::string pretty_name; + std::string path; }; + // Assignació persistent per jugador. `name` i `path` venen d'Options + // (o del menú de servei) i poden referenciar un mando encara no + // connectat; en aquest cas `pad` és `nullptr` i el hot-plug el + // vincularà quan aparegui. + struct Assignment { + std::string name; + std::string path; + SDL_Gamepad* pad{nullptr}; + }; + static constexpr int MAX_PLAYERS = 2; + std::array assignments_{}; + + // Busca a `entries_` el millor match per a una assignació (primer + // per path, després per nom). Retorna l'iterador o `entries_.end()`. + // També accepta assignar `path` nou si ha coincidit per nom + // (cas mando reenchufat a un altre port USB). + void rebindAssignment(Assignment& a); + GamepadManager() = default; ~GamepadManager() = default; diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index db7ce2c..6faa501 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -25,11 +25,14 @@ namespace { // repòs i no contribueix a la velocitat. constexpr Sint16 STICK_DEADZONE = 12000; - // Acumula la contribució del primer gamepad (cruceta + stick esquerre) - // sobre els acumuladors de velocitat. Extracció d'`update()` per a - // mantenir la complexitat cognitiva sota llindar (threshold 25). - void accumulateGamepadMovement(float& vx, float& vy) { - SDL_Gamepad* pad = GamepadManager::get().getPrimary(); + // Acumula la contribució del mando assignat al jugador (cruceta + + // stick esquerre) sobre els acumuladors de velocitat. Extracció + // d'`update()` per a mantenir la complexitat cognitiva sota llindar + // (threshold 25). L'assignació mando↔jugador viu al `GamepadManager` + // i la persisteix Options; si aquest jugador no en té cap assignat, + // la funció és un no-op (sols es mou amb el teclat). + void accumulateGamepadMovement(int player_id, float& vx, float& vy) { + SDL_Gamepad* pad = GamepadManager::get().getForPlayer(player_id); if (pad == nullptr) { return; } if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; } if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; } @@ -256,7 +259,7 @@ void Player::update(float delta_time) { // Direccions oposades (teclat left + d-pad right) es cancel·len de // forma natural; premuda simultània en la mateixa direcció no duplica // la velocitat perquè cada branca només resta/suma una vegada. - accumulateGamepadMovement(vx, vy); + accumulateGamepadMovement(id_, vx, vy); setVelX(vx); setVelY(vy); diff --git a/source/game/options.cpp b/source/game/options.cpp index b12773e..9749a41 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -9,6 +9,7 @@ #include #include +#include "core/input/gamepad_manager.hpp" #include "external/fkyaml_node.hpp" #include "utils/defines.hpp" @@ -26,6 +27,7 @@ namespace { Options::Window g_window{}; Options::Video g_video{}; Options::Audio g_audio{}; + Options::Controls g_controls{}; std::vector g_postfx_presets{}; std::vector g_crtpi_presets{}; @@ -160,6 +162,29 @@ namespace { readField(node, "volume", g_audio.sound.volume); } + void parseGamepadAssignment(const fkyaml::node& parent, const char* key, Options::GamepadAssignment& dst) { + if (!parent.contains(key)) { return; } + const auto& node = parent[key]; + readField(node, "name", dst.name); + readField(node, "path", dst.path); + } + + void parseControls(const fkyaml::node& yaml) { + if (!yaml.contains("controls")) { return; } + const auto& node = yaml["controls"]; + parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1); + parseGamepadAssignment(node, "gamepad_p2", g_controls.gamepad_p2); + } + + // 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). + 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); + } + void parseAudio(const fkyaml::node& yaml) { if (!yaml.contains("audio")) { return; } const auto& node = yaml["audio"]; @@ -442,6 +467,7 @@ namespace Options { auto window() -> const Window& { return g_window; } auto video() -> const Video& { return g_video; } auto audio() -> const Audio& { return g_audio; } + auto controls() -> const Controls& { return g_controls; } auto locale() -> const std::string& { return g_locale; } auto configVersion() -> const std::string& { return g_config_version; } auto postfxPresets() -> const std::vector& { return g_postfx_presets; } @@ -513,6 +539,15 @@ namespace Options { saveToFile(); } + void setGamepadAssignment(int player_id, const std::string& name, const std::string& path) { + if (player_id < 0 || player_id > 1) { return; } + auto& slot = (player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2; + slot.name = name; + slot.path = path; + GamepadManager::get().assignToPlayer(player_id, name, path); + saveToFile(); + } + void setConfigFile(const std::string& path) { g_config_file_path = path; } @@ -548,6 +583,8 @@ namespace Options { parseWindow(yaml); parseVideo(yaml); parseAudio(yaml); + parseControls(yaml); + applyControlsToGamepadManager(); return true; } catch (const std::exception& e) { std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n'; @@ -594,7 +631,15 @@ namespace Options { out << " volume: " << g_audio.music.volume << "\n"; out << " sound:\n"; out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n"; - out << " volume: " << g_audio.sound.volume << "\n"; + out << " volume: " << g_audio.sound.volume << "\n\n"; + + out << "controls:\n"; + out << " gamepad_p1:\n"; + out << " name: \"" << g_controls.gamepad_p1.name << "\"\n"; + out << " path: \"" << g_controls.gamepad_p1.path << "\"\n"; + out << " gamepad_p2:\n"; + out << " name: \"" << g_controls.gamepad_p2.name << "\"\n"; + out << " path: \"" << g_controls.gamepad_p2.path << "\"\n"; return writeFile(g_config_file_path, out.str()); } diff --git a/source/game/options.hpp b/source/game/options.hpp index cf698b0..16657fb 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -104,10 +104,26 @@ namespace Options { Sound sound{}; }; + // Assignació d'un mando a un jugador: nom visible (el que retorna + // `GamepadManager::Info::name`) + ruta física (de `SDL_GetGamepadPath`). + // Tots dos buits = sense mando. La ruta serveix per re-vincular + // automàticament el mateix mando al mateix port; el nom és el + // fallback quan l'usuari el canvia de port USB. + struct GamepadAssignment { + std::string name; + std::string path; + }; + + struct Controls { + GamepadAssignment gamepad_p1{}; + GamepadAssignment gamepad_p2{}; + }; + // --- Getters de l'estat (read-only) --- [[nodiscard]] auto window() -> const Window&; [[nodiscard]] auto video() -> const Video&; [[nodiscard]] auto audio() -> const Audio&; + [[nodiscard]] auto controls() -> const Controls&; [[nodiscard]] auto locale() -> const std::string&; [[nodiscard]] auto configVersion() -> const std::string&; [[nodiscard]] auto postfxPresets() -> const std::vector&; @@ -132,6 +148,10 @@ namespace Options { void setAudioMusicVolume(float value); void setAudioSoundEnabled(bool value); void setAudioSoundVolume(float value); + // Fixa l'assignació mando→jugador. Persisteix a `config.yaml` i + // propaga a `GamepadManager::assignToPlayer`. `name` i `path` buits + // = desassignació. `player_id`: 0 = P1, 1 = P2. + void setGamepadAssignment(int player_id, const std::string& name, const std::string& path); // --- API pública: càrrega/desament de fitxers --- void setConfigFile(const std::string& path);