From e5a91825b1b3e8308c5b9345c58117094a034315 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 19:55:42 +0200 Subject: [PATCH 01/15] feat(input): notifica connexio/desconnexio de mandos via Notifier Co-Authored-By: Claude Opus 4.7 (1M context) --- data/locale/ca.yaml | 2 + data/locale/en.yaml | 2 + source/core/input/input.cpp | 91 +++++++++++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 71ccc66..4bc25f2 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -14,6 +14,8 @@ notification: postfx_on: "POSTPROCESSAT ACTIU" postfx_off: "POSTPROCESSAT INACTIU" locale_switched: "IDIOMA: {lang}" + gamepad_connected: "{name} CONNECTAT" + gamepad_disconnected: "{name} DESCONNECTAT" language: ca: "CATALA" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index f5fc757..77755c8 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -13,6 +13,8 @@ notification: postfx_on: "POSTPROCESS ON" postfx_off: "POSTPROCESS OFF" locale_switched: "LANGUAGE: {lang}" + gamepad_connected: "{name} CONNECTED" + gamepad_disconnected: "{name} DISCONNECTED" language: ca: "CATALAN" diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index a668690..e0e9e67 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -8,6 +8,9 @@ #include // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include // Para move +#include "core/locale/locale.hpp" +#include "core/system/notifier.hpp" + // Singleton Input* Input::instance = nullptr; @@ -407,6 +410,16 @@ auto Input::addGamepad(int device_index) -> std::string { auto name = gamepad->name; std::cout << "Gamepad connected (" << name << ")" << '\n'; gamepads_.push_back(std::move(gamepad)); + + // Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial + // (l'Input::init() es crida abans que el Director instanciï el Notifier). + if (auto* notifier = System::Notifier::get(); notifier != nullptr) { + notifier->notifyInfo(localeSubstitute( + Locale::get().text("notification.gamepad_connected"), + "{name}", + name)); + } + return name + " CONNECTED"; } @@ -419,6 +432,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { std::string name = (*it)->name; std::cout << "Gamepad disconnected (" << name << ")" << '\n'; gamepads_.erase(it); + + if (auto* notifier = System::Notifier::get(); notifier != nullptr) { + notifier->notifyInfo(localeSubstitute( + Locale::get().text("notification.gamepad_disconnected"), + "{name}", + name)); + } + return name + " DISCONNECTED"; } std::cerr << "No se encontró el gamepad con ID " << id << '\n'; @@ -465,6 +486,39 @@ 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 { + if (gamepads_.empty()) { + return nullptr; + } + + if (!bindings.gamepad_path.empty()) { + for (const auto& pad : gamepads_) { + if (pad && pad->path == bindings.gamepad_path) { + return pad; + } + } + } + + if (!bindings.gamepad_name.empty()) { + for (const auto& pad : gamepads_) { + if (pad && pad->name == bindings.gamepad_name) { + return pad; + } + } + } + + // 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; +} + // Aplica configuración de controles del player 1 void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { // 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) @@ -474,15 +528,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. Encontrar gamepad por nombre (o usar primer gamepad como fallback) - std::shared_ptr gamepad = nullptr; - if (bindings.gamepad_name.empty()) { - // Fallback: usar primer gamepad disponible - gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr; - } else { - // Buscar por nombre - gamepad = findAvailableGamepadByName(bindings.gamepad_name); - } + // 2. Resoldre gamepad per path/name/slot + std::shared_ptr gamepad = resolvePlayerGamepad(bindings, 0); if (!gamepad) { player1_gamepad_ = nullptr; @@ -494,6 +541,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; + gamepad->bindings[Action::START].button = bindings.gamepad.button_start; + gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu; // 4. Cachear referencia player1_gamepad_ = gamepad; @@ -508,15 +557,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. Encontrar gamepad por nombre (o usar segundo gamepad como fallback) - std::shared_ptr gamepad = nullptr; - if (bindings.gamepad_name.empty()) { - // Fallback: usar segundo gamepad disponible - gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr; - } else { - // Buscar por nombre - gamepad = findAvailableGamepadByName(bindings.gamepad_name); - } + // 2. Resoldre gamepad per path/name/slot + std::shared_ptr gamepad = resolvePlayerGamepad(bindings, 1); if (!gamepad) { player2_gamepad_ = nullptr; @@ -528,6 +570,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) { gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; + gamepad->bindings[Action::START].button = bindings.gamepad.button_start; + gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu; // 4. Cachear referencia player2_gamepad_ = gamepad; @@ -555,6 +599,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool { return keyboard_active || gamepad_active; } +// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr. +auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr { + if (player_index == 0) { + return player1_gamepad_; + } + if (player_index == 1) { + return player2_gamepad_; + } + return nullptr; +} + // Consulta de input para player 2 auto Input::checkActionPlayer2(Action action, bool repeat) -> bool { // Comprobar teclado con el mapa específico de P2 From 3e8f2f35bf250ce39c0c06fb3e06b3628fc78683 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 19:56:59 +0200 Subject: [PATCH 02/15] feat(input): accio MENU i assignacio de mando per path + name Afegeix l'accio MENU a InputAction (obre el menu de servei des del mando, equivalent a F12 al teclat) i els camps gamepad.button_start i gamepad.button_menu al config per jugador. Tambe afegeix gamepad_path per distingir dos mandos del mateix model i prioritza path > name > slot a applyPlayerNBindings via el nou resolvePlayerGamepad. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/config/engine_config.hpp | 3 +++ source/core/input/input.hpp | 10 +++++++- source/core/input/input_types.cpp | 4 ++++ source/core/input/input_types.hpp | 1 + source/core/system/global_events.cpp | 36 ++++++++++++++++++++++++++++ source/game/config_yaml.cpp | 32 +++++++++++++++++++++---- source/game/config_yaml.hpp | 1 + 7 files changed, 82 insertions(+), 5 deletions(-) diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index a9a3f7d..29680a2 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -48,12 +48,15 @@ namespace Config { int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button + int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button + int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei }; struct PlayerBindings { KeyboardBindings keyboard{}; GamepadBindings gamepad{}; std::string gamepad_name; // Empty = auto-assign by index + std::string gamepad_path; // Prioritari sobre name per distingir mateixos models }; struct AudioConfig { diff --git a/source/core/input/input.hpp b/source/core/input/input.hpp index 541c3b4..9a5134a 100644 --- a/source/core/input/input.hpp +++ b/source/core/input/input.hpp @@ -62,7 +62,9 @@ class Input { {Action::LEFT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {Action::RIGHT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, {Action::THRUST, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_WEST)}}, - {Action::SHOOT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_SOUTH)}}} {} + {Action::SHOOT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_SOUTH)}}, + {Action::START, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_START)}}, + {Action::MENU, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_BACK)}}} {} ~Gamepad() { if (pad != nullptr) { @@ -107,6 +109,10 @@ class Input { auto checkActionPlayer1(Action action, bool repeat = true) -> bool; auto checkActionPlayer2(Action action, bool repeat = true) -> bool; + // Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi + // ha cap d'assignat o connectat. Usat per la UI de redefinició de botons. + [[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr; + // Check if any player pressed any action from a list auto checkAnyPlayerAction(const std::span& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; @@ -142,6 +148,8 @@ 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; // --- Variables miembro --- static Input* instance; // Instancia única del singleton diff --git a/source/core/input/input_types.cpp b/source/core/input/input_types.cpp index d4d4008..a425b59 100644 --- a/source/core/input/input_types.cpp +++ b/source/core/input/input_types.cpp @@ -6,6 +6,8 @@ const std::unordered_map ACTION_TO_STRING = { {InputAction::RIGHT, "RIGHT"}, {InputAction::THRUST, "THRUST"}, {InputAction::SHOOT, "SHOOT"}, + {InputAction::START, "START"}, + {InputAction::MENU, "MENU"}, {InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"}, {InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"}, {InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"}, @@ -18,6 +20,8 @@ const std::unordered_map STRING_TO_ACTION = { {"RIGHT", InputAction::RIGHT}, {"THRUST", InputAction::THRUST}, {"SHOOT", InputAction::SHOOT}, + {"START", InputAction::START}, + {"MENU", InputAction::MENU}, {"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM}, {"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM}, {"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN}, diff --git a/source/core/input/input_types.hpp b/source/core/input/input_types.hpp index 40a7997..0f5d0db 100644 --- a/source/core/input/input_types.hpp +++ b/source/core/input/input_types.hpp @@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j THRUST, // Acelerar SHOOT, // Disparar START, // Empezar match + MENU, // Abrir/cerrar menu de servicio (equivalent a F12) // Inputs de sistema (globales) WINDOW_INC_ZOOM, // F2 diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index 6b06f25..3b38575 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -43,6 +43,37 @@ namespace GlobalEvents { 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. + auto handleGamepadMenuButton(const SDL_Event& event) -> bool { + if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + return false; + } + auto* input = Input::get(); + if (input == nullptr) { + return false; + } + auto match_player = [&](int player_index) { + auto pad = input->getPlayerGamepad(player_index); + if (!pad || pad->instance_id != event.gbutton.which) { + return false; + } + auto it = pad->bindings.find(InputAction::MENU); + if (it == pad->bindings.end()) { + return false; + } + return it->second.button == static_cast(event.gbutton.button); + }; + if (!match_player(0) && !match_player(1)) { + return false; + } + if (auto* menu = System::ServiceMenu::get(); menu != nullptr) { + menu->toggle(); + } + return true; + } + } // namespace auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool { @@ -62,6 +93,11 @@ namespace GlobalEvents { // 3. Gestió del ratolí (auto-ocultar) Mouse::handleEvent(event); + // 3b. Botó MENU al mando (equivalent a F12) + if (handleGamepadMenuButton(event)) { + return true; + } + // 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de // funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen, // vsync, AA, postfx, locale, exit prompt). Aixi el menu captura diff --git a/source/game/config_yaml.cpp b/source/game/config_yaml.cpp index 4c1d9ce..43a821a 100644 --- a/source/game/config_yaml.cpp +++ b/source/game/config_yaml.cpp @@ -373,12 +373,21 @@ namespace ConfigYaml { if (gp.contains("button_shoot")) { player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); } + if (gp.contains("button_start")) { + player1.gamepad.button_start = stringToButton(gp["button_start"].get_value()); + } + if (gp.contains("button_menu")) { + player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value()); + } } - // Carregar nom del gamepad + // Carregar nom i path del gamepad assignat if (p1.contains("gamepad_name")) { player1.gamepad_name = p1["gamepad_name"].get_value(); } + if (p1.contains("gamepad_path")) { + player1.gamepad_path = p1["gamepad_path"].get_value(); + } } // Carregar controls del player 2 desde YAML @@ -421,12 +430,21 @@ namespace ConfigYaml { if (gp.contains("button_shoot")) { player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); } + if (gp.contains("button_start")) { + player2.gamepad.button_start = stringToButton(gp["button_start"].get_value()); + } + if (gp.contains("button_menu")) { + player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value()); + } } - // Carregar nom del gamepad + // Carregar nom i path del gamepad assignat if (p2.contains("gamepad_name")) { player2.gamepad_name = p2["gamepad_name"].get_value(); } + if (p2.contains("gamepad_path")) { + player2.gamepad_path = p2["gamepad_path"].get_value(); + } } // Carregar configuración des del file YAML @@ -531,7 +549,10 @@ namespace ConfigYaml { file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n"; - file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n"; + file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n"; + file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n"; + file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n"; + file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n"; } // Guardar controls del player 2 a YAML @@ -548,7 +569,10 @@ namespace ConfigYaml { file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n"; - file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n"; + file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n"; + file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n"; + file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n"; + file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n"; } // Guardar configuración al file YAML diff --git a/source/game/config_yaml.hpp b/source/game/config_yaml.hpp index 79ae832..27e00b4 100644 --- a/source/game/config_yaml.hpp +++ b/source/game/config_yaml.hpp @@ -28,6 +28,7 @@ namespace ConfigYaml { .key_start = SDL_SCANCODE_2, }, .gamepad_name = "", + .gamepad_path = "", }, }; From fcf13591be4cdc31e21cba69a70fe1733cb28219 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:17:30 +0200 Subject: [PATCH 03/15] feat(input): modul DefineInputs per redefinir teclat i mando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Singleton inspirat en aee_arcade DefineButtons: pinta una caixa central modal, captura events SDL i avança per una sequencia fixa d'accions per jugador. Teclat: LEFT/RIGHT/FIRE/ACCELERATE. Mando: FIRE/ACCELERATE/ START/MENU. ESC cancel-la, duplicats dins la sessio es rebutgen. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/define_inputs.cpp | 401 ++++++++++++++++++++++++++++ source/core/input/define_inputs.hpp | 107 ++++++++ 2 files changed, 508 insertions(+) create mode 100644 source/core/input/define_inputs.cpp create mode 100644 source/core/input/define_inputs.hpp diff --git a/source/core/input/define_inputs.cpp b/source/core/input/define_inputs.cpp new file mode 100644 index 0000000..8f46847 --- /dev/null +++ b/source/core/input/define_inputs.cpp @@ -0,0 +1,401 @@ +// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio +// © 2026 JailDesigner + +#include "core/input/define_inputs.hpp" + +#include +#include +#include +#include +#include + +#include "core/defaults/service_menu.hpp" +#include "core/input/input.hpp" +#include "core/locale/locale.hpp" +#include "core/types.hpp" +#include "game/config_yaml.hpp" + +namespace { + + constexpr float CANVAS_W = 1280.0F; + constexpr float CANVAS_H = 720.0F; + + // Llindar de trigger per a edge-detect L2/R2 com a boto virtual. + constexpr Sint16 TRIGGER_THRESHOLD = 16384; + + // Codis virtuals per als triggers (consistents amb input_types.cpp). + constexpr int TRIGGER_L2_VIRTUAL = 100; + constexpr int TRIGGER_R2_VIRTUAL = 101; + + // Durada del missatge de confirmacio abans de tancar-se. + constexpr float COMPLETE_DISPLAY_S = 1.5F; + + // Llindar dpad als axis sticks: no es captura per evitar conflicte amb el + // moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando). + constexpr Sint16 STICK_THRESHOLD = 16384; + + // Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]). + void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) { + renderer->pushRect(x, y, w, h, static_cast(color.r) / 255.0F, static_cast(color.g) / 255.0F, static_cast(color.b) / 255.0F, static_cast(color.a) / 255.0F); + } + + auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string { + const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD); + const bool IS_P1 = (player == System::DefineInputs::Player::P1); + if (IS_KB && IS_P1) { + return "define.title_keyboard_p1"; + } + if (IS_KB) { + return "define.title_keyboard_p2"; + } + if (IS_P1) { + return "define.title_gamepad_p1"; + } + return "define.title_gamepad_p2"; + } + + // Scancodes que MAI capturem com a binding (reservats per a navegacio o + // global hotkeys). Evita que l'usuari es bloquegi ell mateix. + auto isReservedScancode(SDL_Scancode sc) -> bool { + if (sc == SDL_SCANCODE_ESCAPE) { + return true; // ESC sempre cancel·la + } + if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) { + return true; // F1-F12 globals + } + if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) { + return true; + } + if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) { + return true; + } + return false; + } + + // Conversio sense pèrdua de SDL_Scancode → int per a comparacions + // homogenies dins de sequence_ (que guarda codis de tots dos modes). + auto scancodeToInt(SDL_Scancode sc) -> int { + return static_cast(sc); + } + +} // namespace + +namespace System { + + std::unique_ptr DefineInputs::instance; + + void DefineInputs::init(Rendering::Renderer* renderer) { + if (!instance) { + instance = std::unique_ptr(new DefineInputs(renderer)); + } + } + + void DefineInputs::destroy() { instance.reset(); } + + auto DefineInputs::get() -> DefineInputs* { return instance.get(); } + + DefineInputs::DefineInputs(Rendering::Renderer* renderer) + : renderer_(renderer), + text_(renderer) {} + + auto DefineInputs::isActive() const -> bool { + return phase_ != Phase::INACTIVE; + } + + auto DefineInputs::begin(Mode mode, Player player) -> bool { + if (mode == Mode::GAMEPAD) { + // Requereix un pad assignat al jugador. + const auto* input = Input::get(); + if (input == nullptr) { + return false; + } + const int IDX = (player == Player::P1) ? 0 : 1; + if (input->getPlayerGamepad(IDX) == nullptr) { + return false; + } + } + + mode_ = mode; + player_ = player; + index_ = 0; + complete_timer_s_ = 0.0F; + l2_was_pressed_ = false; + r2_was_pressed_ = false; + + buildSequence(); + phase_ = Phase::CAPTURING; + return true; + } + + void DefineInputs::cancel() { + phase_ = Phase::INACTIVE; + sequence_.clear(); + index_ = 0; + complete_timer_s_ = 0.0F; + } + + void DefineInputs::buildSequence() { + sequence_.clear(); + if (mode_ == Mode::KEYBOARD) { + // Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST) + sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1}); + } else { + // Mando: FIRE, ACCELERATE, START, MENU + sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1}); + } + } + + auto DefineInputs::isInUse(int code) const -> bool { + return std::ranges::any_of(sequence_, [code](const Step& s) { + return s.captured == code; + }); + } + + void DefineInputs::captureAndAdvance(int code) { + if (index_ >= sequence_.size()) { + return; + } + if (isInUse(code)) { + return; // Duplicat dins de la sessio: rebutgem silenciosament + } + sequence_[index_].captured = code; + ++index_; + if (index_ >= sequence_.size()) { + persistAndComplete(); + } + } + + void DefineInputs::persistAndComplete() { + auto& cfg = (player_ == Player::P1) + ? ConfigYaml::engine_config.player1 + : ConfigYaml::engine_config.player2; + + if (mode_ == Mode::KEYBOARD) { + for (const Step& s : sequence_) { + switch (s.action) { + case InputAction::LEFT: + cfg.keyboard.key_left = static_cast(s.captured); + break; + case InputAction::RIGHT: + cfg.keyboard.key_right = static_cast(s.captured); + break; + case InputAction::SHOOT: + cfg.keyboard.key_shoot = static_cast(s.captured); + break; + case InputAction::THRUST: + cfg.keyboard.key_thrust = static_cast(s.captured); + break; + default: + break; // START / MENU no es redefineixen al teclat + } + } + } else { + for (const Step& s : sequence_) { + switch (s.action) { + case InputAction::SHOOT: + cfg.gamepad.button_shoot = s.captured; + break; + case InputAction::THRUST: + cfg.gamepad.button_thrust = s.captured; + break; + case InputAction::START: + cfg.gamepad.button_start = s.captured; + break; + case InputAction::MENU: + cfg.gamepad.button_menu = s.captured; + break; + default: + break; // LEFT / RIGHT no es redefineixen al mando + } + } + } + + // Aplicar canvis al runtime de l'Input i persistir a disc. + if (auto* input = Input::get(); input != nullptr) { + if (player_ == Player::P1) { + input->applyPlayer1Bindings(ConfigYaml::engine_config.player1); + } else { + input->applyPlayer2Bindings(ConfigYaml::engine_config.player2); + } + } + ConfigYaml::saveToFile(); + + phase_ = Phase::COMPLETE; + complete_timer_s_ = COMPLETE_DISPLAY_S; + } + + void DefineInputs::update(float delta_time) { + if (phase_ != Phase::COMPLETE) { + return; + } + complete_timer_s_ -= delta_time; + if (complete_timer_s_ <= 0.0F) { + cancel(); + } + } + + void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) { + if (now && !was_pressed) { + captureAndAdvance(virtual_button); + } + was_pressed = now; + } + + auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool { + if (event.type != SDL_EVENT_KEY_DOWN) { + return true; // Empassem la resta sense fer res + } + const SDL_Scancode SC = event.key.scancode; + if (SC == SDL_SCANCODE_ESCAPE) { + cancel(); + return true; + } + if (isReservedScancode(SC)) { + return true; // Ignorada + } + captureAndAdvance(scancodeToInt(SC)); + return true; + } + + auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool { + // ESC al teclat tambe cancel·la durant rebind del mando. + if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { + cancel(); + return true; + } + + // Filtrar events al pad del jugador actiu. + const auto* input = Input::get(); + if (input == nullptr) { + return true; + } + const int IDX = (player_ == Player::P1) ? 0 : 1; + auto pad = input->getPlayerGamepad(IDX); + if (!pad) { + return true; + } + + if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + if (event.gbutton.which != pad->instance_id) { + return true; + } + captureAndAdvance(static_cast(event.gbutton.button)); + return true; + } + + if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + if (event.gaxis.which != pad->instance_id) { + return true; + } + const auto AXIS = static_cast(event.gaxis.axis); + const Sint16 VAL = event.gaxis.value; + if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { + processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD); + } else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD); + } + // Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles). + (void)STICK_THRESHOLD; + return true; + } + + return true; + } + + auto DefineInputs::handleEvent(const SDL_Event& event) -> bool { + if (phase_ == Phase::INACTIVE) { + return false; + } + if (phase_ == Phase::COMPLETE) { + // Mentre mostrem el missatge OK, empassem els events sense capturar. + return true; + } + if (mode_ == Mode::KEYBOARD) { + return handleKeyboardEvent(event); + } + return handleGamepadEvent(event); + } + + void DefineInputs::draw() const { + if (phase_ == Phase::INACTIVE) { + return; + } + + using namespace Defaults::ServiceMenu; + + // Caixa centrada, dimensions fixes (no depen del contingut a redefinir). + constexpr float BOX_W = 560.0F; + constexpr float BOX_H = 280.0F; + const float BOX_X = (CANVAS_W - BOX_W) * 0.5F; + const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F; + + // Fons + brackets als 4 cantons (estil HUD del menu de servei). + fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR); + + const auto T = static_cast(CORNER_THICKNESS); + const auto AH = static_cast(CORNER_ARM_H); + const auto AV = static_cast(CORNER_ARM_V); + fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR); + + const std::string TITLE = Locale::get().text(titleKey(mode_, player_)); + const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING); + const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F); + const float TITLE_Y = BOX_Y + 26.0F; + text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR); + + if (phase_ == Phase::COMPLETE) { + const std::string OK = Locale::get().text("define.complete"); + constexpr float OK_SCALE = 0.7F; + const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING); + const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F); + const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F; + constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255}; + text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR); + return; + } + + // Instruccio (premeu tecla / boto) + accio actual + progres. + const std::string PROMPT = Locale::get().text( + mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button"); + const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING); + const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F); + const float PROMPT_Y = BOX_Y + 86.0F; + text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR); + + if (index_ < sequence_.size()) { + const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key); + constexpr float ACTION_SCALE = 0.9F; + const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING); + const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F); + const float ACTION_Y = BOX_Y + 130.0F; + text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR); + } + + const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size()); + constexpr float PROG_SCALE = 0.4F; + const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING); + const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F); + const float PROG_Y = BOX_Y + 200.0F; + text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR); + + const std::string CANCEL_HINT = Locale::get().text("define.cancel_hint"); + const float CH_W = Graphics::VectorText::getTextWidth(CANCEL_HINT, PROG_SCALE, TEXT_SPACING); + const float CH_X = BOX_X + ((BOX_W - CH_W) * 0.5F); + const float CH_Y = BOX_Y + BOX_H - 30.0F; + text_.render(CANCEL_HINT, Vec2{.x = CH_X, .y = CH_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, SEPARATOR_COLOR); + } + +} // namespace System diff --git a/source/core/input/define_inputs.hpp b/source/core/input/define_inputs.hpp new file mode 100644 index 0000000..e117316 --- /dev/null +++ b/source/core/input/define_inputs.hpp @@ -0,0 +1,107 @@ +// define_inputs.hpp - Overlay modal de redefinici de controls (singleton) +// © 2026 JailDesigner +// +// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el +// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest +// singleton pren el control: pinta una caixa central, captura events SDL i +// avança per una seqüència fixa d'accions, persistint les noves assignacions +// a config.yaml en acabar. +// +// Cicle de vida: +// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa +// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad. +// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense +// desar; duplicats dins de la sessió es rebutgen silenciosament. +// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile, +// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s +// abans d'auto-tancar-se. +// +// El routing d'events es fa des de GlobalEvents::handle: mentre isActive() +// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni +// al menú de servei. + +#pragma once + +#include + +#include +#include +#include +#include + +#include "core/graphics/vector_text.hpp" +#include "core/input/input_types.hpp" +#include "core/rendering/render_context.hpp" + +namespace System { + + class DefineInputs { + public: + enum class Mode : std::uint8_t { KEYBOARD, + GAMEPAD }; + enum class Player : std::uint8_t { P1, + P2 }; + + static void init(Rendering::Renderer* renderer); + static void destroy(); + [[nodiscard]] static auto get() -> DefineInputs*; + + // Comença la sessió. Retorna false per a GAMEPAD si el jugador no té + // cap pad assignat (el caller hauria de notificar a l'usuari abans). + auto begin(Mode mode, Player player) -> bool; + void cancel(); + + [[nodiscard]] auto isActive() const -> bool; + + void update(float delta_time); + void draw() const; + + // Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay + // és actiu sempre consumeix tot per evitar passages al joc o menú). + auto handleEvent(const SDL_Event& event) -> bool; + + private: + explicit DefineInputs(Rendering::Renderer* renderer); + + enum class Phase : std::uint8_t { + INACTIVE, + CAPTURING, + COMPLETE, // mostra missatge OK breu abans d'auto-cancel + }; + + struct Step { + std::string action_label_key; // p.ex. "define.action.left" + InputAction action; // mapeig a la struct PlayerBindings + int captured{-1}; // scancode o button code; -1 = sense capturar + }; + + void buildSequence(); + [[nodiscard]] auto isInUse(int code) const -> bool; + void captureAndAdvance(int code); + void persistAndComplete(); + + // Handlers especialitzats segons mode_. + auto handleKeyboardEvent(const SDL_Event& event) -> bool; + auto handleGamepadEvent(const SDL_Event& event) -> bool; + + // Edge-detect per als triggers L2/R2 com a botons virtuals. + void processTrigger(int virtual_button, bool& was_pressed, bool now); + + Rendering::Renderer* renderer_; + Graphics::VectorText text_; + + Phase phase_{Phase::INACTIVE}; + Mode mode_{Mode::KEYBOARD}; + Player player_{Player::P1}; + std::vector sequence_; + std::size_t index_{0}; + float complete_timer_s_{0.0F}; + + // Estat d'edge-detect dels triggers durant la sessió GAMEPAD. + bool l2_was_pressed_{false}; + bool r2_was_pressed_{false}; + + static std::unique_ptr instance; + }; + +} // namespace System From 34be79192c2fe431f7a37e9b31de415160729a8a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:18:49 +0200 Subject: [PATCH 04/15] 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); From 10a54aef91bc3e1d828b763fafbc012178ba9fbe Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:33:01 +0200 Subject: [PATCH 05/15] fix(ui): nom del mando en majuscules a la UI sense modificar el config VectorText nomes admet ASCII en majuscules; els noms dels mandos (i el git hash) passaven pel toUpperAscii local del service_menu, pero les notificacions de hot-plug i el text del CYCLE de la pagina CONTROLS es mostraven amb el case original. Mou el helper a un utils compartit i l'aplica a tots els punts de display sense tocar gamepad_name al config. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/input.cpp | 5 +++-- source/core/system/service_menu.cpp | 16 +++------------- source/core/utils/string_utils.hpp | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 source/core/utils/string_utils.hpp diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index e0e9e67..670e3c7 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -10,6 +10,7 @@ #include "core/locale/locale.hpp" #include "core/system/notifier.hpp" +#include "core/utils/string_utils.hpp" // Singleton Input* Input::instance = nullptr; @@ -417,7 +418,7 @@ auto Input::addGamepad(int device_index) -> std::string { notifier->notifyInfo(localeSubstitute( Locale::get().text("notification.gamepad_connected"), "{name}", - name)); + Utils::toUpperAscii(name))); } return name + " CONNECTED"; @@ -437,7 +438,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { notifier->notifyInfo(localeSubstitute( Locale::get().text("notification.gamepad_disconnected"), "{name}", - name)); + Utils::toUpperAscii(name))); } return name + " DISCONNECTED"; diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 8575680..2dc6f25 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -23,6 +23,7 @@ #include "core/system/notifier.hpp" #include "core/system/relaunch.hpp" #include "core/types.hpp" +#include "core/utils/string_utils.hpp" #include "game/config_yaml.hpp" #include "project.h" @@ -57,17 +58,6 @@ namespace { } } - // VectorText nomes admet ASCII en majuscules. El git hash sortit de git - // rev-parse es lowercase (a-f), aixi que el passem a uppercase per al - // display sense modificar Project::GIT_HASH. - auto toUpperAscii(const std::string& s) -> std::string { - std::string result = s; - for (char& c : result) { - c = static_cast(std::toupper(static_cast(c))); - } - return result; - } - // Resol el text del label d'un item: prioritza label_text (literal) sobre // label_key (locale). Retorna cadena buida si tots dos son buits. auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string { @@ -91,7 +81,7 @@ namespace { if (!pad) { return Locale::get().text("service_menu.controls_no_pad"); } - return pad->name; + return Utils::toUpperAscii(pad->name); } // Index actual del pad assignat dins de la llista de mandos detectats. @@ -582,7 +572,7 @@ namespace System { // Versio + hash com a subtitol sota el titol (apagat, mes petit). // Uppercase del hash perque VectorText nomes admet majuscules. page.subtitle_provider = [] { - return std::format("V{} - {}", Project::VERSION, toUpperAscii(Project::GIT_HASH)); + return std::format("V{} - {}", Project::VERSION, Utils::toUpperAscii(Project::GIT_HASH)); }; page.items = { // REINICIAR (amb confirmacio). diff --git a/source/core/utils/string_utils.hpp b/source/core/utils/string_utils.hpp new file mode 100644 index 0000000..157a963 --- /dev/null +++ b/source/core/utils/string_utils.hpp @@ -0,0 +1,23 @@ +// string_utils.hpp - Utilitats genèriques de cadenes +// © 2026 JailDesigner +// +// VectorText només admet ASCII en majúscules; les notificacions, el menú +// de servei i l'overlay de rebind passen els textos dinàmics per aquest +// helper abans de pintar-los. + +#pragma once + +#include +#include + +namespace Utils { + + inline auto toUpperAscii(const std::string& s) -> std::string { + std::string result = s; + for (char& c : result) { + c = static_cast(std::toupper(static_cast(c))); + } + return result; + } + +} // namespace Utils From c4933875ddc7ca4e4951b40a822b4943612be516 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:36:51 +0200 Subject: [PATCH 06/15] fix(input): impedeix que els events traspassin al joc en acabar el rebind El menu de servei queda obert per sota de l'overlay DefineInputs durant tot el rebind (en lloc de tancar-se al activar la accio), de manera que absorbeix qualsevol KEY_DOWN que arribi un cop l'overlay s'auto-cancela. La pantalla de titol tambe pausa la seua logica mentre el menu de servei esta obert, igual que GameScene, per evitar que detecti un START fantasma si l'usuari encara te una tecla pulsada al moment de tancar-se el modal. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/system/service_menu.cpp | 22 +++++++++------------- source/core/system/service_menu.hpp | 2 +- source/game/scenes/title_scene.cpp | 8 ++++++++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 2dc6f25..45c63f8 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -523,16 +523,16 @@ namespace System { }; } - auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl, ServiceMenu* menu) -> ServiceMenu::Item { + auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> 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(); - } }, + // El menu de servei NO es tanca: queda obert per sota de + // l'overlay i absorbira qualsevol event que arribi un cop + // l'overlay s'haja auto-cancel·lat. + .on_activate = [mode, pl] { startDefine(mode, pl); }, .get_value_text = {}, .on_change = {}, }; @@ -548,20 +548,16 @@ namespace System { makeCyclePadItem("service_menu.controls_pad_p2", 1), makeDefineItem("service_menu.controls_define_keyboard_p1", DefineInputs::Mode::KEYBOARD, - DefineInputs::Player::P1, - this), + DefineInputs::Player::P1), makeDefineItem("service_menu.controls_define_keyboard_p2", DefineInputs::Mode::KEYBOARD, - DefineInputs::Player::P2, - this), + DefineInputs::Player::P2), makeDefineItem("service_menu.controls_define_gamepad_p1", DefineInputs::Mode::GAMEPAD, - DefineInputs::Player::P1, - this), + DefineInputs::Player::P1), makeDefineItem("service_menu.controls_define_gamepad_p2", DefineInputs::Mode::GAMEPAD, - DefineInputs::Player::P2, - this), + DefineInputs::Player::P2), }; return page; } diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index c1c9fe9..0fa1538 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -98,7 +98,7 @@ namespace System { [[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildSystemPage() -> Page; - [[nodiscard]] auto buildControlsPage() -> Page; + [[nodiscard]] static 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); diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 6e57169..238c1bf 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -18,6 +18,7 @@ #include "core/math/easing.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/scene_context.hpp" +#include "core/system/service_menu.hpp" #include "project.h" using SceneManager::SceneContext; @@ -294,6 +295,13 @@ auto TitleScene::isFinished() const -> bool { } void TitleScene::update(float delta_time) { + // Pausa global: mentre el menu de servei esta obert (i, per tant, el + // sub-overlay de rebind tambe, si esta actiu), congelem la logica de la + // pantalla de titol per no consumir un START fantasma quan l'overlay + // s'auto-tanca i les tecles encara s'estan pulsant. + if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) { + return; + } if (starfield_) { starfield_->update(delta_time); } From 2e74fea2d565cda12d5a265a4d2792b97d4fc748 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:38:26 +0200 Subject: [PATCH 07/15] feat(input): stick com a font alternativa de LEFT/RIGHT al mando LEFT i RIGHT no son redefinibles al mando i s'assumeix dpad O stick. Input::update() ara llegeix SDL_GAMEPAD_AXIS_LEFTX i fa OR amb l'estat del dpad: qualsevol de les dos fonts dispara l'accio. Llindar 30000 (coherent amb el constant AXIS_THRESHOLD ja existent). Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/input.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 670e3c7..cc19d43 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -377,9 +377,25 @@ void Input::update() { // --- MANDOS --- for (const auto& gamepad : gamepads_) { + // LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick). + // Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol + // de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000). + const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX); + const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD; + const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD; + for (auto& binding : gamepad->bindings) { bool button_is_down_now = static_cast(SDL_GetGamepadButton(gamepad->pad, static_cast(binding.second.button))) != 0; + // Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions + // (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol + // dispar amb trigger L2/R2 cal binding amb codi 100/101. + if (binding.first == Action::LEFT) { + button_is_down_now = button_is_down_now || STICK_LEFT; + } else if (binding.first == Action::RIGHT) { + button_is_down_now = button_is_down_now || STICK_RIGHT; + } + // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo binding.second.just_pressed = button_is_down_now && !binding.second.is_held; binding.second.is_held = button_is_down_now; From a4b567588ffadb310dc5689c63df50dc4aa77b91 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:42:33 +0200 Subject: [PATCH 08/15] feat(service_menu): navegacio amb mando (dpad, stick, fire = enter, accelerate = back) ServiceMenu::handleEvent ara accepta tambe SDL_EVENT_GAMEPAD_BUTTON_DOWN i SDL_EVENT_GAMEPAD_AXIS_MOTION. Mapeig: dpad UP/DOWN/LEFT/RIGHT mouen el cursor, el boto FIRE configurat per qualsevol jugador equival a ENTER (activa l'item), ACCELERATE equival a BACK (popPage). El stick esquerre fa nav amb edge-detect: cal tornar a centre per disparar una altra entrada. GlobalEvents::forwardToServiceMenu envia tots aquests events al menu quan esta obert. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/system/global_events.cpp | 33 ++++---- source/core/system/service_menu.cpp | 108 ++++++++++++++++++++++++++- source/core/system/service_menu.hpp | 22 +++++- 3 files changed, 146 insertions(+), 17 deletions(-) diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index 2f8e6f5..ad4e2df 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -23,25 +23,32 @@ namespace GlobalEvents { namespace { - // Reenvia el KEY_DOWN al menu de servei si esta obert i la tecla no - // es F1-F12 ni ESC (que sempre passen com a globals). Retorna true si - // el menu l'ha consumit. + // Reenvia events al menu de servei si esta obert. Accepta: + // - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals) + // - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE) + // - GAMEPAD_AXIS_MOTION (per navegacio amb stick) + // Retorna true si l'event s'ha entregat al menu. auto forwardToServiceMenu(const SDL_Event& event) -> bool { - if (event.type != SDL_EVENT_KEY_DOWN) { - return false; - } auto* menu = System::ServiceMenu::get(); if (menu == nullptr || !menu->isOpen()) { return false; } - const SDL_Scancode SC = event.key.scancode; - const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) || - (SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12); - if (PASSTHROUGH) { - return false; + if (event.type == SDL_EVENT_KEY_DOWN) { + const SDL_Scancode SC = event.key.scancode; + const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) || + (SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12); + if (PASSTHROUGH) { + return false; + } + menu->handleEvent(event); + return true; } - menu->handleEvent(event); - return true; + if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || + event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + menu->handleEvent(event); + return true; + } + return false; } // Si l'overlay de redefinicio esta actiu, engoleix tots els events. diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 45c63f8..b3b520b 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -718,10 +718,52 @@ namespace System { } } - auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool { - if (!open_ || stack_.empty() || event.type != SDL_EVENT_KEY_DOWN) { + namespace { + + // Llindar de stick per a navegacio de menu (mig camp del rang ±32767). + // Mes baix que el del joc (30000) per a una resposta mes agil al menu. + constexpr Sint16 MENU_STICK_THRESHOLD = 16384; + + // Retorna true si el codi de boto SDL coincideix amb l'accio + // configurada per algun dels dos jugadors (es a dir, el boto te el + // mateix codi al binding de FIRE o ACCELERATE del pad emissor). + auto buttonMatchesAction(SDL_JoystickID which, int button, InputAction action) -> bool { + const auto* input = Input::get(); + if (input == nullptr) { + return false; + } + for (int i = 0; i < 2; ++i) { + auto pad = input->getPlayerGamepad(i); + if (!pad || pad->instance_id != which) { + continue; + } + auto it = pad->bindings.find(action); + if (it != pad->bindings.end() && it->second.button == button) { + return true; + } + } return false; } + + } // namespace + + auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool { + if (!open_ || stack_.empty()) { + return false; + } + if (event.type == SDL_EVENT_KEY_DOWN) { + return handleKeyDown(event); + } + if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + return handleGamepadButton(event); + } + if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + return handleGamepadAxis(event); + } + return false; + } + + auto ServiceMenu::handleKeyDown(const SDL_Event& event) -> bool { switch (event.key.scancode) { case SDL_SCANCODE_UP: moveCursor(-1); @@ -747,6 +789,68 @@ namespace System { } } + auto ServiceMenu::handleGamepadButton(const SDL_Event& event) -> bool { + const int BTN = static_cast(event.gbutton.button); + if (BTN == SDL_GAMEPAD_BUTTON_DPAD_UP) { + moveCursor(-1); + return true; + } + if (BTN == SDL_GAMEPAD_BUTTON_DPAD_DOWN) { + moveCursor(+1); + return true; + } + if (BTN == SDL_GAMEPAD_BUTTON_DPAD_LEFT) { + changeValue(-1); + return true; + } + if (BTN == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) { + changeValue(+1); + return true; + } + // Botons d'accio per al pad emissor: FIRE = ENTER, ACCELERATE = BACK. + if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::SHOOT)) { + activateCurrent(); + return true; + } + if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::THRUST)) { + popPage(); + return true; + } + return false; + } + + auto ServiceMenu::handleGamepadAxis(const SDL_Event& event) -> bool { + const auto AXIS = static_cast(event.gaxis.axis); + const Sint16 VAL = event.gaxis.value; + if (AXIS == SDL_GAMEPAD_AXIS_LEFTX) { + const bool LEFT_NOW = VAL < -MENU_STICK_THRESHOLD; + const bool RIGHT_NOW = VAL > MENU_STICK_THRESHOLD; + if (LEFT_NOW && !stick_left_held_) { + changeValue(-1); + } + if (RIGHT_NOW && !stick_right_held_) { + changeValue(+1); + } + stick_left_held_ = LEFT_NOW; + stick_right_held_ = RIGHT_NOW; + return true; + } + if (AXIS == SDL_GAMEPAD_AXIS_LEFTY) { + const bool UP_NOW = VAL < -MENU_STICK_THRESHOLD; + const bool DOWN_NOW = VAL > MENU_STICK_THRESHOLD; + if (UP_NOW && !stick_up_held_) { + moveCursor(-1); + } + if (DOWN_NOW && !stick_down_held_) { + moveCursor(+1); + } + stick_up_held_ = UP_NOW; + stick_down_held_ = DOWN_NOW; + return true; + } + return false; + } + auto ServiceMenu::computeTargetHeight() const -> float { if (stack_.empty()) { return 0.0F; diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index 0fa1538..fc99612 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -85,13 +85,22 @@ namespace System { void update(float delta_time); void draw() const; - // Processa el KEY_DOWN. Retorna true si l'ha consumit (UP/DOWN/ENTER/ - // RIGHT/BACKSPACE/LEFT mentre esta obert). false en qualsevol altre cas. + // Processa events de navegacio. Retorna true si l'event s'ha consumit. + // Accepta: + // - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE. + // - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER, + // ACCELERATE = BACK. La resta de botons s'ignoren. + // - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect. auto handleEvent(const SDL_Event& event) -> bool; private: ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); + // Sub-handlers de handleEvent. Privats, no son part de l'API publica. + auto handleKeyDown(const SDL_Event& event) -> bool; + auto handleGamepadButton(const SDL_Event& event) -> bool; + auto handleGamepadAxis(const SDL_Event& event) -> bool; + void buildRootPage(); [[nodiscard]] auto buildVideoPage() -> Page; [[nodiscard]] auto buildResolutionPage() const -> Page; @@ -140,6 +149,15 @@ namespace System { float highlight_h_ = 0.0F; bool highlight_snap_ = true; + // Edge-detect de stick analogic per a navegacio. Una sola activacio + // per direccio: cal tornar a centre (sota el llindar) per disparar + // una altra. Compartit entre tots els pads — qualsevol jugador pot + // navegar el menu. + bool stick_left_held_ = false; + bool stick_right_held_ = false; + bool stick_up_held_ = false; + bool stick_down_held_ = false; + static std::unique_ptr instance; }; From 64a6599e8166f6b4676bffa979317e9fe657a739 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:54:04 +0200 Subject: [PATCH 09/15] fix(title): manten animacions amb menu obert, bloqueja nomes els polls d'input El fix anterior pausava tot el title quan el menu de servei estava obert, trencant l'efecte d'animacio de fons. Ara title segueix animant-se i nomes guardem handleSkipInput/handleStartInput mentre el menu o el modal de rebind estan actius, per evitar START fantasma sense congelar el render. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/system/director.cpp | 9 +++++++-- source/game/scenes/title_scene.cpp | 24 +++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index e5308d5..f1ab997 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -395,10 +395,15 @@ auto Director::iterate() -> SDL_AppResult { if (const auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->draw(); // toast: per damunt de tot } - if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) { + // Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei + // (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui, + // pero no es pinta per no confondre's visualment amb el modal). + const auto* di = System::DefineInputs::get(); + const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive(); + if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) { menu->draw(); // service menu: per damunt fins i tot dels toasts } - if (const auto* di = System::DefineInputs::get(); di != nullptr) { + if (di != nullptr) { di->draw(); // overlay de rebind: per damunt de tot } sdl_->present(); diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 238c1bf..520bfb0 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -13,6 +13,7 @@ #include "core/audio/audio.hpp" #include "core/defaults.hpp" #include "core/graphics/shape_loader.hpp" +#include "core/input/define_inputs.hpp" #include "core/input/input.hpp" #include "core/locale/locale.hpp" #include "core/math/easing.hpp" @@ -295,13 +296,6 @@ auto TitleScene::isFinished() const -> bool { } void TitleScene::update(float delta_time) { - // Pausa global: mentre el menu de servei esta obert (i, per tant, el - // sub-overlay de rebind tambe, si esta actiu), congelem la logica de la - // pantalla de titol per no consumir un START fantasma quan l'overlay - // s'auto-tanca i les tecles encara s'estan pulsant. - if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) { - return; - } if (starfield_) { starfield_->update(delta_time); } @@ -332,8 +326,20 @@ void TitleScene::update(float delta_time) { break; } - handleSkipInput(); - handleStartInput(); + // Les animacions segueixen pero els inputs es bloquegen mentre el menu + // de servei o l'overlay de redefinicio estiguin actius: en cas contrari, + // SDL_GetKeyboardState i SDL_GetGamepadButton segueixen veient les tecles + // pulsades i podrien disparar handleSkipInput/handleStartInput sense + // intencio. Mateixa logica: per a GameScene tota la pausa es global, + // pero a TitleScene nomes guardem els polls d'input. + const auto* menu = System::ServiceMenu::get(); + const auto* di = System::DefineInputs::get(); + const bool INPUT_BLOCKED = (menu != nullptr && menu->isOpen()) || + (di != nullptr && di->isActive()); + if (!INPUT_BLOCKED) { + handleSkipInput(); + handleStartInput(); + } } void TitleScene::updateStarfieldFadeInState(float delta_time) { From 120c5502fd5923442c25390c67c4efb1b11139f0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:58:52 +0200 Subject: [PATCH 10/15] feat(vector-text): afegeix el glyph / al charset El progres "i/n" del modal de redefinicio (ex. 1/4) sortia com a "14" perque VectorText no tenia shape per a la barra i emetia un warning. Afegim font/char_slash.shp (diagonal de baix-esquerra a dalt-dreta dins de la caixa 20x40) i el registrem al loader i al getShapeFilename. Co-Authored-By: Claude Opus 4.7 (1M context) --- data/shapes/font/char_slash.shp | 9 +++++++++ source/core/graphics/vector_text.cpp | 4 +++- source/core/graphics/vector_text.hpp | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 data/shapes/font/char_slash.shp diff --git a/data/shapes/font/char_slash.shp b/data/shapes/font/char_slash.shp new file mode 100644 index 0000000..399fcc7 --- /dev/null +++ b/data/shapes/font/char_slash.shp @@ -0,0 +1,9 @@ +# char_slash.shp - Símbol / (barra) +# Dimensions: 20×40 (blocky display) + +name: char_slash +scale: 1.0 +center: 10, 20 + +# Línia diagonal de baix-esquerra a dalt-dreta +line: 4,36 16,4 diff --git a/source/core/graphics/vector_text.cpp b/source/core/graphics/vector_text.cpp index 2e30810..fec3639 100644 --- a/source/core/graphics/vector_text.cpp +++ b/source/core/graphics/vector_text.cpp @@ -47,7 +47,7 @@ namespace Graphics { } // Cargar símbolos - const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"}; + const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/"}; for (const auto& sym : SYMBOLS) { char c = sym[0]; std::string filename = getShapeFilename(c); @@ -164,6 +164,8 @@ namespace Graphics { return "font/char_exclamation.shp"; case '?': return "font/char_question.shp"; + case '/': + return "font/char_slash.shp"; case ' ': return ""; // Espai es maneja sin load shape diff --git a/source/core/graphics/vector_text.hpp b/source/core/graphics/vector_text.hpp index 864390e..4978c74 100644 --- a/source/core/graphics/vector_text.hpp +++ b/source/core/graphics/vector_text.hpp @@ -21,7 +21,7 @@ namespace Graphics { // Renderizar string completo // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', - // '!', '?', ' ') + // '!', '?', '/', ' ') // - position: posición inicial (esquina superior izquierda) // - scale: factor de scale (1.0 = 20×40 px por carácter) // - spacing: espacio entre caracteres en píxeles (a scale 1.0) From 85050c8da460525d4722b4c181ba2a7905c89e95 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 21:20:25 +0200 Subject: [PATCH 11/15] fix(define-inputs): deixa passar QUIT i ESC al pipeline global L'overlay de redefinicio engolia tots els events mentre estava actiu, fet que impedia tancar la finestra amb l'aspa (SDL_EVENT_QUIT) i deixava prendre ESC com a cancel-lacio del rebind. Ara: - QUIT i WINDOW_CLOSE_REQUESTED passen sempre al global per tancar l'aplicacio des de l'aspa. - ESC ja no cancel-la la sequencia; cau al global on obre el prompt d'eixida com a la resta del joc. - isReservedScancode (ESC/F1-F12/RETURN/BACKSPACE/TAB) deixa passar. Tambe ajusta DISPAR -> DISPARAR a ca.yaml i treu el hint "ESC PER CANCEL-LAR" del modal i les claus de locale corresponents. Co-Authored-By: Claude Opus 4.7 (1M context) --- data/locale/ca.yaml | 3 +-- data/locale/en.yaml | 1 - source/core/input/define_inputs.cpp | 31 ++++++++++++++-------------- source/core/system/global_events.cpp | 8 ++++--- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 0d410f4..21e1767 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -103,11 +103,10 @@ define: 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" + fire: "DISPARAR" accelerate: "ACCELERAR" start: "START" menu: "MENU" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index e35509a..111c936 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -102,7 +102,6 @@ define: 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" diff --git a/source/core/input/define_inputs.cpp b/source/core/input/define_inputs.cpp index 8f46847..27e8b9e 100644 --- a/source/core/input/define_inputs.cpp +++ b/source/core/input/define_inputs.cpp @@ -252,22 +252,20 @@ namespace System { return true; // Empassem la resta sense fer res } const SDL_Scancode SC = event.key.scancode; - if (SC == SDL_SCANCODE_ESCAPE) { - cancel(); - return true; - } if (isReservedScancode(SC)) { - return true; // Ignorada + // ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline + // global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.). + return false; } captureAndAdvance(scancodeToInt(SC)); return true; } auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool { - // ESC al teclat tambe cancel·la durant rebind del mando. - if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { - cancel(); - return true; + // KEY_DOWN no es per al rebind de mando: deixem que el global el + // gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.). + if (event.type == SDL_EVENT_KEY_DOWN) { + return false; } // Filtrar events al pad del jugador actiu. @@ -312,8 +310,15 @@ namespace System { if (phase_ == Phase::INACTIVE) { return false; } + // SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la + // finestra encara que el modal estiga obert; els passem al pipeline. + if (event.type == SDL_EVENT_QUIT || + event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + return false; + } if (phase_ == Phase::COMPLETE) { - // Mentre mostrem el missatge OK, empassem els events sense capturar. + // Mentre mostrem el missatge OK, empassem la resta d'events sense + // capturar perque l'usuari no puga avançar accions sense voler. return true; } if (mode_ == Mode::KEYBOARD) { @@ -390,12 +395,6 @@ namespace System { const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F); const float PROG_Y = BOX_Y + 200.0F; text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR); - - const std::string CANCEL_HINT = Locale::get().text("define.cancel_hint"); - const float CH_W = Graphics::VectorText::getTextWidth(CANCEL_HINT, PROG_SCALE, TEXT_SPACING); - const float CH_X = BOX_X + ((BOX_W - CH_W) * 0.5F); - const float CH_Y = BOX_Y + BOX_H - 30.0F; - text_.render(CANCEL_HINT, Vec2{.x = CH_X, .y = CH_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, SEPARATOR_COLOR); } } // namespace System diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index ad4e2df..9fa22bf 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -51,14 +51,16 @@ namespace GlobalEvents { return false; } - // Si l'overlay de redefinicio esta actiu, engoleix tots els events. + // Engoleix els events que DefineInputs vol consumir mentre l'overlay + // es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen + // cap al pipeline normal i poden tancar la finestra o obrir el prompt + // d'eixida sense haver de completar la sequencia. auto consumeIfDefineActive(const SDL_Event& event) -> bool { auto* di = System::DefineInputs::get(); if (di == nullptr || !di->isActive()) { return false; } - di->handleEvent(event); - return true; + return di->handleEvent(event); } // Botó MENU al mando d'algun jugador → alterna el menú de servei From 99d0f62ab578f7a7a296911a730c9bb03f04e9f3 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 21:22:23 +0200 Subject: [PATCH 12/15] feat(service_menu): slot 'sense mando' al cycle i swap automatic en conflicte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El CYCLE de la pagina CONTROLS ara inclou un slot virtual al final que desassigna el mando (gamepad_name + gamepad_path buits → padDisplayName mostra "SENSE MANDO"). Aixi l'usuari pot recuperar el control teclat sense haver d'editar el YAML. A mes, si en assignar un mando l'altre jugador ja el tenia, fem swap automatic: l'altre jugador rep l'assignacio previa d'aquest, evitant que dos jugadors comparteixen el mateix dispositiu. La deteccio prioritza path (mateixa branca que resolvePlayerGamepad). Extracta tambe reapplyBindings per mantenir cyclePlayerPad llegible. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/define_inputs.cpp | 8 ++-- source/core/system/service_menu.cpp | 73 +++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/source/core/input/define_inputs.cpp b/source/core/input/define_inputs.cpp index 27e8b9e..9bfb857 100644 --- a/source/core/input/define_inputs.cpp +++ b/source/core/input/define_inputs.cpp @@ -55,13 +55,15 @@ namespace { } // Scancodes que MAI capturem com a binding (reservats per a navegacio o - // global hotkeys). Evita que l'usuari es bloquegi ell mateix. + // global hotkeys). Tornen true → handleEvent les deixa passar al pipeline + // global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12 + // son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio). auto isReservedScancode(SDL_Scancode sc) -> bool { if (sc == SDL_SCANCODE_ESCAPE) { - return true; // ESC sempre cancel·la + return true; } if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) { - return true; // F1-F12 globals + return true; } if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) { return true; diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index b3b520b..33198cc 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -84,8 +84,8 @@ namespace { return Utils::toUpperAscii(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. + // Index del pad assignat dins de la llista. Retorna pads.size() per + // representar el slot virtual SENSE MANDO (al final del cycle). auto findAssignedIndex(const std::vector>& pads, const Config::PlayerBindings& pcfg) -> std::size_t { for (std::size_t i = 0; i < pads.size(); ++i) { @@ -98,12 +98,35 @@ namespace { return i; } } - return 0; + return pads.size(); // Slot virtual "sense mando" + } + + // Aplica les noves assignacions a Input. Si ha hagut swap, refresca els + // dos jugadors; en cas contrari nomes el que ha canviat. + void reapplyBindings(int player_index, bool swap_other) { + auto* input = Input::get(); + if (input == nullptr) { + return; + } + if (player_index == 0) { + input->applyPlayer1Bindings(ConfigYaml::engine_config.player1); + if (swap_other) { + input->applyPlayer2Bindings(ConfigYaml::engine_config.player2); + } + } else { + input->applyPlayer2Bindings(ConfigYaml::engine_config.player2); + if (swap_other) { + input->applyPlayer1Bindings(ConfigYaml::engine_config.player1); + } + } } // 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) { - auto* input = Input::get(); + const auto* input = Input::get(); if (input == nullptr) { return; } @@ -114,21 +137,41 @@ namespace { auto& pcfg = (player_index == 0) ? ConfigYaml::engine_config.player1 : ConfigYaml::engine_config.player2; + auto& other = (player_index == 0) + ? ConfigYaml::engine_config.player2 + : ConfigYaml::engine_config.player1; - 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; + 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; } - 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); + + // Detecta conflicte amb l'altre jugador per fer swap. + 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; + const bool DO_SWAP = CONFLICT || CONFLICT_BY_NAME; + + const std::string PREV_NAME = pcfg.gamepad_name; + const std::string PREV_PATH = pcfg.gamepad_path; + + pcfg.gamepad_name = new_name; + pcfg.gamepad_path = new_path; + if (DO_SWAP) { + other.gamepad_name = PREV_NAME; + other.gamepad_path = PREV_PATH; } + + reapplyBindings(player_index, DO_SWAP); ConfigYaml::saveToFile(); } From 3dcf5c3a9903c9ef77ba2f988744a405d8df8c7c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 22:12:53 +0200 Subject: [PATCH 13/15] 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); From daa7eaf811584cebfbd4641e47041265c65f6f50 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 22:20:29 +0200 Subject: [PATCH 14/15] feat(service_menu): glyphs () + tanca picker al seleccionar mando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegim els glyphs ( i ) a VectorText (char_lparen.shp, char_rparen.shp, arcs de 4 trams dins la caixa 20x40) perque el sufix (P1)/(P2) de la picker de mando es renderitze net sense warnings. A mes, al triar un mando o "SENSE MANDO" a la picker fem popPage automatic, perque l'usuari no haja de tornar enrere a ma després d'una assignacio. Co-Authored-By: Claude Opus 4.7 (1M context) --- data/shapes/font/char_lparen.shp | 9 +++++++++ data/shapes/font/char_rparen.shp | 9 +++++++++ source/core/graphics/vector_text.cpp | 6 +++++- source/core/graphics/vector_text.hpp | 2 +- source/core/system/service_menu.cpp | 8 ++++++-- source/core/system/service_menu.hpp | 7 ++++--- 6 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 data/shapes/font/char_lparen.shp create mode 100644 data/shapes/font/char_rparen.shp diff --git a/data/shapes/font/char_lparen.shp b/data/shapes/font/char_lparen.shp new file mode 100644 index 0000000..f99f143 --- /dev/null +++ b/data/shapes/font/char_lparen.shp @@ -0,0 +1,9 @@ +# char_lparen.shp - Símbol ( (parèntesi esquerre) +# Dimensions: 20×40 (blocky display) + +name: char_lparen +scale: 1.0 +center: 10, 20 + +# Arc cap a l'esquerra aproximat amb 4 trams rectes +polyline: 14,4 8,12 6,20 8,28 14,36 diff --git a/data/shapes/font/char_rparen.shp b/data/shapes/font/char_rparen.shp new file mode 100644 index 0000000..f10eb95 --- /dev/null +++ b/data/shapes/font/char_rparen.shp @@ -0,0 +1,9 @@ +# char_rparen.shp - Símbol ) (parèntesi dret) +# Dimensions: 20×40 (blocky display) + +name: char_rparen +scale: 1.0 +center: 10, 20 + +# Arc cap a la dreta aproximat amb 4 trams rectes +polyline: 6,4 12,12 14,20 12,28 6,36 diff --git a/source/core/graphics/vector_text.cpp b/source/core/graphics/vector_text.cpp index fec3639..c916980 100644 --- a/source/core/graphics/vector_text.cpp +++ b/source/core/graphics/vector_text.cpp @@ -47,7 +47,7 @@ namespace Graphics { } // Cargar símbolos - const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/"}; + const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"}; for (const auto& sym : SYMBOLS) { char c = sym[0]; std::string filename = getShapeFilename(c); @@ -166,6 +166,10 @@ namespace Graphics { return "font/char_question.shp"; case '/': return "font/char_slash.shp"; + case '(': + return "font/char_lparen.shp"; + case ')': + return "font/char_rparen.shp"; case ' ': return ""; // Espai es maneja sin load shape diff --git a/source/core/graphics/vector_text.hpp b/source/core/graphics/vector_text.hpp index 4978c74..137cade 100644 --- a/source/core/graphics/vector_text.hpp +++ b/source/core/graphics/vector_text.hpp @@ -21,7 +21,7 @@ namespace Graphics { // Renderizar string completo // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', - // '!', '?', '/', ' ') + // '!', '?', '/', '(', ')', ' ') // - position: posición inicial (esquina superior izquierda) // - scale: factor de scale (1.0 = 20×40 px por carácter) // - spacing: espacio entre caracteres en píxeles (a scale 1.0) diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index fdd8913..0f046a7 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -631,8 +631,9 @@ namespace System { .label_key = {}, .label_text = std::move(label), .selectable = true, - .on_activate = [PI, PAD_NAME, PAD_PATH] { + .on_activate = [this, PI, PAD_NAME, PAD_PATH] { assignPadToPlayer(PI, PAD_NAME, PAD_PATH); + popPage(); }, .get_value_text = {}, .on_change = {}, @@ -646,7 +647,10 @@ namespace System { .label_key = "service_menu.controls_no_pad", .label_text = {}, .selectable = true, - .on_activate = [PI] { assignPadToPlayer(PI, {}, {}); }, + .on_activate = [this, PI] { + assignPadToPlayer(PI, {}, {}); + popPage(); + }, .get_value_text = {}, .on_change = {}, }); diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index 49cbca8..d3bd8b4 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -109,9 +109,10 @@ namespace System { [[nodiscard]] auto buildSystemPage() -> 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; + // pad triat (amb swap automatic si l'altre jugador ja el tenia) i + // tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a + // desasignar. + [[nodiscard]] 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); From cefafe99e4fb9c8bd1e5c204aac43d75f7d31679 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 22:38:10 +0200 Subject: [PATCH 15/15] feat(service_menu): triggers L2/R2 navegables + so al rebind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El menu de servei nomes processava AXIS_MOTION dels sticks i descartava els triggers. Com SDL3 mai emet button events per a L2/R2 (nomes axis), rebindar FIRE o ACCEL a un trigger feia que no funcionaren al menu, fins i tot estant correctament al joc per via del poll de Input::checkTriggerInput. Afegim edge-detect dels dos triggers al handleGamepadAxis i, quan creuen el llindar, mirem si el codi virtual (100=L2, 101=R2) coincideix amb el binding de FIRE → activateCurrent, o ACCEL → popPage. Estat held per trigger per evitar repeticions mentre es mante premut. DefineInputs ara reprodueix el so accept del menu en cada captura valida, que estava silent i no donava feedback al rebind. Tambe extraiem processStickX/Y i processTriggerEdge per mantenir handleGamepadAxis com a dispatcher i sota el llindar de complexitat cognitiva del clang-tidy. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/define_inputs.cpp | 5 +- source/core/system/service_menu.cpp | 88 ++++++++++++++++++++--------- source/core/system/service_menu.hpp | 10 ++++ 3 files changed, 76 insertions(+), 27 deletions(-) diff --git a/source/core/input/define_inputs.cpp b/source/core/input/define_inputs.cpp index 9bfb857..f1f8350 100644 --- a/source/core/input/define_inputs.cpp +++ b/source/core/input/define_inputs.cpp @@ -4,11 +4,11 @@ #include "core/input/define_inputs.hpp" #include -#include #include #include #include +#include "core/audio/audio.hpp" #include "core/defaults/service_menu.hpp" #include "core/input/input.hpp" #include "core/locale/locale.hpp" @@ -168,6 +168,9 @@ namespace System { } sequence_[index_].captured = code; ++index_; + if (auto* audio = Audio::get(); audio != nullptr) { + audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE); + } if (index_ >= sequence_.size()) { persistAndComplete(); } diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 0f046a7..d5432c3 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -818,6 +818,9 @@ namespace System { // Llindar de stick per a navegacio de menu (mig camp del rang ±32767). // Mes baix que el del joc (30000) per a una resposta mes agil al menu. constexpr Sint16 MENU_STICK_THRESHOLD = 16384; + // Llindar de trigger (mateix valor que MENU_TRIGGER_THRESHOLD, que + // és private). Edge a partir del 50% del rang. + constexpr Sint16 MENU_TRIGGER_THRESHOLD = 16384; // Retorna true si el codi de boto SDL coincideix amb l'accio // configurada per algun dels dos jugadors (es a dir, el boto te el @@ -914,36 +917,69 @@ namespace System { return false; } + void ServiceMenu::processStickX(Sint16 val) { + const bool LEFT_NOW = val < -MENU_STICK_THRESHOLD; + const bool RIGHT_NOW = val > MENU_STICK_THRESHOLD; + if (LEFT_NOW && !stick_left_held_) { + changeValue(-1); + } + if (RIGHT_NOW && !stick_right_held_) { + changeValue(+1); + } + stick_left_held_ = LEFT_NOW; + stick_right_held_ = RIGHT_NOW; + } + + void ServiceMenu::processStickY(Sint16 val) { + const bool UP_NOW = val < -MENU_STICK_THRESHOLD; + const bool DOWN_NOW = val > MENU_STICK_THRESHOLD; + if (UP_NOW && !stick_up_held_) { + moveCursor(-1); + } + if (DOWN_NOW && !stick_down_held_) { + moveCursor(+1); + } + stick_up_held_ = UP_NOW; + stick_down_held_ = DOWN_NOW; + } + + // Edge-detect d'un trigger: si creua el llindar amunt, despatxa + // ENTER/BACK segons el binding (FIRE/ACCEL) que apunta al codi + // virtual del trigger (100 = L2, 101 = R2). + void ServiceMenu::processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held) { + const bool NOW = val > MENU_TRIGGER_THRESHOLD; + if (NOW && !held) { + if (buttonMatchesAction(which, virtual_button, InputAction::SHOOT)) { + activateCurrent(); + } else if (buttonMatchesAction(which, virtual_button, InputAction::THRUST)) { + popPage(); + } + } + held = NOW; + } + auto ServiceMenu::handleGamepadAxis(const SDL_Event& event) -> bool { const auto AXIS = static_cast(event.gaxis.axis); const Sint16 VAL = event.gaxis.value; - if (AXIS == SDL_GAMEPAD_AXIS_LEFTX) { - const bool LEFT_NOW = VAL < -MENU_STICK_THRESHOLD; - const bool RIGHT_NOW = VAL > MENU_STICK_THRESHOLD; - if (LEFT_NOW && !stick_left_held_) { - changeValue(-1); - } - if (RIGHT_NOW && !stick_right_held_) { - changeValue(+1); - } - stick_left_held_ = LEFT_NOW; - stick_right_held_ = RIGHT_NOW; - return true; + switch (AXIS) { + case SDL_GAMEPAD_AXIS_LEFTX: + processStickX(VAL); + return true; + case SDL_GAMEPAD_AXIS_LEFTY: + processStickY(VAL); + return true; + // Triggers L2/R2: SDL3 nomes emet AXIS_MOTION, no button events. + // Per poder rebindar FIRE/ACCEL als triggers, sintetitzem aqui + // la pulsacio amb edge-detect i la passem pel mateix flux. + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: + processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_L2_AS_BUTTON, trigger_l2_held_); + return true; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: + processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_R2_AS_BUTTON, trigger_r2_held_); + return true; + default: + return false; } - if (AXIS == SDL_GAMEPAD_AXIS_LEFTY) { - const bool UP_NOW = VAL < -MENU_STICK_THRESHOLD; - const bool DOWN_NOW = VAL > MENU_STICK_THRESHOLD; - if (UP_NOW && !stick_up_held_) { - moveCursor(-1); - } - if (DOWN_NOW && !stick_down_held_) { - moveCursor(+1); - } - stick_up_held_ = UP_NOW; - stick_down_held_ = DOWN_NOW; - return true; - } - return false; } auto ServiceMenu::computeTargetHeight() const -> float { diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index d3bd8b4..61f7465 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -100,6 +100,11 @@ namespace System { auto handleKeyDown(const SDL_Event& event) -> bool; auto handleGamepadButton(const SDL_Event& event) -> bool; auto handleGamepadAxis(const SDL_Event& event) -> bool; + // Helpers per a cada eix; permeten que handleGamepadAxis es quedi + // com a dispatcher i no bote el llindar de complexitat. + void processStickX(Sint16 val); + void processStickY(Sint16 val); + void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held); void buildRootPage(); [[nodiscard]] auto buildVideoPage() -> Page; @@ -162,6 +167,11 @@ namespace System { bool stick_right_held_ = false; bool stick_up_held_ = false; bool stick_down_held_ = false; + // Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no + // emet button events per als triggers; els llegim com a axis i + // sintetitzem una pulsacio quan creuen el llindar. + bool trigger_l2_held_ = false; + bool trigger_r2_held_ = false; static std::unique_ptr instance; };