From 34be79192c2fe431f7a37e9b31de415160729a8a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:18:49 +0200 Subject: [PATCH] feat(service_menu): pagina CONTROLS amb assignacio de pad i rebind per jugador Afegeix submenu CONTROLS al menu de servei amb 2 items CYCLE per seleccionar el mando assignat a cada jugador (persistit per name + path) i 4 items ACTION per arrancar DefineInputs (teclat/mando per a P1/P2). Tambe afegeix: - Director: init/update/draw/destroy del singleton DefineInputs. - GlobalEvents: routing prioritari de tots els events a DefineInputs mentre l'overlay esta actiu. - Locale ca/en: claus del submenu CONTROLS i de l'overlay de rebind. Co-Authored-By: Claude Opus 4.7 (1M context) --- data/locale/ca.yaml | 27 ++++++ data/locale/en.yaml | 27 ++++++ source/core/system/director.cpp | 9 ++ source/core/system/global_events.cpp | 17 ++++ source/core/system/service_menu.cpp | 139 +++++++++++++++++++++++++++ source/core/system/service_menu.hpp | 1 + 6 files changed, 220 insertions(+) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 4bc25f2..0d410f4 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -84,3 +84,30 @@ service_menu: # Valors comuns value_on: "ACTIU" value_off: "INACTIU" + # Items del submenu CONTROLS + controls_pad_p1: "MANDO JUGADOR 1" + controls_pad_p2: "MANDO JUGADOR 2" + controls_no_pad: "SENSE MANDO" + controls_define_keyboard_p1: "REDEFINIR TECLES P1" + controls_define_keyboard_p2: "REDEFINIR TECLES P2" + controls_define_gamepad_p1: "REDEFINIR BOTONS P1" + controls_define_gamepad_p2: "REDEFINIR BOTONS P2" + +# Overlay modal de redefinicio (DefineInputs) +define: + title_keyboard_p1: "REDEFINIR TECLES P1" + title_keyboard_p2: "REDEFINIR TECLES P2" + title_gamepad_p1: "REDEFINIR BOTONS P1" + title_gamepad_p2: "REDEFINIR BOTONS P2" + press_key: "PREMEU UNA TECLA" + press_button: "PREMEU UN BOTO" + complete: "CONFIGURACIO COMPLETA" + no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR" + cancel_hint: "ESC PER CANCEL-LAR" + action: + left: "ESQUERRA" + right: "DRETA" + fire: "DISPAR" + accelerate: "ACCELERAR" + start: "START" + menu: "MENU" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index 77755c8..e35509a 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -83,3 +83,30 @@ service_menu: # Common values value_on: "ON" value_off: "OFF" + # Items of CONTROLS submenu + controls_pad_p1: "PLAYER 1 GAMEPAD" + controls_pad_p2: "PLAYER 2 GAMEPAD" + controls_no_pad: "NO GAMEPAD" + controls_define_keyboard_p1: "REDEFINE KEYS P1" + controls_define_keyboard_p2: "REDEFINE KEYS P2" + controls_define_gamepad_p1: "REDEFINE BUTTONS P1" + controls_define_gamepad_p2: "REDEFINE BUTTONS P2" + +# Modal overlay for input redefinition (DefineInputs) +define: + title_keyboard_p1: "REDEFINE KEYS P1" + title_keyboard_p2: "REDEFINE KEYS P2" + title_gamepad_p1: "REDEFINE BUTTONS P1" + title_gamepad_p2: "REDEFINE BUTTONS P2" + press_key: "PRESS A KEY" + press_button: "PRESS A BUTTON" + complete: "CONFIGURATION COMPLETE" + no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER" + cancel_hint: "ESC TO CANCEL" + action: + left: "LEFT" + right: "RIGHT" + fire: "FIRE" + accelerate: "ACCELERATE" + start: "START" + menu: "MENU" diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 36f8fd9..e5308d5 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -13,6 +13,7 @@ #include "core/audio/audio_adapter.hpp" #include "core/defaults/window.hpp" #include "core/graphics/shape_loader.hpp" +#include "core/input/define_inputs.hpp" #include "core/input/input.hpp" #include "core/input/mouse.hpp" #include "core/locale/locale.hpp" @@ -178,6 +179,7 @@ Director::Director(int argc, char* argv[]) System::Notifier::init(sdl_->getRenderer()); System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get()); + System::DefineInputs::init(sdl_->getRenderer()); last_ticks_ms_ = SDL_GetTicks(); } @@ -192,6 +194,7 @@ Director::~Director() { // l'hem de cridar nosaltres. current_scene_.reset(); debug_overlay_.reset(); + System::DefineInputs::destroy(); System::ServiceMenu::destroy(); System::Notifier::destroy(); context_.reset(); @@ -376,6 +379,9 @@ auto Director::iterate() -> SDL_AppResult { if (auto* menu = System::ServiceMenu::get(); menu != nullptr) { menu->update(delta_time); } + if (auto* di = System::DefineInputs::get(); di != nullptr) { + di->update(delta_time); + } Audio::update(); // Si la swapchain no està disponible (finestra minimitzada, etc.), @@ -392,6 +398,9 @@ auto Director::iterate() -> SDL_AppResult { if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) { menu->draw(); // service menu: per damunt fins i tot dels toasts } + if (const auto* di = System::DefineInputs::get(); di != nullptr) { + di->draw(); // overlay de rebind: per damunt de tot + } sdl_->present(); return SDL_APP_CONTINUE; } diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index 3b38575..2f8e6f5 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -5,6 +5,7 @@ #include +#include "core/input/define_inputs.hpp" #include "core/input/input.hpp" #include "core/input/mouse.hpp" #include "core/locale/locale.hpp" @@ -43,6 +44,16 @@ namespace GlobalEvents { return true; } + // Si l'overlay de redefinicio esta actiu, engoleix tots els events. + auto consumeIfDefineActive(const SDL_Event& event) -> bool { + auto* di = System::DefineInputs::get(); + if (di == nullptr || !di->isActive()) { + return false; + } + di->handleEvent(event); + return true; + } + // Botó MENU al mando d'algun jugador → alterna el menú de servei // (mateix comportament que F12 al teclat). Retorna true si l'event és // un GAMEPAD_BUTTON_DOWN consumit. @@ -83,6 +94,12 @@ namespace GlobalEvents { std::cout << "[Input] " << event_msg << '\n'; } + // 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events + // (cap arriba al joc, al menu de servei ni als hotkeys F1-F12). + if (consumeIfDefineActive(event)) { + return true; + } + // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego) if (event.type == SDL_EVENT_QUIT) { context.setNextScene(SceneType::EXIT); diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 6960e29..8575680 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -15,9 +15,12 @@ #include "core/defaults/audio.hpp" #include "core/defaults/rendering.hpp" #include "core/defaults/service_menu.hpp" +#include "core/input/define_inputs.hpp" +#include "core/input/input.hpp" #include "core/locale/locale.hpp" #include "core/rendering/sdl_manager.hpp" #include "core/system/debug_overlay.hpp" +#include "core/system/notifier.hpp" #include "core/system/relaunch.hpp" #include "core/types.hpp" #include "game/config_yaml.hpp" @@ -77,6 +80,84 @@ namespace { return Locale::get().text(item.label_key); } + // ---- Helpers de la pagina CONTROLS ---- + + auto padDisplayName(int player_index) -> std::string { + const auto* input = Input::get(); + if (input == nullptr) { + return Locale::get().text("service_menu.controls_no_pad"); + } + auto pad = input->getPlayerGamepad(player_index); + if (!pad) { + return Locale::get().text("service_menu.controls_no_pad"); + } + return pad->name; + } + + // Index actual del pad assignat dins de la llista de mandos detectats. + // Prioritat path > name. Si no n'hi ha cap match, retorna 0. + auto findAssignedIndex(const std::vector>& pads, + const Config::PlayerBindings& pcfg) -> std::size_t { + for (std::size_t i = 0; i < pads.size(); ++i) { + if (pads[i] && !pcfg.gamepad_path.empty() && pads[i]->path == pcfg.gamepad_path) { + return i; + } + } + for (std::size_t i = 0; i < pads.size(); ++i) { + if (pads[i] && !pcfg.gamepad_name.empty() && pads[i]->name == pcfg.gamepad_name) { + return i; + } + } + return 0; + } + + // Avança ciclicament l'assignacio de pad d'un jugador i la persisteix. + void cyclePlayerPad(int player_index, int dir) { + auto* input = Input::get(); + if (input == nullptr) { + return; + } + const auto& pads = input->getGamepads(); + if (pads.empty()) { + return; + } + auto& pcfg = (player_index == 0) + ? ConfigYaml::engine_config.player1 + : ConfigYaml::engine_config.player2; + + const std::size_t CURRENT = findAssignedIndex(pads, pcfg); + const std::size_t N = pads.size(); + const std::size_t STEP = (dir > 0) ? 1 : (N - 1); + const std::size_t NEXT = (CURRENT + STEP) % N; + if (!pads[NEXT]) { + return; + } + pcfg.gamepad_name = pads[NEXT]->name; + pcfg.gamepad_path = pads[NEXT]->path; + if (player_index == 0) { + input->applyPlayer1Bindings(ConfigYaml::engine_config.player1); + } else { + input->applyPlayer2Bindings(ConfigYaml::engine_config.player2); + } + ConfigYaml::saveToFile(); + } + + // Arranca un rebind o avisa si el jugador no te pad. Retorna true si el + // rebind ha començat (el caller ha de tancar el menu). + auto startDefine(System::DefineInputs::Mode mode, System::DefineInputs::Player pl) -> bool { + auto* di = System::DefineInputs::get(); + if (di == nullptr) { + return false; + } + if (!di->begin(mode, pl)) { + if (auto* n = System::Notifier::get(); n != nullptr) { + n->notifyWarn(Locale::get().text("define.no_gamepad")); + } + return false; + } + return true; + } + } // namespace namespace System { @@ -153,6 +234,7 @@ namespace System { makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }), makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }), makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }), + makeSubmenu("service_menu.controls", [this] { pushPage(buildControlsPage()); }), makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }), }; stack_.clear(); @@ -437,6 +519,63 @@ namespace System { return page; } + 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* menu) -> ServiceMenu::Item { + return ServiceMenu::Item{ + .kind = ServiceMenu::Kind::ACTION, + .label_key = label_key, + .label_text = {}, + .selectable = true, + .on_activate = [mode, pl, menu] { + if (startDefine(mode, pl) && menu != nullptr && menu->isOpen()) { + menu->toggle(); + } }, + .get_value_text = {}, + .on_change = {}, + }; + } + + } // namespace + + auto ServiceMenu::buildControlsPage() -> Page { + Page page; + page.title_key = "service_menu.controls"; + page.items = { + makeCyclePadItem("service_menu.controls_pad_p1", 0), + makeCyclePadItem("service_menu.controls_pad_p2", 1), + makeDefineItem("service_menu.controls_define_keyboard_p1", + DefineInputs::Mode::KEYBOARD, + DefineInputs::Player::P1, + this), + makeDefineItem("service_menu.controls_define_keyboard_p2", + DefineInputs::Mode::KEYBOARD, + DefineInputs::Player::P2, + this), + makeDefineItem("service_menu.controls_define_gamepad_p1", + DefineInputs::Mode::GAMEPAD, + DefineInputs::Player::P1, + this), + makeDefineItem("service_menu.controls_define_gamepad_p2", + DefineInputs::Mode::GAMEPAD, + DefineInputs::Player::P2, + this), + }; + 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 8b4b732..c1c9fe9 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -98,6 +98,7 @@ namespace System { [[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildSystemPage() -> Page; + [[nodiscard]] auto buildControlsPage() -> 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);