feat(input): assignació mando↔jugador persistida a config.yaml

This commit is contained in:
JailDesigner committed 2026-04-24 15:13:44 +02:00
1 parent 4e9d5df872
commit a725b76684
5 files changed
+226 -13

No files matched your search

+99 -3
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <utility>
@@ -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<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{};
}
+52 -3
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <array>
#include <cstddef>
#include <string>
#include <vector>
@@ -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<Info>;
// 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<Assignment, MAX_PLAYERS> 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;
+9 -6
View File
@@ -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);
+46 -1
View File
@@ -9,6 +9,7 @@
#include <string_view>
#include <utility>
#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<Options::PostFXPreset> g_postfx_presets{};
std::vector<Options::CrtPiPreset> 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<PostFXPreset>& { 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());
}
+20
View File
@@ -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<PostFXPreset>&;
@@ -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);