fix(input): dedup de mandos, auto-assign per ordre i cycles sense col·lisió
This commit is contained in:
1 parent
e5b8d7731b
commit
996ff0053e
4 files changed
+111
-20
No files matched your search
@@ -79,6 +79,17 @@ auto GamepadManager::uniqueName(const std::string& base) const -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
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);
|
installWebStandardMapping(jid);
|
||||||
if (!SDL_IsGamepad(jid)) { return; }
|
if (!SDL_IsGamepad(jid)) { return; }
|
||||||
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
||||||
@@ -257,3 +268,28 @@ void GamepadManager::unassignFromPlayer(int player_id) {
|
|||||||
if (player_id < 0 || player_id >= MAX_PLAYERS) { return; }
|
if (player_id < 0 || player_id >= MAX_PLAYERS) { return; }
|
||||||
assignments_[static_cast<std::size_t>(player_id)] = Assignment{};
|
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;
|
||||||
|
}
|
||||||
@@ -80,6 +80,19 @@ class GamepadManager {
|
|||||||
// Esborra l'assignació (per si l'usuari trie "Cap" al menú).
|
// Esborra l'assignació (per si l'usuari trie "Cap" al menú).
|
||||||
void unassignFromPlayer(int player_id);
|
void unassignFromPlayer(int player_id);
|
||||||
|
|
||||||
|
// Per a cada slot buit, hi vincula el primer mando connectat que no
|
||||||
|
// estigui reclamat per un altre slot. Equivalent al patró de
|
||||||
|
// `coffee_crisis_arcade_edition`: les assignacions persistides tenen
|
||||||
|
// prioritat; els slots que queden buits després s'emplenen per
|
||||||
|
// ordre. Retorna la llista de parells (player_id, name, path) que
|
||||||
|
// s'han vinculat, per a que Options pugui persistir-los.
|
||||||
|
struct AutoAssigned {
|
||||||
|
int player_id;
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
auto autoAssignFreeSlots() -> std::vector<AutoAssigned>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry {
|
||||||
SDL_Gamepad* pad{nullptr};
|
SDL_Gamepad* pad{nullptr};
|
||||||
|
|||||||
+31
-4
@@ -179,10 +179,24 @@ namespace {
|
|||||||
// Aplica les assignacions persistides al GamepadManager. Cridat
|
// Aplica les assignacions persistides al GamepadManager. Cridat
|
||||||
// després de `loadFromFile` perquè l'ordre és: GamepadManager::init
|
// després de `loadFromFile` perquè l'ordre és: GamepadManager::init
|
||||||
// (obre tots els mandos físics) → Options::loadFromFile (llig YAML)
|
// (obre tots els mandos físics) → Options::loadFromFile (llig YAML)
|
||||||
// → aquesta funció (casa mandos↔jugadors).
|
// → aquesta funció (casa mandos↔jugadors) → auto-assign per ordre
|
||||||
|
// els slots que hagen quedat buits (equivalent al patró de
|
||||||
|
// `coffee_crisis_arcade_edition`).
|
||||||
void applyControlsToGamepadManager() {
|
void applyControlsToGamepadManager() {
|
||||||
GamepadManager::get().assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path);
|
auto& gm = GamepadManager::get();
|
||||||
GamepadManager::get().assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path);
|
gm.assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path);
|
||||||
|
gm.assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path);
|
||||||
|
const auto AUTO = gm.autoAssignFreeSlots();
|
||||||
|
if (AUTO.empty()) { return; }
|
||||||
|
// Sincronitzem Options amb el que el manager ha vinculat per
|
||||||
|
// ordre, per persistir-ho al config.yaml. No cridem els setters
|
||||||
|
// individuals perquè cada un dispararia un saveToFile; preferim
|
||||||
|
// fer un sol save al final.
|
||||||
|
for (const auto& a : AUTO) {
|
||||||
|
auto& slot = (a.player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2;
|
||||||
|
slot.name = a.name;
|
||||||
|
slot.path = a.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseAudio(const fkyaml::node& yaml) {
|
void parseAudio(const fkyaml::node& yaml) {
|
||||||
@@ -557,7 +571,11 @@ namespace Options {
|
|||||||
|
|
||||||
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
|
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
|
||||||
if (CONTENT.empty()) {
|
if (CONTENT.empty()) {
|
||||||
// Primera execució: creem el fitxer amb els valors per defecte.
|
// Primera execució: fem l'auto-assign inicial dels mandos
|
||||||
|
// (per a que P1 obtingui el primer mando connectat sense que
|
||||||
|
// l'usuari haja de passar pel menú) i després escrivim el
|
||||||
|
// fitxer amb els valors resultants.
|
||||||
|
applyControlsToGamepadManager();
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +602,16 @@ namespace Options {
|
|||||||
parseVideo(yaml);
|
parseVideo(yaml);
|
||||||
parseAudio(yaml);
|
parseAudio(yaml);
|
||||||
parseControls(yaml);
|
parseControls(yaml);
|
||||||
|
// Captura les assignacions inicials per detectar si
|
||||||
|
// l'auto-assign (dins d'`applyControls...`) les ha tocat i
|
||||||
|
// cal reescriure el fitxer amb els valors nous.
|
||||||
|
const auto BEFORE_P1 = g_controls.gamepad_p1;
|
||||||
|
const auto BEFORE_P2 = g_controls.gamepad_p2;
|
||||||
applyControlsToGamepadManager();
|
applyControlsToGamepadManager();
|
||||||
|
const bool CHANGED =
|
||||||
|
(BEFORE_P1.name != g_controls.gamepad_p1.name || BEFORE_P1.path != g_controls.gamepad_p1.path ||
|
||||||
|
BEFORE_P2.name != g_controls.gamepad_p2.name || BEFORE_P2.path != g_controls.gamepad_p2.path);
|
||||||
|
if (CHANGED) { saveToFile(); }
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
||||||
|
|||||||
@@ -755,12 +755,31 @@ auto ServiceMenu::buildKeyboardControls() -> Page {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Índex del cycle per a un jugador donat. 0 = "Cap" (desassignat o
|
// Mandos visibles al cycle d'un jugador: tots els connectats
|
||||||
// sense coincidència amb cap mando connectat); 1..N = mando `idx-1`
|
// EXCEPTE els reclamats per l'altre jugador. Així l'usuari mai pot
|
||||||
// de la llista actual de `GamepadManager::getAll()`. La coincidència
|
// assignar el mateix mando a dos jugadors des de la UI — per
|
||||||
// es fa primer per ruta (estable entre sessions) i si no, per nom.
|
// reassignar-lo ha d'alliberar-lo primer posant-lo a "Cap" a l'altre
|
||||||
|
// slot. L'ordre de la llista manté el de `getAll()`.
|
||||||
|
auto visiblePads(int player_id) -> std::vector<GamepadManager::Info> {
|
||||||
|
const auto ALL = GamepadManager::get().getAll();
|
||||||
|
const auto& other = (player_id == 0) ? Options::controls().gamepad_p2
|
||||||
|
: Options::controls().gamepad_p1;
|
||||||
|
std::vector<GamepadManager::Info> out;
|
||||||
|
out.reserve(ALL.size());
|
||||||
|
for (const auto& p : ALL) {
|
||||||
|
const bool OWNED_BY_OTHER =
|
||||||
|
(!other.path.empty() && p.path == other.path) ||
|
||||||
|
(other.path.empty() && !other.name.empty() && p.name == other.name);
|
||||||
|
if (!OWNED_BY_OTHER) { out.push_back(p); }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Índex del cycle dins de la llista `visiblePads(player_id)`.
|
||||||
|
// 0 = "Cap" (desassignat o sense coincidència amb cap mando visible);
|
||||||
|
// 1..N = mando `idx-1`. Primer match per ruta, fallback per nom.
|
||||||
auto currentPadIndex(int player_id) -> std::size_t {
|
auto currentPadIndex(int player_id) -> std::size_t {
|
||||||
const auto PADS = GamepadManager::get().getAll();
|
const auto PADS = visiblePads(player_id);
|
||||||
const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1
|
const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1
|
||||||
: Options::controls().gamepad_p2;
|
: Options::controls().gamepad_p2;
|
||||||
if (slot.name.empty() && slot.path.empty()) { return 0; }
|
if (slot.name.empty() && slot.path.empty()) { return 0; }
|
||||||
@@ -773,29 +792,25 @@ namespace {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text visible per a cada índex del cycle. 0 → "Cap"; altrament
|
|
||||||
// el nom del mando. Si l'índex queda fora de rang (p.ex. un mando
|
|
||||||
// desenchufat just abans que es repinte el label) mostrem el nom
|
|
||||||
// persistit amb un sufix "(desconnectat)" per no deixar l'usuari
|
|
||||||
// sense feedback.
|
|
||||||
auto padLabel(int player_id, std::size_t idx) -> std::string {
|
auto padLabel(int player_id, std::size_t idx) -> std::string {
|
||||||
if (idx == 0) { return Locale::get().text("menu.values.gamepad_none"); }
|
if (idx == 0) { return Locale::get().text("menu.values.gamepad_none"); }
|
||||||
const auto PADS = GamepadManager::get().getAll();
|
const auto PADS = visiblePads(player_id);
|
||||||
const std::size_t POS = idx - 1;
|
const std::size_t POS = idx - 1;
|
||||||
if (POS < PADS.size()) { return PADS[POS].name; }
|
if (POS < PADS.size()) { return PADS[POS].name; }
|
||||||
|
// Fora de rang: l'assignació guardada no és visible ara (p. ex.
|
||||||
|
// el mando s'acaba de desconnectar). Mostrem el nom persistit
|
||||||
|
// amb un sufix per no deixar l'usuari sense feedback.
|
||||||
const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1
|
const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1
|
||||||
: Options::controls().gamepad_p2;
|
: Options::controls().gamepad_p2;
|
||||||
return slot.name + " (" + Locale::get().text("menu.values.gamepad_disconnected") + ')';
|
return slot.name + " (" + Locale::get().text("menu.values.gamepad_disconnected") + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica la selecció del cycle: 0 desassigna, altres índexs vinculen
|
|
||||||
// el mando corresponent. Options persisteix i crida el GamepadManager.
|
|
||||||
void applyPadSelection(int player_id, std::size_t idx) {
|
void applyPadSelection(int player_id, std::size_t idx) {
|
||||||
if (idx == 0) {
|
if (idx == 0) {
|
||||||
Options::setGamepadAssignment(player_id, "", "");
|
Options::setGamepadAssignment(player_id, "", "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto PADS = GamepadManager::get().getAll();
|
const auto PADS = visiblePads(player_id);
|
||||||
const std::size_t POS = idx - 1;
|
const std::size_t POS = idx - 1;
|
||||||
if (POS >= PADS.size()) { return; }
|
if (POS >= PADS.size()) { return; }
|
||||||
Options::setGamepadAssignment(player_id, PADS[POS].name, PADS[POS].path);
|
Options::setGamepadAssignment(player_id, PADS[POS].name, PADS[POS].path);
|
||||||
@@ -814,14 +829,14 @@ auto ServiceMenu::buildGamepadControls() -> Page {
|
|||||||
page.items.push_back(makeCycle(
|
page.items.push_back(makeCycle(
|
||||||
"menu.items.gamepad_p1",
|
"menu.items.gamepad_p1",
|
||||||
[]() { return currentPadIndex(0); },
|
[]() { return currentPadIndex(0); },
|
||||||
[]() { return GamepadManager::get().getAll().size() + 1; },
|
[]() { return visiblePads(0).size() + 1; },
|
||||||
[](std::size_t idx) { applyPadSelection(0, idx); },
|
[](std::size_t idx) { applyPadSelection(0, idx); },
|
||||||
[](std::size_t idx) { return padLabel(0, idx); }));
|
[](std::size_t idx) { return padLabel(0, idx); }));
|
||||||
|
|
||||||
page.items.push_back(makeCycle(
|
page.items.push_back(makeCycle(
|
||||||
"menu.items.gamepad_p2",
|
"menu.items.gamepad_p2",
|
||||||
[]() { return currentPadIndex(1); },
|
[]() { return currentPadIndex(1); },
|
||||||
[]() { return GamepadManager::get().getAll().size() + 1; },
|
[]() { return visiblePads(1).size() + 1; },
|
||||||
[](std::size_t idx) { applyPadSelection(1, idx); },
|
[](std::size_t idx) { applyPadSelection(1, idx); },
|
||||||
[](std::size_t idx) { return padLabel(1, idx); }));
|
[](std::size_t idx) { return padLabel(1, idx); }));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user