feat(controls): redefinició de tecles i botons del mando des del menú de servei
This commit is contained in:
1 parent
4739354875
commit
37fdcb4751
15 files changed
+1169
-80
No files matched your search
@@ -0,0 +1,238 @@
|
||||
#include "core/input/define_buttons.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Threshold per als triggers L2/R2 — equivalent a CC (16384 ≈ 50%).
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Geometria de l'overlay (canvas lògic 320×240).
|
||||
constexpr int BOX_WIDTH = 240;
|
||||
constexpr int PAD_Y = 8;
|
||||
constexpr int LINE_GAP = 4;
|
||||
constexpr Uint8 BG_ALPHA = 230;
|
||||
|
||||
} // namespace
|
||||
|
||||
void DefineButtons::init(std::shared_ptr<Text> title_font,
|
||||
std::shared_ptr<Text> items_font,
|
||||
const Palette& ui_palette) {
|
||||
title_font_ = std::move(title_font);
|
||||
items_font_ = std::move(items_font);
|
||||
ui_palette_ = ui_palette;
|
||||
}
|
||||
|
||||
auto DefineButtons::enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool {
|
||||
if (pad == nullptr) { return false; }
|
||||
pad_ = pad;
|
||||
pad_name_ = std::move(name);
|
||||
pad_path_ = std::move(path);
|
||||
enabled_ = true;
|
||||
finished_ = false;
|
||||
closing_ = false;
|
||||
message_shown_ = false;
|
||||
message_timer_ms_ = 0;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
resetSequence();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DefineButtons::disable() {
|
||||
enabled_ = false;
|
||||
finished_ = false;
|
||||
closing_ = false;
|
||||
message_shown_ = false;
|
||||
message_timer_ms_ = 0;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
pad_ = nullptr;
|
||||
pad_name_.clear();
|
||||
pad_path_.clear();
|
||||
sequence_.clear();
|
||||
index_ = 0;
|
||||
}
|
||||
|
||||
void DefineButtons::resetSequence() {
|
||||
// Ordre: ATTACK, JUMP, SERVICE — replica l'estructura de
|
||||
// `clearButtons()` de coffee_crisis_arcade_edition (seqüència ordenada
|
||||
// d'accions amb la SERVICE al final) adaptada al joc actual. La label
|
||||
// de SERVICE va a una clau pròpia (`menu.items.service`) per no
|
||||
// reutilitzar la del submenú dels mandos.
|
||||
sequence_.clear();
|
||||
sequence_.push_back({"menu.items.attack", GamepadBindings::Action::ATTACK, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
index_ = 0;
|
||||
}
|
||||
|
||||
auto DefineButtons::isInUse(int button) const -> bool {
|
||||
return std::ranges::any_of(sequence_, [button](const ButtonAssignment& a) {
|
||||
return a.button == button;
|
||||
});
|
||||
}
|
||||
|
||||
auto DefineButtons::isReadyToClose() const -> bool {
|
||||
// Llest per tancar quan: la seqüència ha acabat, ja s'ha mostrat el
|
||||
// missatge, està en mode tancament, i el timer ha expirat. Replica el
|
||||
// patró de `coffee_crisis_arcade_edition::DefineButtons::isReadyToClose`,
|
||||
// simplificat perquè ací no tenim animació de WindowMessage.
|
||||
return finished_ && message_shown_ && closing_ && message_timer_ms_ <= 0;
|
||||
}
|
||||
|
||||
void DefineButtons::update(int delta_ms) {
|
||||
if (!enabled_) { return; }
|
||||
if (finished_ && message_shown_) {
|
||||
message_timer_ms_ -= delta_ms;
|
||||
if (message_timer_ms_ <= 0 && !closing_) {
|
||||
closing_ = true;
|
||||
message_timer_ms_ = 0; // anchor a 0 per a `isReadyToClose`.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::handleEvent(const SDL_Event& ev) {
|
||||
if (!enabled_) { return; }
|
||||
switch (ev.type) {
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
handleButtonDown(ev.gbutton);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
onButtonUp();
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
handleAxisMotion(ev.gaxis);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::assignAndAdvance(int button) {
|
||||
if (finished_ || isInUse(button)) { return; }
|
||||
sequence_.at(index_).button = button;
|
||||
if (index_ + 1 < sequence_.size()) {
|
||||
++index_;
|
||||
} else {
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||
if (now && !was_pressed) { assignAndAdvance(virtual_button); }
|
||||
if (!now && was_pressed) { onButtonUp(); }
|
||||
was_pressed = now;
|
||||
}
|
||||
|
||||
void DefineButtons::handleButtonDown(const SDL_GamepadButtonEvent& ev) {
|
||||
// Filtra: només acceptem events del mando que estem configurant.
|
||||
// (Per l'instance_id no podem comparar directament; obtenim el handle
|
||||
// i el comparem.)
|
||||
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||
if (source != pad_) { return; }
|
||||
assignAndAdvance(static_cast<int>(ev.button));
|
||||
}
|
||||
|
||||
void DefineButtons::handleAxisMotion(const SDL_GamepadAxisEvent& ev) {
|
||||
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||
if (source != pad_) { return; }
|
||||
|
||||
// Triggers L2/R2 com a "botons" amb edge-detect (transició inactiu →
|
||||
// actiu). Replica `doControllerAxisMotion` de
|
||||
// coffee_crisis_arcade_edition.
|
||||
const bool NOW = ev.value > TRIGGER_THRESHOLD;
|
||||
if (ev.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||
processTrigger(GamepadBindings::TRIGGER_L2_AS_BUTTON, l2_was_pressed_, NOW);
|
||||
} else if (ev.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
processTrigger(GamepadBindings::TRIGGER_R2_AS_BUTTON, r2_was_pressed_, NOW);
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::onButtonUp() {
|
||||
if (!finished_ || message_shown_) { return; }
|
||||
persistAndShowComplete();
|
||||
}
|
||||
|
||||
void DefineButtons::persistAndShowComplete() {
|
||||
std::unordered_map<GamepadBindings::Action, int> mapping;
|
||||
for (const auto& a : sequence_) {
|
||||
mapping[a.action] = a.button;
|
||||
}
|
||||
GamepadBindings::get().setBindings(pad_name_, pad_path_, mapping);
|
||||
message_shown_ = true;
|
||||
message_timer_ms_ = MESSAGE_DISPLAY_DURATION_MS;
|
||||
}
|
||||
|
||||
void DefineButtons::render() const {
|
||||
if (!enabled_ || !title_font_ || !items_font_) { return; }
|
||||
|
||||
const int TITLE_H = title_font_->getLineHeight();
|
||||
const int ITEM_H = items_font_->getLineHeight();
|
||||
const int TITLE_KERNING = title_font_->getDefaultKerning();
|
||||
const int ITEM_KERNING = items_font_->getDefaultKerning();
|
||||
|
||||
// Calcular l'alçada total: títol + 2 línies (instrucció + label).
|
||||
int box_h = (PAD_Y * 2) + TITLE_H + LINE_GAP + (ITEM_H * 2) + LINE_GAP;
|
||||
int box_x = (GameCanvas::WIDTH - BOX_WIDTH) / 2;
|
||||
int box_y = (GameCanvas::HEIGHT - box_h) / 2;
|
||||
|
||||
const auto BG_ARGB = ui_palette_[UiConfig::get().menuBg()];
|
||||
const auto BORDER_ARGB = ui_palette_[UiConfig::get().menuBorder()];
|
||||
Screen::get().blendRect(box_x, box_y, BOX_WIDTH, box_h, BG_ARGB, BG_ALPHA);
|
||||
Screen::get().fillRect(box_x, box_y, BOX_WIDTH, 1, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x, box_y + box_h - 1, BOX_WIDTH, 1, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x, box_y, 1, box_h, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x + BOX_WIDTH - 1, box_y, 1, box_h, BORDER_ARGB);
|
||||
|
||||
int cursor_y = box_y + PAD_Y;
|
||||
const int CENTER_X = box_x + (BOX_WIDTH / 2);
|
||||
|
||||
// Títol: nom del mando.
|
||||
const std::string TITLE = pad_name_;
|
||||
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
TITLE,
|
||||
TITLE_KERNING,
|
||||
UiConfig::get().menuTitle());
|
||||
cursor_y += TITLE_H + LINE_GAP;
|
||||
|
||||
if (message_shown_) {
|
||||
// Pantalla final: missatge de configuració completa.
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text("menu.values.define_buttons_complete"),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuValue());
|
||||
return;
|
||||
}
|
||||
|
||||
// Instrucció + acció pendent. Replica visualment el WindowMessage de
|
||||
// coffee_crisis_arcade_edition.
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text("menu.values.press_button"),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuLabel());
|
||||
cursor_y += ITEM_H + LINE_GAP;
|
||||
|
||||
if (index_ < sequence_.size()) {
|
||||
const auto& current = sequence_[index_];
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text(current.label_key),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuCursor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/rendering/surface.hpp" // Palette
|
||||
|
||||
class Text;
|
||||
|
||||
// Sub-mòdul de captura seqüencial de botons per a un mando concret.
|
||||
// Replica `DefineButtons` de coffee_crisis_arcade_edition: l'usuari obre el
|
||||
// menú de servei, escull "Definir botons P1" / "P2", i el sub-mòdul li
|
||||
// demana un per un els botons per a cada acció (JUMP → ATTACK → SERVICE).
|
||||
//
|
||||
// Cicle de vida:
|
||||
// 1. `enable(pad, name, path)`: arrenca la seqüència. `index_` = 0.
|
||||
// 2. Cada SDL_EVENT_GAMEPAD_BUTTON_DOWN (o transició de L2/R2 via
|
||||
// AXIS_MOTION) que NO duplique cap botó ja assignat dins la sessió
|
||||
// avança `index_`. Si `index_` arriba a la mida de la seqüència,
|
||||
// `finished_` es posa a true i, al pròxim BUTTON_UP, escrivim el mapeig
|
||||
// complet a `GamepadBindings`, mostrem "Configuració completa" durant
|
||||
// `MESSAGE_DISPLAY_DURATION_MS`, i posem `closing_`. Quan tot l'overlay
|
||||
// ja no és visible, `isReadyToClose()` torna `true` i el caller ens
|
||||
// fa `disable()`.
|
||||
//
|
||||
// El layer runtime de l'aplicació viu a `GamepadBindings`: aquesta classe
|
||||
// només decideix què s'escriu i quan.
|
||||
class DefineButtons {
|
||||
public:
|
||||
DefineButtons() = default;
|
||||
~DefineButtons() = default;
|
||||
DefineButtons(const DefineButtons&) = delete;
|
||||
DefineButtons(DefineButtons&&) = delete;
|
||||
auto operator=(const DefineButtons&) -> DefineButtons& = delete;
|
||||
auto operator=(DefineButtons&&) -> DefineButtons& = delete;
|
||||
|
||||
// Injecta les fonts i la paleta UI; cridat des de `ServiceMenu::init`
|
||||
// perquè comparteixin tipografia i colors de tema.
|
||||
void init(std::shared_ptr<Text> title_font,
|
||||
std::shared_ptr<Text> items_font,
|
||||
const Palette& ui_palette);
|
||||
|
||||
// Arranca la sessió per al mando indicat. `pad` ha de ser un handle
|
||||
// viu obtingut de `GamepadManager`. Retorna false si `pad` és nullptr.
|
||||
auto enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool;
|
||||
|
||||
// Atura la sessió (sense persistir) i neteja l'estat.
|
||||
void disable();
|
||||
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||
[[nodiscard]] auto isReadyToClose() const -> bool;
|
||||
|
||||
// Avança el timer de tancament (ms) — replica el `update` de
|
||||
// coffee_crisis_arcade_edition. No fa res si no està actiu.
|
||||
void update(int delta_ms);
|
||||
|
||||
// Pinta l'overlay sobre el canvas actual (centrat). No fa res si no
|
||||
// està actiu.
|
||||
void render() const;
|
||||
|
||||
// Punt d'entrada d'events: BUTTON_DOWN / BUTTON_UP / AXIS_MOTION del
|
||||
// mando objectiu. La resta s'ignoren.
|
||||
void handleEvent(const SDL_Event& ev);
|
||||
|
||||
private:
|
||||
static constexpr int MESSAGE_DISPLAY_DURATION_MS = 2000;
|
||||
|
||||
struct ButtonAssignment {
|
||||
std::string label_key;
|
||||
GamepadBindings::Action action;
|
||||
int button; // SDL_GamepadButton o trigger virtual; INVALID = sense assignar.
|
||||
};
|
||||
|
||||
// Reinicialitza la seqüència ordenada (JUMP → ATTACK → SERVICE) amb
|
||||
// tots els botons sense assignar. Cridat a `enable`.
|
||||
void resetSequence();
|
||||
|
||||
// Comprova si un botó ja ha estat assignat dins de la sessió actual.
|
||||
// Replica `checkButtonNotInUse` / `checkTriggerNotInUse`.
|
||||
[[nodiscard]] auto isInUse(int button) const -> bool;
|
||||
|
||||
void handleButtonDown(const SDL_GamepadButtonEvent& ev);
|
||||
void handleAxisMotion(const SDL_GamepadAxisEvent& ev);
|
||||
|
||||
// Helper de la lògica comuna a button-down i als triggers L2/R2:
|
||||
// assigna `button` a l'acció pendent i avança l'índex (o marca
|
||||
// finished_ si era l'última). No fa res si `button` ja està en ús.
|
||||
void assignAndAdvance(int button);
|
||||
|
||||
// Tracta una transició de trigger (L2 o R2) com a botó virtual amb
|
||||
// edge-detect. `virtual_button` és `TRIGGER_L?_AS_BUTTON`,
|
||||
// `was_pressed` és l'estat anterior (mutat per referència).
|
||||
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||
|
||||
// Crida `onSequenceEnd()` quan l'usuari deixa anar el botó després
|
||||
// d'haver completat la seqüència — replica la branca de
|
||||
// `SDL_EVENT_GAMEPAD_BUTTON_UP` a `DefineButtons::handleEvents`.
|
||||
void onButtonUp();
|
||||
|
||||
// Persistix les assignacions a `GamepadBindings` i posa `message_shown_`.
|
||||
void persistAndShowComplete();
|
||||
|
||||
SDL_Gamepad* pad_{nullptr};
|
||||
std::string pad_name_;
|
||||
std::string pad_path_;
|
||||
|
||||
std::shared_ptr<Text> title_font_;
|
||||
std::shared_ptr<Text> items_font_;
|
||||
Palette ui_palette_{};
|
||||
|
||||
std::vector<ButtonAssignment> sequence_;
|
||||
std::size_t index_{0};
|
||||
int message_timer_ms_{0};
|
||||
|
||||
bool enabled_{false};
|
||||
bool finished_{false};
|
||||
bool closing_{false};
|
||||
bool message_shown_{false};
|
||||
bool l2_was_pressed_{false};
|
||||
bool r2_was_pressed_{false};
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Threshold per a considerar L2/R2 com a premuts (mateix valor que
|
||||
// coffee_crisis_arcade_edition: 16384 ≈ 50% de ±32767).
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Taules de mapejat string ↔ valor: replica BUTTON_TO_STRING /
|
||||
// STRING_TO_BUTTON / ACTION_TO_STRING / STRING_TO_ACTION del projecte
|
||||
// de referència. Els noms en lowercase per llegibilitat al YAML.
|
||||
using ActionPair = std::pair<GamepadBindings::Action, std::string_view>;
|
||||
constexpr std::array ACTION_NAMES = {
|
||||
ActionPair{GamepadBindings::Action::JUMP, std::string_view{"jump"}},
|
||||
ActionPair{GamepadBindings::Action::ATTACK, std::string_view{"attack"}},
|
||||
ActionPair{GamepadBindings::Action::SERVICE, std::string_view{"service"}},
|
||||
};
|
||||
|
||||
using ButtonPair = std::pair<int, std::string_view>;
|
||||
constexpr std::array BUTTON_NAMES = {
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH), std::string_view{"south"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_EAST), std::string_view{"east"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_WEST), std::string_view{"west"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_NORTH), std::string_view{"north"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_BACK), std::string_view{"back"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_GUIDE), std::string_view{"guide"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_START), std::string_view{"start"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_STICK), std::string_view{"left_stick"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_STICK), std::string_view{"right_stick"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), std::string_view{"l1"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), std::string_view{"r1"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_UP), std::string_view{"dpad_up"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_DOWN), std::string_view{"dpad_down"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT), std::string_view{"dpad_left"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), std::string_view{"dpad_right"}},
|
||||
ButtonPair{GamepadBindings::TRIGGER_L2_AS_BUTTON, std::string_view{"l2"}},
|
||||
ButtonPair{GamepadBindings::TRIGGER_R2_AS_BUTTON, std::string_view{"r2"}},
|
||||
};
|
||||
|
||||
auto actionToString(GamepadBindings::Action a) -> std::string_view {
|
||||
for (const auto& [act, name] : ACTION_NAMES) {
|
||||
if (act == a) { return name; }
|
||||
}
|
||||
return ACTION_NAMES.front().second;
|
||||
}
|
||||
|
||||
auto stringToAction(std::string_view s, GamepadBindings::Action& out) -> bool {
|
||||
for (const auto& [act, name] : ACTION_NAMES) {
|
||||
if (name == s) {
|
||||
out = act;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto buttonToString(int btn) -> std::string_view {
|
||||
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||
if (val == btn) { return name; }
|
||||
}
|
||||
return std::string_view{""};
|
||||
}
|
||||
|
||||
auto stringToButton(std::string_view s, int& out) -> bool {
|
||||
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||
if (name == s) {
|
||||
out = val;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GamepadBindings::get() -> GamepadBindings& {
|
||||
static GamepadBindings instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
auto GamepadBindings::defaultButton(Action action) -> int {
|
||||
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement
|
||||
// i les consultes del Director per al menú).
|
||||
switch (action) {
|
||||
case Action::JUMP:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
case Action::ATTACK:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_WEST);
|
||||
case Action::SERVICE:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_BACK);
|
||||
}
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID);
|
||||
}
|
||||
|
||||
auto GamepadBindings::findConfig(const std::string& name, const std::string& path) const -> int {
|
||||
if (!path.empty()) {
|
||||
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||
if (configs_[i].path == path) { return static_cast<int>(i); }
|
||||
}
|
||||
}
|
||||
if (!name.empty()) {
|
||||
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||
if (configs_[i].name == name) { return static_cast<int>(i); }
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto GamepadBindings::getButton(const std::string& name, const std::string& path, Action action) const -> int {
|
||||
const int IDX = findConfig(name, path);
|
||||
if (IDX < 0) { return defaultButton(action); }
|
||||
const auto& m = configs_[static_cast<std::size_t>(IDX)].bindings;
|
||||
auto it = m.find(action);
|
||||
return (it != m.end()) ? it->second : defaultButton(action);
|
||||
}
|
||||
|
||||
void GamepadBindings::setBindings(const std::string& name,
|
||||
const std::string& path,
|
||||
const std::unordered_map<Action, int>& bindings) {
|
||||
int idx = findConfig(name, path);
|
||||
if (idx < 0) {
|
||||
configs_.push_back(Config{.name = name, .path = path, .bindings = bindings});
|
||||
} else {
|
||||
auto& cfg = configs_[static_cast<std::size_t>(idx)];
|
||||
cfg.name = name;
|
||||
cfg.path = path;
|
||||
cfg.bindings = bindings;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
auto GamepadBindings::isPressed(SDL_Gamepad* pad, int button) -> bool {
|
||||
if (pad == nullptr) { return false; }
|
||||
if (button == TRIGGER_L2_AS_BUTTON) {
|
||||
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||
}
|
||||
if (button == TRIGGER_R2_AS_BUTTON) {
|
||||
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||
}
|
||||
return SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(button));
|
||||
}
|
||||
|
||||
void GamepadBindings::load(const std::string& file_path) {
|
||||
file_path_ = file_path;
|
||||
configs_.clear();
|
||||
|
||||
std::ifstream file(file_path);
|
||||
if (!file) { return; }
|
||||
std::stringstream buf;
|
||||
buf << file.rdbuf();
|
||||
const std::string CONTENT = buf.str();
|
||||
if (CONTENT.empty()) { return; }
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
||||
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
|
||||
<< file_path << '\n';
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : yaml["gamepads"]) {
|
||||
Config cfg;
|
||||
if (entry.contains("name")) { cfg.name = entry["name"].get_value<std::string>(); }
|
||||
if (entry.contains("path")) { cfg.path = entry["path"].get_value<std::string>(); }
|
||||
if (entry.contains("bindings") && entry["bindings"].is_mapping()) {
|
||||
for (const auto& kv : entry["bindings"].as_map()) {
|
||||
const auto KEY = kv.first.get_value<std::string>();
|
||||
const auto VAL = kv.second.get_value<std::string>();
|
||||
Action action{};
|
||||
int btn = 0;
|
||||
if (stringToAction(KEY, action) && stringToButton(VAL, btn)) {
|
||||
cfg.bindings[action] = btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
configs_.push_back(std::move(cfg));
|
||||
}
|
||||
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
|
||||
<< file_path << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
|
||||
<< e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
auto GamepadBindings::save() -> bool {
|
||||
if (file_path_.empty()) { return false; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n";
|
||||
out << "# Auto-generat des del menú de servei. Editable a mà.\n";
|
||||
out << "gamepads:\n";
|
||||
for (const auto& cfg : configs_) {
|
||||
out << " - name: \"" << cfg.name << "\"\n";
|
||||
out << " path: \"" << cfg.path << "\"\n";
|
||||
out << " bindings:\n";
|
||||
for (const auto& [action, button] : cfg.bindings) {
|
||||
out << " " << actionToString(action) << ": \""
|
||||
<< buttonToString(button) << "\"\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(file_path_, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
|
||||
return false;
|
||||
}
|
||||
file << out.str();
|
||||
return file.good();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Vinculacions remapeables d'accions del joc a botons d'un mando concret,
|
||||
// indexades per `(name, path)` del mando — replica el patró
|
||||
// `GamepadConfig` + `GamepadConfigManager` de coffee_crisis_arcade_edition.
|
||||
//
|
||||
// Persisteix en un YAML aïllat al system folder (no a `config.yaml`) perquè
|
||||
// és per dispositiu, no per usuari/perfil: dos mandos diferents poden
|
||||
// tenir mappings diferents i cap d'ells va lligat a un slot de jugador.
|
||||
//
|
||||
// El layer runtime (consultat per Player/Director cada frame) és
|
||||
// directament aquesta classe; no hi ha caché a `GamepadManager::Entry`. El
|
||||
// nombre d'entries persistides és típicament <10, i les accions són 3
|
||||
// (JUMP/ATTACK/SERVICE), així que les consultes són trivialment ràpides.
|
||||
class GamepadBindings {
|
||||
public:
|
||||
enum class Action : std::uint8_t {
|
||||
JUMP,
|
||||
ATTACK,
|
||||
SERVICE,
|
||||
};
|
||||
|
||||
// Triggers L2/R2 representats com a "botons virtuals" — replica la
|
||||
// convenció de coffee_crisis_arcade_edition (`TRIGGER_L2_AS_BUTTON`,
|
||||
// `TRIGGER_R2_AS_BUTTON`). El threshold viu a `isPressed`.
|
||||
static constexpr int TRIGGER_L2_AS_BUTTON = 100;
|
||||
static constexpr int TRIGGER_R2_AS_BUTTON = 101;
|
||||
|
||||
static auto get() -> GamepadBindings&;
|
||||
|
||||
GamepadBindings(const GamepadBindings&) = delete;
|
||||
GamepadBindings(GamepadBindings&&) = delete;
|
||||
auto operator=(const GamepadBindings&) -> GamepadBindings& = delete;
|
||||
auto operator=(GamepadBindings&&) -> GamepadBindings& = delete;
|
||||
|
||||
// Fixa la ruta del fitxer persistent + el carrega. Si no existix
|
||||
// encara, l'estat queda buit (les consultes retornaran defaults) i
|
||||
// `save()` el crearà al primer `setBindings`.
|
||||
void load(const std::string& file_path);
|
||||
|
||||
// Persistix l'estat actual (totes les Config conegudes). Cridat
|
||||
// automàticament dins de `setBindings` — no cal cridar-la a mà.
|
||||
auto save() -> bool;
|
||||
|
||||
// Retorna el botó (o trigger virtual 100/101) lligat a `action` per al
|
||||
// mando identificat per (name, path). Si no hi ha entrada per aquest
|
||||
// mando, retorna el default global. La cerca és primer per `path`
|
||||
// (re-vincular el mateix mando al mateix port és prioritari) i, si no
|
||||
// troba, fallback per `name` (mando connectat a un altre port USB).
|
||||
[[nodiscard]] auto getButton(const std::string& name, const std::string& path, Action action) const -> int;
|
||||
|
||||
// Reescriu totes les vinculacions del mando (name, path). Si no hi
|
||||
// havia config per aquest dispositiu, en crea una nova. Persisteix a
|
||||
// disc immediatament. Replicació de `Input::saveGamepadConfigFromGamepad`.
|
||||
void setBindings(const std::string& name,
|
||||
const std::string& path,
|
||||
const std::unordered_map<Action, int>& bindings);
|
||||
|
||||
// Wrapper de polling: torna `true` si el "botó" (o trigger virtual)
|
||||
// està premut ara mateix al mando `pad`. Encapsula el threshold per
|
||||
// a L2/R2 (~50% del rang).
|
||||
[[nodiscard]] static auto isPressed(SDL_Gamepad* pad, int button) -> bool;
|
||||
|
||||
// Default de fàbrica per a una acció — usat tant pel fallback de
|
||||
// `getButton` com per inicialitzar la seqüència del DefineButtons.
|
||||
[[nodiscard]] static auto defaultButton(Action action) -> int;
|
||||
|
||||
private:
|
||||
struct Config {
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::unordered_map<Action, int> bindings;
|
||||
};
|
||||
|
||||
GamepadBindings() = default;
|
||||
~GamepadBindings() = default;
|
||||
|
||||
// Localitza la Config que coincideix amb (name, path); retorna -1 si
|
||||
// no la troba. Match prioritari per path, fallback per nom.
|
||||
[[nodiscard]] auto findConfig(const std::string& name, const std::string& path) const -> int;
|
||||
|
||||
std::vector<Config> configs_;
|
||||
std::string file_path_;
|
||||
};
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
@@ -82,6 +83,11 @@ Director::Director() {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||
}
|
||||
|
||||
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
|
||||
// perquè és per (name, path) del mando, no per perfil/usuari. Replica
|
||||
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
||||
|
||||
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||
// es crea amb presets per defecte (off, crt, ntsc).
|
||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||
@@ -296,12 +302,29 @@ void Director::processEvents() {
|
||||
if (QUIT_REQUESTED) { current_scene_.reset(); }
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Resol el botó SERVICE configurat per al mando origen d'aquest event.
|
||||
// Retorna -1 si no en troba cap. La cerca passa per `GamepadManager`
|
||||
// per obtenir el (name, path) i d'ahí a `GamepadBindings`.
|
||||
auto serviceButtonForGamepadEvent(SDL_JoystickID jid) -> int {
|
||||
for (const auto& info : GamepadManager::get().getAll()) {
|
||||
if (info.pad == nullptr) { continue; }
|
||||
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
|
||||
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Director::processGamepadEvents() {
|
||||
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
|
||||
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
|
||||
// tractar el botó BACK com a toggle del menú quan està tancat (el
|
||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
|
||||
// passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||
// tractar el botó SERVICE (per defecte BACK; remapejable per
|
||||
// `GamepadBindings`) com a toggle del menú quan està tancat (el
|
||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el botó
|
||||
// SERVICE passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||
// `processMenuInput()` i allà es converteix en `close()`.
|
||||
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||
bool consumed = false;
|
||||
@@ -309,10 +332,12 @@ void Director::processGamepadEvents() {
|
||||
GamepadManager::get().handleEvent(*it);
|
||||
consumed = true;
|
||||
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
|
||||
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
|
||||
!ServiceMenu::get().isOpen()) {
|
||||
ServiceMenu::get().toggle();
|
||||
consumed = true;
|
||||
const int SERVICE_BTN = serviceButtonForGamepadEvent(it->gbutton.which);
|
||||
if (SERVICE_BTN >= 0 && static_cast<int>(it->gbutton.button) == SERVICE_BTN) {
|
||||
ServiceMenu::get().toggle();
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
if (consumed) {
|
||||
it = EventBuffer::events.erase(it);
|
||||
|
||||
Reference in new issue
Block a user