From 3dcf5c3a9903c9ef77ba2f988744a405d8df8c7c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 22:12:53 +0200 Subject: [PATCH] feat(service_menu): picker de mando per llista i fix SENSE MANDO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- source/core/input/input.cpp | 22 ++--- source/core/input/input.hpp | 3 +- source/core/system/director.cpp | 28 ++++++ source/core/system/service_menu.cpp | 132 +++++++++++++++++++--------- source/core/system/service_menu.hpp | 6 +- 5 files changed, 132 insertions(+), 59 deletions(-) diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index cc19d43..80ff7e7 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -503,10 +503,10 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std:: // ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ========== -// Cerca el gamepad assignat a un jugador. Prioritat: path > name > slot -// per índex (fallback). Retorna nullptr si no n'hi ha cap de connectat. -auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings, - std::size_t fallback_index) -> std::shared_ptr { +// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els +// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense +// mando explicit). L'autoassignacio inicial es resol al boot. +auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr { if (gamepads_.empty()) { 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; } @@ -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::START].scancode = bindings.keyboard.key_start; - // 2. Resoldre gamepad per path/name/slot - std::shared_ptr gamepad = resolvePlayerGamepad(bindings, 0); + // 2. Resoldre gamepad per path/name + std::shared_ptr gamepad = resolvePlayerGamepad(bindings); if (!gamepad) { 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::START].scancode = bindings.keyboard.key_start; - // 2. Resoldre gamepad per path/name/slot - std::shared_ptr gamepad = resolvePlayerGamepad(bindings, 1); + // 2. Resoldre gamepad per path/name + std::shared_ptr gamepad = resolvePlayerGamepad(bindings); if (!gamepad) { player2_gamepad_ = nullptr; diff --git a/source/core/input/input.hpp b/source/core/input/input.hpp index 9a5134a..6fdc628 100644 --- a/source/core/input/input.hpp +++ b/source/core/input/input.hpp @@ -148,8 +148,7 @@ class Input { auto removeGamepad(SDL_JoystickID id) -> std::string; void addGamepadMappingsFromFile(); void discoverGamepads(); - auto resolvePlayerGamepad(const Config::PlayerBindings& bindings, - std::size_t fallback_index) -> std::shared_ptr; + auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr; // --- Variables miembro --- static Input* instance; // Instancia única del singleton diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index f1ab997..4999fcd 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -109,6 +109,34 @@ Director::Director(int argc, char* argv[]) // Inicialitzar sistema de input 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 Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer2Bindings(cfg_->player2); diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 33198cc..fdd8913 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -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"; diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index fc99612..49cbca8 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -107,7 +107,11 @@ namespace System { [[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] auto buildOptionsPage() const -> 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 // l'usuari selecciona SI; el cursor per defecte apunta a NO. void pushConfirmPage(const std::string& title_key, std::function on_yes);