diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d5578b..3703674f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ ### Fixed +- Исправлено управление интерфейсом при одновременном подключении нескольких геймпадов - Исправлена коллизия пользовательских данных автоустановок с одинаковыми именами исходных исполняемых файлов - Исправлен поиск скриптов и локалей через `XDG_DATA_DIRS` в NixOS - Исправлена проверка и установка недостающих i586-зависимостей в ALT Linux перед запуском игр diff --git a/build-aux/lib/libportprotonqt_gamepad.so b/build-aux/lib/libportprotonqt_gamepad.so index 940282be..b0aca6ed 100755 Binary files a/build-aux/lib/libportprotonqt_gamepad.so and b/build-aux/lib/libportprotonqt_gamepad.so differ diff --git a/build-aux/src/gamepad_backend.c b/build-aux/src/gamepad_backend.c index cf2a28eb..6caeea4c 100644 --- a/build-aux/src/gamepad_backend.c +++ b/build-aux/src/gamepad_backend.c @@ -3,28 +3,118 @@ #include #include +#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) diff --git a/portprotonqt/input_manager/runtime.py b/portprotonqt/input_manager/runtime.py index b430821a..64168f70 100644 --- a/portprotonqt/input_manager/runtime.py +++ b/portprotonqt/input_manager/runtime.py @@ -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) diff --git a/portprotonqt/native_gamepad.py b/portprotonqt/native_gamepad.py index 3df9bfc9..d9447496 100644 --- a/portprotonqt/native_gamepad.py +++ b/portprotonqt/native_gamepad.py @@ -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)) diff --git a/tests/test_input_manager.py b/tests/test_input_manager.py index b9852431..eb669f3a 100644 --- a/tests/test_input_manager.py +++ b/tests/test_input_manager.py @@ -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(