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) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:18:49 +02:00
parent fcf13591be
commit 34be79192c
6 changed files with 220 additions and 0 deletions
+139
View File
@@ -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<std::shared_ptr<Input::Gamepad>>& 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";