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) ==========
|
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||||
|
|
||||||
// Cerca el gamepad assignat a un jugador. Prioritat: path > name > slot
|
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
|
||||||
// per índex (fallback). Retorna nullptr si no n'hi ha cap de connectat.
|
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
|
||||||
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings,
|
// mando explicit). L'autoassignacio inicial es resol al boot.
|
||||||
std::size_t fallback_index) -> std::shared_ptr<Gamepad> {
|
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
|
||||||
if (gamepads_.empty()) {
|
if (gamepads_.empty()) {
|
||||||
return nullptr;
|
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;
|
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::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Resoldre gamepad per path/name/slot
|
// 2. Resoldre gamepad per path/name
|
||||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings, 0);
|
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
player1_gamepad_ = nullptr;
|
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::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Resoldre gamepad per path/name/slot
|
// 2. Resoldre gamepad per path/name
|
||||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings, 1);
|
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
player2_gamepad_ = nullptr;
|
player2_gamepad_ = nullptr;
|
||||||
|
|||||||
@@ -148,8 +148,7 @@ class Input {
|
|||||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||||
void addGamepadMappingsFromFile();
|
void addGamepadMappingsFromFile();
|
||||||
void discoverGamepads();
|
void discoverGamepads();
|
||||||
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings,
|
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||||
std::size_t fallback_index) -> std::shared_ptr<Gamepad>;
|
|
||||||
|
|
||||||
// --- Variables miembro ---
|
// --- Variables miembro ---
|
||||||
static Input* instance; // Instancia única del singleton
|
static Input* instance; // Instancia única del singleton
|
||||||
|
|||||||
@@ -109,6 +109,34 @@ Director::Director(int argc, char* argv[])
|
|||||||
// Inicialitzar sistema de input
|
// Inicialitzar sistema de input
|
||||||
Input::init("data/gamecontrollerdb.txt");
|
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
|
// Aplicar configuración de controls dels jugadors
|
||||||
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
||||||
Input::get()->applyPlayer2Bindings(cfg_->player2);
|
Input::get()->applyPlayer2Bindings(cfg_->player2);
|
||||||
|
|||||||
@@ -121,19 +121,11 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avança ciclicament l'assignacio de pad d'un jugador i la persisteix.
|
// Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
|
||||||
// El cycle inclou un slot virtual "sense mando" al final (NEXT == N).
|
// Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
|
||||||
// Si l'altre jugador ja tenia el pad triat, fa swap: l'altre rep la
|
// l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
|
||||||
// assignacio prèvia d'aquest jugador.
|
// new_path buits.
|
||||||
void cyclePlayerPad(int player_index, int dir) {
|
void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
|
||||||
const auto* input = Input::get();
|
|
||||||
if (input == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto& pads = input->getGamepads();
|
|
||||||
if (pads.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto& pcfg = (player_index == 0)
|
auto& pcfg = (player_index == 0)
|
||||||
? ConfigYaml::engine_config.player1
|
? ConfigYaml::engine_config.player1
|
||||||
: ConfigYaml::engine_config.player2;
|
: ConfigYaml::engine_config.player2;
|
||||||
@@ -141,21 +133,9 @@ namespace {
|
|||||||
? ConfigYaml::engine_config.player2
|
? ConfigYaml::engine_config.player2
|
||||||
: ConfigYaml::engine_config.player1;
|
: ConfigYaml::engine_config.player1;
|
||||||
|
|
||||||
const std::size_t N = pads.size();
|
// Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
|
||||||
const std::size_t SLOTS = N + 1; // N pads + slot "sense mando"
|
// path (mateix criteri que resolvePlayerGamepad); si nomes tenim
|
||||||
const std::size_t CURRENT = findAssignedIndex(pads, pcfg);
|
// nom (mando reconegut sense path), comparem per nom.
|
||||||
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.
|
|
||||||
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
|
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
|
||||||
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
|
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
|
||||||
other.gamepad_name == new_name;
|
other.gamepad_name == new_name;
|
||||||
@@ -554,18 +534,6 @@ namespace System {
|
|||||||
|
|
||||||
namespace {
|
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 {
|
auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item {
|
||||||
return ServiceMenu::Item{
|
return ServiceMenu::Item{
|
||||||
.kind = ServiceMenu::Kind::ACTION,
|
.kind = ServiceMenu::Kind::ACTION,
|
||||||
@@ -587,8 +555,24 @@ namespace System {
|
|||||||
Page page;
|
Page page;
|
||||||
page.title_key = "service_menu.controls";
|
page.title_key = "service_menu.controls";
|
||||||
page.items = {
|
page.items = {
|
||||||
makeCyclePadItem("service_menu.controls_pad_p1", 0),
|
Item{
|
||||||
makeCyclePadItem("service_menu.controls_pad_p2", 1),
|
.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",
|
makeDefineItem("service_menu.controls_define_keyboard_p1",
|
||||||
DefineInputs::Mode::KEYBOARD,
|
DefineInputs::Mode::KEYBOARD,
|
||||||
DefineInputs::Player::P1),
|
DefineInputs::Player::P1),
|
||||||
@@ -605,6 +589,70 @@ namespace System {
|
|||||||
return page;
|
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 {
|
auto ServiceMenu::buildSystemPage() -> Page {
|
||||||
Page page;
|
Page page;
|
||||||
page.title_key = "service_menu.system";
|
page.title_key = "service_menu.system";
|
||||||
|
|||||||
@@ -107,7 +107,11 @@ namespace System {
|
|||||||
[[nodiscard]] static auto buildAudioPage() -> Page;
|
[[nodiscard]] static auto buildAudioPage() -> Page;
|
||||||
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
||||||
[[nodiscard]] auto buildSystemPage() -> 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
|
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
|
||||||
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
||||||
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
||||||
|
|||||||
Reference in New Issue
Block a user