224 lines
8.8 KiB
C++
224 lines
8.8 KiB
C++
#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"}},
|
|
ActionPair{GamepadBindings::Action::START, std::string_view{"start"}},
|
|
};
|
|
|
|
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);
|
|
case Action::START:
|
|
return static_cast<int>(SDL_GAMEPAD_BUTTON_START);
|
|
}
|
|
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();
|
|
}
|