From 962785bda85a6cc052bc65d8051e7de8092afd2a Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 3 Jun 2026 20:51:22 -0400 Subject: [PATCH 1/4] Fix SDL3 joystick enumeration using device indices as instance IDs get_joysticks/get_controllers/_get_all passed range(count) indices into SDL_OpenJoystick/SDL_OpenGamepad/SDL_IsGamepad, which on SDL3 take an SDL_JoystickID instance ID. _get_number discarded the instance-ID array that SDL_GetJoysticks returns, so enumeration opened the wrong device or failed once instance IDs diverged from device indices (e.g. after a pad was reconnected). Replace _get_number with _get_instance_ids, which keeps and frees the SDL_GetJoysticks array, and enumerate by instance ID. Fixes #181 --- CHANGELOG.md | 4 ++++ tcod/sdl/joystick.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc4bfbb..39f9c702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - PyPy wheels switched from PyPy 3.10 to PyPy 3.11. - Experimental Pyodide wheels are now uploaded to PyPI. +### Fixed + +- `tcod.sdl.joystick.get_joysticks`, `get_controllers`, and related enumeration passed device indices to SDL3 functions expecting instance IDs, so enumeration could fail or open the wrong device after a joystick was reconnected. + ## [21.2.0] - 2026-04-04 ### Added diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index e6f2de84..5965517d 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -126,9 +126,9 @@ def __init__(self, sdl_joystick_p: Any) -> None: # noqa: ANN401 self._by_instance_id[self.id] = self @classmethod - def _open(cls, device_index: int) -> Joystick: + def _open(cls, instance_id: int) -> Joystick: tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) - p = _check_p(ffi.gc(lib.SDL_OpenJoystick(device_index), lib.SDL_CloseJoystick)) + p = _check_p(ffi.gc(lib.SDL_OpenJoystick(instance_id), lib.SDL_CloseJoystick)) return cls(p) @classmethod @@ -184,8 +184,8 @@ def __init__(self, sdl_controller_p: Any) -> None: # noqa: ANN401 self._by_instance_id[self.joystick.id] = self @classmethod - def _open(cls, joystick_index: int) -> GameController: - return cls(_check_p(ffi.gc(lib.SDL_OpenGamepad(joystick_index), lib.SDL_CloseGamepad))) + def _open(cls, instance_id: int) -> GameController: + return cls(_check_p(ffi.gc(lib.SDL_OpenGamepad(instance_id), lib.SDL_CloseGamepad))) @classmethod def _from_instance_id(cls, instance_id: int) -> GameController: @@ -347,17 +347,27 @@ def init() -> None: tcod.sdl.sys.init(controller_systems) -def _get_number() -> int: - """Return the number of attached joysticks.""" +def _get_instance_ids() -> list[int]: + """Return the instance IDs of all attached joysticks. + + SDL3's ``SDL_GetJoysticks`` returns an array of instance IDs, which is what + ``SDL_OpenJoystick``/``SDL_OpenGamepad``/``SDL_IsGamepad`` expect. These are not + contiguous device indices, so they must not be replaced with ``range``. + """ init() count = ffi.new("int*") - lib.SDL_GetJoysticks(count) - return int(count[0]) + joysticks_p = lib.SDL_GetJoysticks(count) # SDL-owned SDL_JoystickID array. + if not joysticks_p: + return [] + try: + return [int(joysticks_p[i]) for i in range(int(count[0]))] + finally: + lib.SDL_free(joysticks_p) def get_joysticks() -> list[Joystick]: """Return a list of all connected joystick devices.""" - return [Joystick._open(i) for i in range(_get_number())] + return [Joystick._open(instance_id) for instance_id in _get_instance_ids()] def get_controllers() -> list[GameController]: @@ -365,7 +375,9 @@ def get_controllers() -> list[GameController]: This ignores joysticks without a game controller mapping. """ - return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGamepad(i)] + return [ + GameController._open(instance_id) for instance_id in _get_instance_ids() if lib.SDL_IsGamepad(instance_id) + ] def _get_all() -> list[Joystick | GameController]: @@ -374,7 +386,10 @@ def _get_all() -> list[Joystick | GameController]: If the joystick has a controller mapping then it is returned as a :any:`GameController`. Otherwise it is returned as a :any:`Joystick`. """ - return [GameController._open(i) if lib.SDL_IsGamepad(i) else Joystick._open(i) for i in range(_get_number())] + return [ + GameController._open(instance_id) if lib.SDL_IsGamepad(instance_id) else Joystick._open(instance_id) + for instance_id in _get_instance_ids() + ] def joystick_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001 From d54bebf79060e0248ea29fd61ef0c0675336facf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:52:04 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcod/sdl/joystick.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 5965517d..e6d5d603 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -375,9 +375,7 @@ def get_controllers() -> list[GameController]: This ignores joysticks without a game controller mapping. """ - return [ - GameController._open(instance_id) for instance_id in _get_instance_ids() if lib.SDL_IsGamepad(instance_id) - ] + return [GameController._open(instance_id) for instance_id in _get_instance_ids() if lib.SDL_IsGamepad(instance_id)] def _get_all() -> list[Joystick | GameController]: From 6b8d26df7a2f82f27bd9f5943da2c36a7ea0503b Mon Sep 17 00:00:00 2001 From: pinghedm <8351465+pinghedm@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:33:48 -0400 Subject: [PATCH 3/4] Update tcod/sdl/joystick.py Co-authored-by: Kyle Benesch <4b796c65+github@gmail.com> --- tcod/sdl/joystick.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index e6d5d603..0b8c8c63 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -356,13 +356,8 @@ def _get_instance_ids() -> list[int]: """ init() count = ffi.new("int*") - joysticks_p = lib.SDL_GetJoysticks(count) # SDL-owned SDL_JoystickID array. - if not joysticks_p: - return [] - try: - return [int(joysticks_p[i]) for i in range(int(count[0]))] - finally: - lib.SDL_free(joysticks_p) + joysticks_p = _check_p(ffi.gc(lib.SDL_GetJoysticks(count), lib.SDL_free)) # SDL_JoystickID array + return [int(i) for i in joysticks_p[0:count[0]]] def get_joysticks() -> list[Joystick]: From 6c24410169437a29ecca7355229c5458ec77beee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:34:17 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcod/sdl/joystick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 0b8c8c63..3466756a 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -357,7 +357,7 @@ def _get_instance_ids() -> list[int]: init() count = ffi.new("int*") joysticks_p = _check_p(ffi.gc(lib.SDL_GetJoysticks(count), lib.SDL_free)) # SDL_JoystickID array - return [int(i) for i in joysticks_p[0:count[0]]] + return [int(i) for i in joysticks_p[0 : count[0]]] def get_joysticks() -> list[Joystick]: