fix: support input from multiple gamepads #263
No files matched your search
@@ -41,6 +41,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Исправлено управление интерфейсом при одновременном подключении нескольких геймпадов
|
||||
- Исправлена коллизия пользовательских данных автоустановок с одинаковыми именами исходных исполняемых файлов
|
||||
- Исправлен поиск скриптов и локалей через `XDG_DATA_DIRS` в NixOS
|
||||
- Исправлена проверка и установка недостающих i586-зависимостей в ALT Linux перед запуском игр
|
||||
|
||||
Binary file not shown.
+136
-35
@@ -3,28 +3,118 @@
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define AXIS_ACTIVATION_THRESHOLD 16000
|
||||
|
||||
typedef struct {
|
||||
SDL_Gamepad *gamepad;
|
||||
SDL_JoystickID instance_id;
|
||||
int sdl_type;
|
||||
bool buttons[SDL_GAMEPAD_BUTTON_COUNT];
|
||||
bool axes_active[SDL_GAMEPAD_AXIS_COUNT];
|
||||
} GamepadEntry;
|
||||
|
||||
typedef struct {
|
||||
GamepadEntry *entries;
|
||||
int count;
|
||||
SDL_JoystickID active_instance_id;
|
||||
} PortProtonGamepad;
|
||||
|
||||
static PortProtonGamepad *open_gamepad(SDL_JoystickID instance_id)
|
||||
static bool has_gamepad(const PortProtonGamepad *gamepads,
|
||||
SDL_JoystickID instance_id)
|
||||
{
|
||||
for (int index = 0; index < gamepads->count; index++) {
|
||||
if (gamepads->entries[index].instance_id == instance_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool add_gamepad(PortProtonGamepad *gamepads,
|
||||
SDL_JoystickID instance_id)
|
||||
{
|
||||
SDL_Gamepad *gamepad = SDL_OpenGamepad(instance_id);
|
||||
if (gamepad == NULL) {
|
||||
return NULL;
|
||||
return false;
|
||||
}
|
||||
PortProtonGamepad *result = malloc(sizeof(*result));
|
||||
if (result == NULL) {
|
||||
GamepadEntry *entries = realloc(
|
||||
gamepads->entries, sizeof(*entries) * (gamepads->count + 1));
|
||||
if (entries == NULL) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
SDL_SetError("Failed to allocate gamepad handle");
|
||||
return SDL_SetError("Failed to allocate gamepad handle");
|
||||
}
|
||||
gamepads->entries = entries;
|
||||
GamepadEntry entry = {0};
|
||||
entry.gamepad = gamepad;
|
||||
entry.instance_id = instance_id;
|
||||
entry.sdl_type = SDL_GetGamepadTypeForID(instance_id);
|
||||
entries[gamepads->count] = entry;
|
||||
gamepads->count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void remove_disconnected(PortProtonGamepad *gamepads)
|
||||
{
|
||||
int active_count = 0;
|
||||
for (int index = 0; index < gamepads->count; index++) {
|
||||
GamepadEntry entry = gamepads->entries[index];
|
||||
if (SDL_GamepadConnected(entry.gamepad)) {
|
||||
gamepads->entries[active_count++] = entry;
|
||||
} else {
|
||||
if (gamepads->active_instance_id == entry.instance_id) {
|
||||
gamepads->active_instance_id = 0;
|
||||
}
|
||||
SDL_CloseGamepad(entry.gamepad);
|
||||
}
|
||||
}
|
||||
gamepads->count = active_count;
|
||||
}
|
||||
|
||||
static void update_gamepads(PortProtonGamepad *gamepads)
|
||||
{
|
||||
SDL_UpdateGamepads();
|
||||
remove_disconnected(gamepads);
|
||||
int count = 0;
|
||||
SDL_JoystickID *instance_ids = SDL_GetGamepads(&count);
|
||||
if (instance_ids == NULL) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (!has_gamepad(gamepads, instance_ids[index])) {
|
||||
add_gamepad(gamepads, instance_ids[index]);
|
||||
}
|
||||
}
|
||||
SDL_free(instance_ids);
|
||||
}
|
||||
|
||||
static bool update_entry_activity(GamepadEntry *entry)
|
||||
{
|
||||
bool activated = false;
|
||||
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; button++) {
|
||||
bool pressed = SDL_GetGamepadButton(entry->gamepad, button);
|
||||
activated |= pressed && !entry->buttons[button];
|
||||
entry->buttons[button] = pressed;
|
||||
}
|
||||
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; axis++) {
|
||||
int value = SDL_GetGamepadAxis(entry->gamepad, axis);
|
||||
bool active = abs(value) >= AXIS_ACTIVATION_THRESHOLD;
|
||||
activated |= active && !entry->axes_active[axis];
|
||||
entry->axes_active[axis] = active;
|
||||
}
|
||||
return activated;
|
||||
}
|
||||
|
||||
static const GamepadEntry *get_active_gamepad(const PortProtonGamepad *gamepads)
|
||||
{
|
||||
if (gamepads == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
result->gamepad = gamepad;
|
||||
result->instance_id = instance_id;
|
||||
result->sdl_type = SDL_GetGamepadTypeForID(instance_id);
|
||||
return result;
|
||||
for (int index = 0; index < gamepads->count; index++) {
|
||||
if (gamepads->entries[index].instance_id == gamepads->active_instance_id) {
|
||||
return &gamepads->entries[index];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PortProtonGamepad *portproton_gamepad_find(void)
|
||||
@@ -33,23 +123,17 @@ PortProtonGamepad *portproton_gamepad_find(void)
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
return NULL;
|
||||
}
|
||||
SDL_PumpEvents();
|
||||
SDL_UpdateGamepads();
|
||||
int count = 0;
|
||||
SDL_JoystickID *gamepads = SDL_GetGamepads(&count);
|
||||
if (gamepads == NULL) {
|
||||
PortProtonGamepad *result = calloc(1, sizeof(*result));
|
||||
if (result == NULL) {
|
||||
SDL_SetError("Failed to allocate gamepad collection");
|
||||
return NULL;
|
||||
}
|
||||
if (count == 0) {
|
||||
SDL_free(gamepads);
|
||||
update_gamepads(result);
|
||||
if (result->count == 0) {
|
||||
free(result);
|
||||
SDL_ClearError();
|
||||
return NULL;
|
||||
}
|
||||
PortProtonGamepad *result = NULL;
|
||||
for (int index = 0; index < count && result == NULL; index++) {
|
||||
result = open_gamepad(gamepads[index]);
|
||||
}
|
||||
SDL_free(gamepads);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -64,56 +148,73 @@ void portproton_gamepad_close(PortProtonGamepad *gamepad)
|
||||
if (gamepad == NULL) {
|
||||
return;
|
||||
}
|
||||
if (gamepad->gamepad != NULL) {
|
||||
SDL_CloseGamepad(gamepad->gamepad);
|
||||
for (int index = 0; index < gamepad->count; index++) {
|
||||
SDL_CloseGamepad(gamepad->entries[index].gamepad);
|
||||
}
|
||||
free(gamepad->entries);
|
||||
free(gamepad);
|
||||
}
|
||||
|
||||
bool portproton_gamepad_connected(const PortProtonGamepad *gamepad)
|
||||
{
|
||||
return gamepad != NULL && gamepad->gamepad != NULL &&
|
||||
SDL_GamepadConnected(gamepad->gamepad);
|
||||
return gamepad != NULL && gamepad->count > 0;
|
||||
}
|
||||
|
||||
void portproton_gamepad_update(void)
|
||||
void portproton_gamepad_update(PortProtonGamepad *gamepad)
|
||||
{
|
||||
SDL_UpdateGamepads();
|
||||
if (gamepad != NULL) {
|
||||
update_gamepads(gamepad);
|
||||
for (int index = 0; index < gamepad->count; index++) {
|
||||
if (update_entry_activity(&gamepad->entries[index])) {
|
||||
gamepad->active_instance_id = gamepad->entries[index].instance_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int portproton_gamepad_get_button(const PortProtonGamepad *gamepad, int button)
|
||||
{
|
||||
if (gamepad == NULL || gamepad->gamepad == NULL) {
|
||||
const GamepadEntry *active = get_active_gamepad(gamepad);
|
||||
if (active == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return SDL_GetGamepadButton(gamepad->gamepad, button);
|
||||
return SDL_GetGamepadButton(active->gamepad, button);
|
||||
}
|
||||
|
||||
int16_t portproton_gamepad_get_axis(const PortProtonGamepad *gamepad, int axis)
|
||||
{
|
||||
if (gamepad == NULL || gamepad->gamepad == NULL) {
|
||||
const GamepadEntry *active = get_active_gamepad(gamepad);
|
||||
if (active == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return SDL_GetGamepadAxis(gamepad->gamepad, axis);
|
||||
return SDL_GetGamepadAxis(active->gamepad, axis);
|
||||
}
|
||||
|
||||
uint32_t portproton_gamepad_get_active_instance_id(
|
||||
const PortProtonGamepad *gamepad)
|
||||
{
|
||||
return gamepad != NULL ? gamepad->active_instance_id : 0;
|
||||
}
|
||||
|
||||
const char *portproton_gamepad_get_name(const PortProtonGamepad *gamepad)
|
||||
{
|
||||
if (gamepad == NULL || gamepad->gamepad == NULL) {
|
||||
if (gamepad == NULL || gamepad->count == 0) {
|
||||
return "";
|
||||
}
|
||||
const char *name = SDL_GetGamepadName(gamepad->gamepad);
|
||||
const char *name = SDL_GetGamepadName(gamepad->entries[0].gamepad);
|
||||
return name != NULL ? name : "";
|
||||
}
|
||||
|
||||
uint32_t portproton_gamepad_get_instance_id(const PortProtonGamepad *gamepad)
|
||||
{
|
||||
return gamepad != NULL ? gamepad->instance_id : 0;
|
||||
return gamepad != NULL && gamepad->count > 0
|
||||
? gamepad->entries[0].instance_id : 0;
|
||||
}
|
||||
|
||||
int portproton_gamepad_get_type(const PortProtonGamepad *gamepad)
|
||||
{
|
||||
return gamepad != NULL ? gamepad->sdl_type : 0;
|
||||
return gamepad != NULL && gamepad->count > 0
|
||||
? gamepad->entries[0].sdl_type : 0;
|
||||
}
|
||||
|
||||
void portproton_gamepad_shutdown(void)
|
||||
|
||||
@@ -175,7 +175,7 @@ class GamepadRuntimeMixin(InputMixin):
|
||||
keyboard.update_keyboard()
|
||||
|
||||
def find_gamepad(self) -> SDLGamepad | None:
|
||||
"""Find the first SDL3 gamepad with a standardized mapping."""
|
||||
"""Open the SDL3 handle that combines all connected gamepads."""
|
||||
try:
|
||||
gamepad = find_gamepad()
|
||||
self._last_gamepad_error = None
|
||||
@@ -353,10 +353,16 @@ class GamepadRuntimeMixin(InputMixin):
|
||||
self._last_gamepad_check_time = current_time
|
||||
return
|
||||
try:
|
||||
active_gamepad.update()
|
||||
active_changed = active_gamepad.update()
|
||||
if not active_gamepad.connected():
|
||||
self.gamepad_hotplug.emit('remove')
|
||||
return
|
||||
if active_changed:
|
||||
self._reset_input_state()
|
||||
logger.info(
|
||||
"Active gamepad changed to SDL instance %s",
|
||||
active_gamepad.active_instance_id,
|
||||
)
|
||||
self._poll_button_events(active_gamepad, current_time)
|
||||
self._handle_pending_menu_fullscreen(current_time)
|
||||
self._poll_hat_events(active_gamepad, current_time)
|
||||
|
||||
@@ -28,12 +28,14 @@ _library.portproton_gamepad_close.argtypes = [_handle]
|
||||
_library.portproton_gamepad_close.restype = None
|
||||
_library.portproton_gamepad_connected.argtypes = [_handle]
|
||||
_library.portproton_gamepad_connected.restype = ctypes.c_bool
|
||||
_library.portproton_gamepad_update.argtypes = []
|
||||
_library.portproton_gamepad_update.argtypes = [_handle]
|
||||
_library.portproton_gamepad_update.restype = None
|
||||
_library.portproton_gamepad_get_button.argtypes = [_handle, ctypes.c_int]
|
||||
_library.portproton_gamepad_get_button.restype = ctypes.c_int
|
||||
_library.portproton_gamepad_get_axis.argtypes = [_handle, ctypes.c_int]
|
||||
_library.portproton_gamepad_get_axis.restype = ctypes.c_int16
|
||||
_library.portproton_gamepad_get_active_instance_id.argtypes = [_handle]
|
||||
_library.portproton_gamepad_get_active_instance_id.restype = ctypes.c_uint32
|
||||
_library.portproton_gamepad_get_name.argtypes = [_handle]
|
||||
_library.portproton_gamepad_get_name.restype = ctypes.c_char_p
|
||||
_library.portproton_gamepad_get_instance_id.argtypes = [_handle]
|
||||
@@ -45,7 +47,7 @@ _library.portproton_gamepad_shutdown.restype = None
|
||||
|
||||
|
||||
class GamepadBackendError(RuntimeError):
|
||||
"""SDL failed while discovering the active gamepad."""
|
||||
"""SDL failed while discovering connected gamepads."""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -55,6 +57,7 @@ class SDLGamepad:
|
||||
path: str
|
||||
instance_id: int
|
||||
sdl_type: int
|
||||
active_instance_id: int = 0
|
||||
|
||||
def close(self) -> None:
|
||||
if self.controller:
|
||||
@@ -67,8 +70,15 @@ class SDLGamepad:
|
||||
def connected(self) -> bool:
|
||||
return bool(_library.portproton_gamepad_connected(self.controller))
|
||||
|
||||
def update(self) -> None:
|
||||
_library.portproton_gamepad_update()
|
||||
def update(self) -> bool:
|
||||
_library.portproton_gamepad_update(self.controller)
|
||||
active_instance_id = int(
|
||||
_library.portproton_gamepad_get_active_instance_id(self.controller)
|
||||
)
|
||||
if active_instance_id == self.active_instance_id:
|
||||
return False
|
||||
self.active_instance_id = active_instance_id
|
||||
return True
|
||||
|
||||
def get_button(self, button: int) -> int:
|
||||
return int(_library.portproton_gamepad_get_button(self.controller, button))
|
||||
|
||||
@@ -91,6 +91,30 @@ def test_native_gamepad_result_preserves_python_interface(monkeypatch: MonkeyPat
|
||||
gamepad.close()
|
||||
|
||||
|
||||
def test_native_gamepad_update_passes_collection_handle(monkeypatch: MonkeyPatch) -> None:
|
||||
updated_handles: list[int] = []
|
||||
gamepad = SDLGamepad(42, "Gamepads", "sdl3-gamepad:7", 7, 1)
|
||||
monkeypatch.setattr(native_gamepad._library, "portproton_gamepad_close", lambda _handle: None)
|
||||
monkeypatch.setattr(
|
||||
native_gamepad._library,
|
||||
"portproton_gamepad_update",
|
||||
updated_handles.append,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
native_gamepad._library,
|
||||
"portproton_gamepad_get_active_instance_id",
|
||||
lambda _handle: 9,
|
||||
)
|
||||
|
||||
changed = gamepad.update()
|
||||
|
||||
assert updated_handles == [42]
|
||||
assert changed is True
|
||||
assert gamepad.active_instance_id == 9
|
||||
assert gamepad.update() is False
|
||||
gamepad.close()
|
||||
|
||||
|
||||
def test_native_gamepad_reports_sdl_discovery_error(monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(native_gamepad._library, "portproton_gamepad_find", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
|
||||
Reference in new issue
Block a user