267 lines
10 KiB
C++
267 lines
10 KiB
C++
#include "core/input/gamepad.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
#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 std::string prettyName(const char* raw) {
|
|
std::string name = (raw && *raw) ? 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) {
|
|
for (int i = 0; i < count; ++i) {
|
|
installWebStandardMapping(ids[i]);
|
|
if (!SDL_IsGamepad(ids[i])) continue;
|
|
pad_ = SDL_OpenGamepad(ids[i]);
|
|
if (pad_) {
|
|
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_) {
|
|
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_) {
|
|
SDL_JoystickID jid = event.jdevice.which;
|
|
installWebStandardMapping(jid);
|
|
if (!SDL_IsGamepad(jid)) return;
|
|
pad_ = SDL_OpenGamepad(jid);
|
|
if (pad_) {
|
|
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_ && 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_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_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);
|
|
}
|
|
|
|
void update() {
|
|
if (!pad_) return;
|
|
|
|
// D-pad
|
|
bool dup = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
|
bool ddn = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
bool dlt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
bool drt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
|
|
|
// Stick esquerre amb dead-zone
|
|
Sint16 lx = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTX);
|
|
Sint16 ly = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTY);
|
|
bool sup = ly < -STICK_DEADZONE;
|
|
bool sdn = ly > STICK_DEADZONE;
|
|
bool slt = lx < -STICK_DEADZONE;
|
|
bool srt = lx > STICK_DEADZONE;
|
|
|
|
bool up = dup || sup;
|
|
bool dn = ddn || sdn;
|
|
bool lt = dlt || slt;
|
|
bool rt = drt || srt;
|
|
|
|
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
|
|
bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
|
|
bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST);
|
|
bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST);
|
|
bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH);
|
|
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
|
|
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
|
|
|
|
// Select (Back) → obre/tanca menú de servei (flanc)
|
|
if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
|
|
// Start → pausa (flanc)
|
|
if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
|
|
|
|
if (Menu::isOpen()) {
|
|
// Navegació del menú per flanc
|
|
if (up && !prev_up_) pushKey(SDL_SCANCODE_UP);
|
|
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
|
|
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
|
|
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
|
|
// EAST accepta, SOUTH cancela / endarrere
|
|
if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
|
|
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
|
|
|
|
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
|
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
|
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
|
|
} else {
|
|
// Moviment al joc — level-triggered (polling)
|
|
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, up);
|
|
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
|
|
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
|
|
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
|
|
// Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
|
|
if ((south && !prev_south_) || (east && !prev_east_) ||
|
|
(west && !prev_west_) || (north && !prev_north_)) {
|
|
pushKey(SDL_SCANCODE_RETURN);
|
|
}
|
|
}
|
|
|
|
prev_up_ = up;
|
|
prev_down_ = dn;
|
|
prev_left_ = lt;
|
|
prev_right_ = rt;
|
|
prev_south_ = south;
|
|
prev_east_ = east;
|
|
prev_west_ = west;
|
|
prev_north_ = north;
|
|
prev_start_ = start;
|
|
prev_back_ = back;
|
|
}
|
|
|
|
} // namespace Gamepad
|