feat(service_menu): picker de mando per llista i fix SENSE MANDO
El cycle anterior fallava al desasignar perque Input::resolvePlayerGamepad tenia un fallback per slot que reasignava gamepads_[player_index] quan name+path eren buits. Això el contradeia el slot "SENSE MANDO" del cycle: el YAML quedava buit pero el runtime seguia lligant el mando. Treure el fallback i moure l'autoassignacio inicial al boot (nomes si tots dos jugadors venen buits) restaura la semàntica: buit vol dir buit. Sobre el fix, redissenyem la UX dels items MANDO P1/P2: ja no son CYCLE sino SUBMENU que obrin una pàgina-llista (estil RESOLUCIÓ) amb tots els mandos detectats. Cada item porta sufix (P1)/(P2) nomes si el mando el te l'altre jugador, perque sapigues que assignar-lo li'l "robarà". L'ultim item es "SENSE MANDO" per a desassignar explícitament. La lògica de swap automatic en conflicte queda extreta a assignPadToPlayer i es reutilitza des de la picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Gamepad> {
|
||||
// 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<Gamepad> {
|
||||
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> gamepad = resolvePlayerGamepad(bindings, 0);
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> 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> gamepad = resolvePlayerGamepad(bindings, 1);
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player2_gamepad_ = nullptr;
|
||||
|
||||
@@ -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<Gamepad>;
|
||||
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<void()> on_yes);
|
||||
|
||||
Reference in New Issue
Block a user