#include "core/input/gamepad_bindings.hpp" #include #include #include #include #include #include #include #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; 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"}}, ActionPair{GamepadBindings::Action::START, std::string_view{"start"}}, }; using ButtonPair = std::pair; constexpr std::array BUTTON_NAMES = { ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_SOUTH), std::string_view{"south"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_EAST), std::string_view{"east"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_WEST), std::string_view{"west"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_NORTH), std::string_view{"north"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_BACK), std::string_view{"back"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_GUIDE), std::string_view{"guide"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_START), std::string_view{"start"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_LEFT_STICK), std::string_view{"left_stick"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_RIGHT_STICK), std::string_view{"right_stick"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), std::string_view{"l1"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), std::string_view{"r1"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_DPAD_UP), std::string_view{"dpad_up"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_DPAD_DOWN), std::string_view{"dpad_down"}}, ButtonPair{static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT), std::string_view{"dpad_left"}}, ButtonPair{static_cast(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_arcade (vegeu Player::accumulateGamepadMovement // i les consultes del Director per al menú). switch (action) { case Action::JUMP: return static_cast(SDL_GAMEPAD_BUTTON_SOUTH); case Action::ATTACK: return static_cast(SDL_GAMEPAD_BUTTON_WEST); case Action::SERVICE: return static_cast(SDL_GAMEPAD_BUTTON_BACK); case Action::START: return static_cast(SDL_GAMEPAD_BUTTON_START); } return static_cast(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(i); } } } if (!name.empty()) { for (std::size_t i = 0; i < configs_.size(); ++i) { if (configs_[i].name == name) { return static_cast(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(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& 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(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(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(); } if (entry.contains("path")) { cfg.path = entry["path"].get_value(); } if (entry.contains("bindings") && entry["bindings"].is_mapping()) { for (const auto& kv : entry["bindings"].as_map()) { const auto KEY = kv.first.get_value(); const auto VAL = kv.second.get_value(); 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 ARCADE - 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(); }