#include "core/input/gamepad.hpp" #include #include #include "core/input/key_config.hpp" #include "core/jail/jinput.hpp" #include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" namespace Gamepad { static SDL_Gamepad* pad = nullptr; static SDL_JoystickID pad_id = 0; // Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id // de Chrome/Android no porta Vendor/Product, el parser extreu valors // escombraries, el GUID no està a gamecontrollerdb i el gamepad queda // obert amb un mapping incorrecte). Com el W3C Gamepad API garanteix // layout estàndard quan 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 } // Recorta el nom visible del mando: trim des del primer '(' o '[' // (per a evitar coses com "Retroid Controller (vendor: 1001) ..."), // elimina espais finals i talla a 25 caràcters. static auto prettyName(const char* raw) -> std::string { std::string name = ((raw != nullptr) && (*raw != 0)) ? raw : "Gamepad"; 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); } if (name.empty()) { name = "Gamepad"; } return name; } // Dead-zone del stick esquerre (rang Sint16: -32768..32767) static constexpr Sint16 STICK_DEADZONE = 12000; // Estat previ per a detecció de flanc (edge-triggered) static bool prev_up = false; static bool prev_down = false; static bool prev_left = false; static bool prev_right = false; static bool prev_south = false; static bool prev_east = false; static bool prev_west = false; static bool prev_north = false; static bool prev_start = false; static bool prev_back = false; static void notify(const std::string& name, const char* status_key) { std::string msg = name.empty() ? "Gamepad" : name; msg += ' '; msg += Locale::get(status_key); Overlay::showNotification(msg.c_str(), 2.5F); } static void notifyConnected(const std::string& name) { notify(name, "notifications.gamepad_connected"); } static void notifyDisconnected(const std::string& name) { notify(name, "notifications.gamepad_disconnected"); } // Obri el primer joystick disponible que siga reconegut com a gamepad // (o que ho esdevinga després d'injectar el mapping web estàndard). static void openFirstGamepad() { int count = 0; SDL_JoystickID* ids = SDL_GetJoysticks(&count); if (ids != nullptr) { for (int i = 0; i < count; ++i) { installWebStandardMapping(ids[i]); if (!SDL_IsGamepad(ids[i])) { continue; } pad = SDL_OpenGamepad(ids[i]); if (pad != nullptr) { pad_id = ids[i]; SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad)); break; } } SDL_free(ids); } } void init() { if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { SDL_Log("No s'ha pogut inicialitzar SDL_INIT_GAMEPAD: %s", SDL_GetError()); return; } int added = SDL_AddGamepadMappingsFromFile("gamecontrollerdb.txt"); if (added < 0) { SDL_Log("No s'ha pogut carregar gamecontrollerdb.txt: %s", SDL_GetError()); } else { SDL_Log("Carregats %d mappings de gamepad", added); } openFirstGamepad(); } void destroy() { if (pad != nullptr) { SDL_CloseGamepad(pad); pad = nullptr; pad_id = 0; } SDL_QuitSubSystem(SDL_INIT_GAMEPAD); } auto isConnected() -> bool { return pad != nullptr; } void handleEvent(const SDL_Event& event) { // A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no // GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i // injectem el mapping estàndard abans d'obrir el mando. if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) { if (pad == nullptr) { SDL_JoystickID jid = event.jdevice.which; installWebStandardMapping(jid); if (!SDL_IsGamepad(jid)) { return; } pad = SDL_OpenGamepad(jid); if (pad != nullptr) { pad_id = jid; std::string name = prettyName(SDL_GetGamepadName(pad)); SDL_Log("Gamepad connectat: %s", name.c_str()); notifyConnected(name); } } } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) { if ((pad != nullptr) && event.jdevice.which == pad_id) { std::string saved_name = prettyName(SDL_GetGamepadName(pad)); SDL_Log("Gamepad desconnectat: %s", saved_name.c_str()); SDL_CloseGamepad(pad); pad = nullptr; pad_id = 0; // Neteja qualsevol tecla virtual que poguera estar premuda Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false); notifyDisconnected(saved_name); } } } // Emet un parell de KEY_DOWN + KEY_UP sintètics que passaran pel handleEvents // del Director (menú, ESC, F12, etc.) static void pushKey(SDL_Scancode sc) { SDL_Event e; SDL_zero(e); e.type = SDL_EVENT_KEY_DOWN; e.key.scancode = sc; e.key.repeat = false; e.key.down = true; SDL_PushEvent(&e); e.type = SDL_EVENT_KEY_UP; e.key.down = false; SDL_PushEvent(&e); } // Estat agregat d'un frame: D-pad i stick combinats, més botons frontals. struct PadState { bool up; bool down; bool left; bool right; bool south; bool east; bool west; bool north; bool start; bool back; }; static auto readPadState() -> PadState { const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX); const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY); return PadState{ .up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE, .down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE, .left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE, .right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE, .south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH), .east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST), .west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST), .north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH), .start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START), .back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK), }; } static void handleMenuNavigation(const PadState& s) { if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); } if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); } if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); } if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); } if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); } if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); } // Mentre el menú està obert, el joc no ha de rebre moviment. Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false); } static void handleGameInput(const PadState& s) { Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up); Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down); Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left); Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right); const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) || (s.west && !prev_west) || (s.north && !prev_north); if (ANY_FRONT_EDGE) { pushKey(SDL_SCANCODE_RETURN); } } void update() { if (pad == nullptr) { return; } const PadState S = readPadState(); // Flancs globals: Select i Start sempre operen. if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); } if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); } if (Menu::isOpen()) { handleMenuNavigation(S); } else { handleGameInput(S); } prev_up = S.up; prev_down = S.down; prev_left = S.left; prev_right = S.right; prev_south = S.south; prev_east = S.east; prev_west = S.west; prev_north = S.north; prev_start = S.start; prev_back = S.back; } } // namespace Gamepad