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
+90 -42
View File
@@ -121,19 +121,11 @@ namespace {
}
}
// Avança ciclicament l'assignacio de pad d'un jugador i la persisteix.
// El cycle inclou un slot virtual "sense mando" al final (NEXT == N).
// Si l'altre jugador ja tenia el pad triat, fa swap: l'altre rep la
// assignacio prèvia d'aquest jugador.
void cyclePlayerPad(int player_index, int dir) {
const auto* input = Input::get();
if (input == nullptr) {
return;
}
const auto& pads = input->getGamepads();
if (pads.empty()) {
return;
}
// Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
// Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
// l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
// new_path buits.
void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
@@ -141,21 +133,9 @@ namespace {
? ConfigYaml::engine_config.player2
: ConfigYaml::engine_config.player1;
const std::size_t N = pads.size();
const std::size_t SLOTS = N + 1; // N pads + slot "sense mando"
const std::size_t CURRENT = findAssignedIndex(pads, pcfg);
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.
// Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
// path (mateix criteri que resolvePlayerGamepad); si nomes tenim
// nom (mando reconegut sense path), comparem per nom.
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
other.gamepad_name == new_name;
@@ -554,18 +534,6 @@ namespace System {
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 {
return ServiceMenu::Item{
.kind = ServiceMenu::Kind::ACTION,
@@ -587,8 +555,24 @@ namespace System {
Page page;
page.title_key = "service_menu.controls";
page.items = {
makeCyclePadItem("service_menu.controls_pad_p1", 0),
makeCyclePadItem("service_menu.controls_pad_p2", 1),
Item{
.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",
DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P1),
@@ -605,6 +589,70 @@ namespace System {
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 {
Page page;
page.title_key = "service_menu.system";