#include "core/input/input.h" #include #include // for ranges::any_of #include // for basic_ostream, operator<<, cout, basi... #include // Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android // amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el // parser extreu valors escombraries, el GUID resultant no està a la db i el // gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API // garanteix el layout estàndard quan el navegador reporta mapping=="standard", // injectem un mapping SDL amb eixe layout per al GUID del joystick abans // d'obrir-lo com gamepad. Fora d'Emscripten és un no-op. static void installWebStandardMapping(SDL_JoystickID jid) { #ifdef __EMSCRIPTEN__ SDL_GUID guid = SDL_GetJoystickGUIDForID(jid); char guidStr[33]; SDL_GUIDToString(guid, guidStr, sizeof(guidStr)); const char *name = SDL_GetJoystickNameForID(jid); if (!name || !*name) name = "Standard Gamepad"; char mapping[512]; SDL_snprintf(mapping, sizeof(mapping), "%s,%s," "a:b0,b:b1,x:b2,y:b3," "leftshoulder:b4,rightshoulder:b5," "lefttrigger:b6,righttrigger:b7," "back:b8,start:b9," "leftstick:b10,rightstick:b11," "dpup:b12,dpdown:b13,dpleft:b14,dpright:b15," "guide:b16," "leftx:a0,lefty:a1,rightx:a2,righty:a3," "platform:Emscripten", guidStr, name); SDL_AddGamepadMapping(mapping); #else (void)jid; #endif } // Instancia única Input *Input::instance = nullptr; // Singleton API void Input::init(const std::string &game_controller_db_path) { Input::instance = new Input(game_controller_db_path); } void Input::destroy() { delete Input::instance; Input::instance = nullptr; } auto Input::get() -> Input * { return Input::instance; } // Constructor Input::Input(std::string file) : db_path_(std::move(file)) { // Inicializa las variables KeyBindings kb; kb.scancode = 0; kb.active = false; key_bindings_.resize(static_cast(Action::NUMBER_OF_INPUTS), kb); GameControllerBindings gcb; gcb.button = SDL_GAMEPAD_BUTTON_INVALID; gcb.active = false; game_controller_bindings_.resize(static_cast(Action::NUMBER_OF_INPUTS), gcb); } // Destructor Input::~Input() { for (auto *pad : connected_controllers_) { if (pad != nullptr) { SDL_CloseGamepad(pad); } } connected_controllers_.clear(); connected_controller_ids_.clear(); controller_names_.clear(); num_gamepads_ = 0; } // Actualiza el estado del objeto void Input::update() { if (disabled_until_ == Disable::KEY_PRESSED && !checkAnyInput()) { enable(); } } // Asigna inputs a teclas void Input::bindKey(Action input, SDL_Scancode code) { key_bindings_[static_cast(input)].scancode = code; } // Asigna inputs a botones del mando void Input::bindGameControllerButton(Action input, SDL_GamepadButton button) { game_controller_bindings_[static_cast(input)].button = button; } // Comprueba si un input esta activo auto Input::checkInput(Action input, Repeat repeat, Device device, int index) -> bool { if (!enabled_) { return false; } if (device == Device::ANY) { index = 0; } bool success_keyboard = false; if (device == Device::KEYBOARD || device == Device::ANY) { success_keyboard = checkKeyboardInput(input, repeat); } bool success_game_controller = false; if ((device == Device::GAMECONTROLLER || device == Device::ANY) && gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) { success_game_controller = checkGameControllerInput(input, repeat, index); } return success_keyboard || success_game_controller; } // Helper de checkInput: comprueba el estado de una tecla auto Input::checkKeyboardInput(Action input, Repeat repeat) -> bool { const auto IDX = static_cast(input); const bool *key_states = SDL_GetKeyboardState(nullptr); const bool IS_DOWN = key_states[key_bindings_[IDX].scancode]; if (repeat == Repeat::ON) { return IS_DOWN; } // Modo edge-trigger: éxito sólo en el frame en que la tecla pasa de up a down const bool PRESS_EDGE = IS_DOWN && !key_bindings_[IDX].active; key_bindings_[IDX].active = IS_DOWN; return PRESS_EDGE; } // Helper de checkInput: comprueba el estado de un botón de mando auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) -> bool { const auto IDX = static_cast(input); const bool IS_DOWN = SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[IDX].button); if (repeat == Repeat::ON) { return IS_DOWN; } // Modo edge-trigger: éxito sólo en el frame en que el botón pasa de up a down const bool PRESS_EDGE = IS_DOWN && !game_controller_bindings_[IDX].active; game_controller_bindings_[IDX].active = IS_DOWN; return PRESS_EDGE; } // Comprueba si hay almenos un input "humano" activo (moviment, ACCEPT/CANCEL, // FIRE_*). Exclou les accions reservades a hotkeys globals (EXIT, PAUSE, // WINDOW_*, *SHADER*) perque prémer F1-F12 o ESC no s'ha de comptar com // "qualsevol tecla" — ningu vol saltar una intro per modificar el zoom. auto Input::checkAnyInput(Device device, int index) -> bool { if (device == Device::ANY) { index = 0; } auto is_skippable = [](Action a) { switch (a) { case Action::UP: case Action::DOWN: case Action::LEFT: case Action::RIGHT: case Action::ACCEPT: case Action::CANCEL: case Action::FIRE_LEFT: case Action::FIRE_CENTER: case Action::FIRE_RIGHT: return true; default: return false; } }; if (device == Device::KEYBOARD || device == Device::ANY) { const bool *key_states = SDL_GetKeyboardState(nullptr); for (std::size_t i = 0; i < key_bindings_.size(); ++i) { if (is_skippable(static_cast(i)) && key_states[key_bindings_[i].scancode]) { return true; } } } if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) { if (device == Device::GAMECONTROLLER || device == Device::ANY) { for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) { if (is_skippable(static_cast(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) { return true; } } } } return false; } // Construye el nombre visible de un mando. // Recorta des del primer '(' o '[' (per a evitar coses tipus // "Retroid Controller (vendor: 1001) ...") i talla a 25 caràcters. auto Input::buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string { (void)pad_index; const char *pad_name = SDL_GetGamepadName(pad); std::string name = (pad_name != nullptr) ? pad_name : "Unknown"; const auto POS = name.find_first_of("(["); if (POS != std::string::npos) { name.erase(POS); } while (!name.empty() && name.back() == ' ') { name.pop_back(); } if (name.size() > 25) { name.resize(25); } return name; } // Busca si hay un mando conectado. Cierra y limpia el estado previo para // que la función sea idempotente si se invoca más de una vez. auto Input::discoverGameController() -> bool { resetGameControllerState(); ensureGamepadSubsystem(); int num_joysticks = 0; SDL_JoystickID *joysticks = SDL_GetJoysticks(&num_joysticks); if (joysticks == nullptr) { return false; } int gamepad_count = 0; for (int i = 0; i < num_joysticks; ++i) { if (SDL_IsGamepad(joysticks[i])) { gamepad_count++; } } if (verbose_) { std::cout << "\nChecking for game controllers...\n"; std::cout << num_joysticks << " joysticks found, " << gamepad_count << " are gamepads\n"; } bool found = false; if (gamepad_count > 0) { found = true; int pad_index = 0; for (int i = 0; i < num_joysticks; i++) { if (!SDL_IsGamepad(joysticks[i])) { continue; } if (openGamepad(joysticks[i], pad_index)) { pad_index++; } } SDL_SetGamepadEventsEnabled(true); } SDL_free(joysticks); return found; } // Helper de discoverGameController: cierra mandos previos y limpia vectores paralelos void Input::resetGameControllerState() { for (auto *pad : connected_controllers_) { if (pad != nullptr) { SDL_CloseGamepad(pad); } } connected_controllers_.clear(); connected_controller_ids_.clear(); controller_names_.clear(); num_gamepads_ = 0; } // Helper de discoverGameController: inicializa el subsystem de gamepad y carga el mapping void Input::ensureGamepadSubsystem() { if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) { SDL_InitSubSystem(SDL_INIT_GAMEPAD); } if (SDL_AddGamepadMappingsFromFile(db_path_.c_str()) < 0 && verbose_) { std::cout << "Error, could not load " << db_path_.c_str() << " file: " << SDL_GetError() << '\n'; } } // Helper de discoverGameController: abre y registra un mando. Devuelve true si tuvo éxito. auto Input::openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool { installWebStandardMapping(joystick_id); SDL_Gamepad *pad = SDL_OpenGamepad(joystick_id); if (pad == nullptr) { if (verbose_) { std::cout << "SDL_GetError() = " << SDL_GetError() << '\n'; } return false; } const std::string NAME = buildControllerName(pad, pad_index); connected_controllers_.push_back(pad); connected_controller_ids_.push_back(joystick_id); controller_names_.push_back(NAME); num_gamepads_++; if (verbose_) { std::cout << NAME << '\n'; } return true; } // Procesa un evento SDL_EVENT_GAMEPAD_ADDED auto Input::handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool { if (!SDL_IsGamepad(jid)) { return false; } // Si el mando ya está registrado no hace nada (ej. evento retroactivo tras el scan inicial) if (std::ranges::any_of(connected_controller_ids_, [jid](SDL_JoystickID existing) { return existing == jid; })) { return false; } installWebStandardMapping(jid); SDL_Gamepad *pad = SDL_OpenGamepad(jid); if (pad == nullptr) { if (verbose_) { std::cout << "Failed to open gamepad " << jid << ": " << SDL_GetError() << '\n'; } return false; } const int PAD_INDEX = (int)connected_controllers_.size(); const std::string NAME = buildControllerName(pad, PAD_INDEX); connected_controllers_.push_back(pad); connected_controller_ids_.push_back(jid); controller_names_.push_back(NAME); num_gamepads_++; if (verbose_) { std::cout << "Gamepad connected: " << NAME << '\n'; } out_name = NAME; return true; } // Procesa un evento SDL_EVENT_GAMEPAD_REMOVED auto Input::handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool { for (size_t i = 0; i < connected_controller_ids_.size(); ++i) { if (connected_controller_ids_[i] != jid) { continue; } out_name = controller_names_[i]; if (connected_controllers_[i] != nullptr) { SDL_CloseGamepad(connected_controllers_[i]); } connected_controllers_.erase(connected_controllers_.begin() + i); connected_controller_ids_.erase(connected_controller_ids_.begin() + i); controller_names_.erase(controller_names_.begin() + i); num_gamepads_--; num_gamepads_ = std::max(num_gamepads_, 0); if (verbose_) { std::cout << "Gamepad disconnected: " << out_name << '\n'; } return true; } return false; } // Comprueba si hay algun mando conectado auto Input::gameControllerFound() const -> bool { return num_gamepads_ > 0; } // Obten el nombre de un mando de juego auto Input::getControllerName(int index) -> std::string { if (num_gamepads_ > 0) { return controller_names_[index]; } return ""; } // Obten el numero de mandos conectados auto Input::getNumControllers() const -> int { return num_gamepads_; } // Establece si ha de mostrar mensajes void Input::setVerbose(bool value) { verbose_ = value; } // Deshabilita las entradas durante un periodo de tiempo void Input::disableUntil(Disable value) { disabled_until_ = value; enabled_ = false; } // Hablita las entradas void Input::enable() { enabled_ = true; disabled_until_ = Disable::NOT_DISABLED; }