feat(service_menu): picker de mando per llista i fix SENSE MANDO

El cycle anterior fallava al desasignar perque Input::resolvePlayerGamepad
tenia un fallback per slot que reasignava gamepads_[player_index] quan
name+path eren buits. Això el contradeia el slot "SENSE MANDO" del cycle:
el YAML quedava buit pero el runtime seguia lligant el mando. Treure el
fallback i moure l'autoassignacio inicial al boot (nomes si tots dos
jugadors venen buits) restaura la semàntica: buit vol dir buit.

Sobre el fix, redissenyem la UX dels items MANDO P1/P2: ja no son CYCLE
sino SUBMENU que obrin una pàgina-llista (estil RESOLUCIÓ) amb tots els
mandos detectats. Cada item porta sufix (P1)/(P2) nomes si el mando el
te l'altre jugador, perque sapigues que assignar-lo li'l "robarà".
L'ultim item es "SENSE MANDO" per a desassignar explícitament. La
lògica de swap automatic en conflicte queda extreta a assignPadToPlayer
i es reutilitza des de la picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 22:12:53 +02:00
parent 99d0f62ab5
commit 3dcf5c3a99
5 changed files with 132 additions and 59 deletions
+8 -14
View File
@@ -503,10 +503,10 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ========== // ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
// Cerca el gamepad assignat a un jugador. Prioritat: path > name > slot // Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
// per índex (fallback). Retorna nullptr si no n'hi ha cap de connectat. // dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings, // mando explicit). L'autoassignacio inicial es resol al boot.
std::size_t fallback_index) -> std::shared_ptr<Gamepad> { auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
if (gamepads_.empty()) { if (gamepads_.empty()) {
return nullptr; return nullptr;
} }
@@ -527,12 +527,6 @@ auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings,
} }
} }
// Fallback: pad pel slot del jugador (P1=0, P2=1). Si no hi ha pad per al
// slot, retornem nullptr en lloc de robar-li el pad a l'altre jugador.
if (fallback_index < gamepads_.size() && bindings.gamepad_path.empty() && bindings.gamepad_name.empty()) {
return gamepads_[fallback_index];
}
return nullptr; return nullptr;
} }
@@ -545,8 +539,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Resoldre gamepad per path/name/slot // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings, 0); std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (!gamepad) { if (!gamepad) {
player1_gamepad_ = nullptr; player1_gamepad_ = nullptr;
@@ -574,8 +568,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Resoldre gamepad per path/name/slot // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings, 1); std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (!gamepad) { if (!gamepad) {
player2_gamepad_ = nullptr; player2_gamepad_ = nullptr;
+1 -2
View File
@@ -148,8 +148,7 @@ class Input {
auto removeGamepad(SDL_JoystickID id) -> std::string; auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile(); void addGamepadMappingsFromFile();
void discoverGamepads(); void discoverGamepads();
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings, auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
std::size_t fallback_index) -> std::shared_ptr<Gamepad>;
// --- Variables miembro --- // --- Variables miembro ---
static Input* instance; // Instancia única del singleton static Input* instance; // Instancia única del singleton
+28
View File
@@ -109,6 +109,34 @@ Director::Director(int argc, char* argv[])
// Inicialitzar sistema de input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); Input::init("data/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
// arrancades.
{
auto& p1 = cfg_->player1;
auto& p2 = cfg_->player2;
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
if (BOTH_EMPTY) {
const auto& pads = Input::get()->getGamepads();
bool changed = false;
if (!pads.empty() && pads[0]) {
p1.gamepad_name = pads[0]->name;
p1.gamepad_path = pads[0]->path;
changed = true;
}
if (pads.size() > 1 && pads[1]) {
p2.gamepad_name = pads[1]->name;
p2.gamepad_path = pads[1]->path;
changed = true;
}
if (changed) {
ConfigYaml::saveToFile();
}
}
}
// Aplicar configuración de controls dels jugadors // Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer1Bindings(cfg_->player1);
Input::get()->applyPlayer2Bindings(cfg_->player2); Input::get()->applyPlayer2Bindings(cfg_->player2);
+90 -42
View File
@@ -121,19 +121,11 @@ namespace {
} }
} }
// Avança ciclicament l'assignacio de pad d'un jugador i la persisteix. // Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
// El cycle inclou un slot virtual "sense mando" al final (NEXT == N). // Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
// Si l'altre jugador ja tenia el pad triat, fa swap: l'altre rep la // l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
// assignacio prèvia d'aquest jugador. // new_path buits.
void cyclePlayerPad(int player_index, int dir) { void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
const auto* input = Input::get();
if (input == nullptr) {
return;
}
const auto& pads = input->getGamepads();
if (pads.empty()) {
return;
}
auto& pcfg = (player_index == 0) auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1 ? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2; : ConfigYaml::engine_config.player2;
@@ -141,21 +133,9 @@ namespace {
? ConfigYaml::engine_config.player2 ? ConfigYaml::engine_config.player2
: ConfigYaml::engine_config.player1; : ConfigYaml::engine_config.player1;
const std::size_t N = pads.size(); // Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
const std::size_t SLOTS = N + 1; // N pads + slot "sense mando" // path (mateix criteri que resolvePlayerGamepad); si nomes tenim
const std::size_t CURRENT = findAssignedIndex(pads, pcfg); // nom (mando reconegut sense path), comparem per nom.
const std::size_t STEP = (dir > 0) ? 1 : (SLOTS - 1);
const std::size_t NEXT = (CURRENT + STEP) % SLOTS;
// Determinem el nou nom + path (buits si seleccionem "sense mando").
std::string new_name;
std::string new_path;
if (NEXT < N && pads[NEXT]) {
new_name = pads[NEXT]->name;
new_path = pads[NEXT]->path;
}
// Detecta conflicte amb l'altre jugador per fer swap.
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path; const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() && const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
other.gamepad_name == new_name; other.gamepad_name == new_name;
@@ -554,18 +534,6 @@ namespace System {
namespace { namespace {
auto makeCyclePadItem(const char* label_key, int player_index) -> ServiceMenu::Item {
return ServiceMenu::Item{
.kind = ServiceMenu::Kind::CYCLE,
.label_key = label_key,
.label_text = {},
.selectable = true,
.on_activate = {},
.get_value_text = [player_index] { return padDisplayName(player_index); },
.on_change = [player_index](int dir) { cyclePlayerPad(player_index, dir); },
};
}
auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item { auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item {
return ServiceMenu::Item{ return ServiceMenu::Item{
.kind = ServiceMenu::Kind::ACTION, .kind = ServiceMenu::Kind::ACTION,
@@ -587,8 +555,24 @@ namespace System {
Page page; Page page;
page.title_key = "service_menu.controls"; page.title_key = "service_menu.controls";
page.items = { page.items = {
makeCyclePadItem("service_menu.controls_pad_p1", 0), Item{
makeCyclePadItem("service_menu.controls_pad_p2", 1), .kind = Kind::SUBMENU,
.label_key = "service_menu.controls_pad_p1",
.label_text = {},
.selectable = true,
.on_activate = [this] { pushPage(buildPadPickerPage(0)); },
.get_value_text = [] { return padDisplayName(0); },
.on_change = {},
},
Item{
.kind = Kind::SUBMENU,
.label_key = "service_menu.controls_pad_p2",
.label_text = {},
.selectable = true,
.on_activate = [this] { pushPage(buildPadPickerPage(1)); },
.get_value_text = [] { return padDisplayName(1); },
.on_change = {},
},
makeDefineItem("service_menu.controls_define_keyboard_p1", makeDefineItem("service_menu.controls_define_keyboard_p1",
DefineInputs::Mode::KEYBOARD, DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P1), DefineInputs::Player::P1),
@@ -605,6 +589,70 @@ namespace System {
return page; return page;
} }
auto ServiceMenu::buildPadPickerPage(int player_index) -> Page {
Page page;
page.title_key = (player_index == 0)
? "service_menu.controls_pad_p1"
: "service_menu.controls_pad_p2";
const auto* input = Input::get();
if (input == nullptr) {
return page;
}
const auto& pads = input->getGamepads();
const auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
const auto& other = (player_index == 0)
? ConfigYaml::engine_config.player2
: ConfigYaml::engine_config.player1;
// Cursor inicial sobre el pad assignat, o sobre "SENSE MANDO"
// (ultim item) si el jugador no en te cap.
page.cursor = findAssignedIndex(pads, pcfg);
for (const auto& pad : pads) {
if (!pad) {
continue;
}
std::string label = Utils::toUpperAscii(pad->name);
// Sufix (PX) nomes si el mando el te l'altre jugador, perque
// l'usuari sapiga que assignar-lo li'l "robarà".
const bool OTHER_HAS_BY_PATH = !other.gamepad_path.empty() && other.gamepad_path == pad->path;
const bool OTHER_HAS_BY_NAME = other.gamepad_path.empty() && !other.gamepad_name.empty() && other.gamepad_name == pad->name;
if (OTHER_HAS_BY_PATH || OTHER_HAS_BY_NAME) {
label += (player_index == 0) ? " (P2)" : " (P1)";
}
const std::string PAD_NAME = pad->name;
const std::string PAD_PATH = pad->path;
const int PI = player_index;
page.items.push_back(Item{
.kind = Kind::ACTION,
.label_key = {},
.label_text = std::move(label),
.selectable = true,
.on_activate = [PI, PAD_NAME, PAD_PATH] {
assignPadToPlayer(PI, PAD_NAME, PAD_PATH);
},
.get_value_text = {},
.on_change = {},
});
}
// Item final: desasignar.
const int PI = player_index;
page.items.push_back(Item{
.kind = Kind::ACTION,
.label_key = "service_menu.controls_no_pad",
.label_text = {},
.selectable = true,
.on_activate = [PI] { assignPadToPlayer(PI, {}, {}); },
.get_value_text = {},
.on_change = {},
});
return page;
}
auto ServiceMenu::buildSystemPage() -> Page { auto ServiceMenu::buildSystemPage() -> Page {
Page page; Page page;
page.title_key = "service_menu.system"; page.title_key = "service_menu.system";
+5 -1
View File
@@ -107,7 +107,11 @@ namespace System {
[[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> Page; [[nodiscard]] auto buildSystemPage() -> Page;
[[nodiscard]] static auto buildControlsPage() -> Page; [[nodiscard]] auto buildControlsPage() -> Page;
// Llista de mandos detectats per a un jugador. Cada item assigna el
// pad triat (amb swap automatic si l'altre jugador ja el tenia).
// L'ultim item es "SENSE MANDO" per a desasignar.
[[nodiscard]] static auto buildPadPickerPage(int player_index) -> Page;
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si // Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
// l'usuari selecciona SI; el cursor per defecte apunta a NO. // l'usuari selecciona SI; el cursor per defecte apunta a NO.
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes); void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);