Skip to content

Commit 962785b

Browse files
committed
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
1 parent efb077c commit 962785b

2 files changed

Lines changed: 30 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
1111
- PyPy wheels switched from PyPy 3.10 to PyPy 3.11.
1212
- Experimental Pyodide wheels are now uploaded to PyPI.
1313

14+
### Fixed
15+
16+
- `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.
17+
1418
## [21.2.0] - 2026-04-04
1519

1620
### Added

tcod/sdl/joystick.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ def __init__(self, sdl_joystick_p: Any) -> None: # noqa: ANN401
126126
self._by_instance_id[self.id] = self
127127

128128
@classmethod
129-
def _open(cls, device_index: int) -> Joystick:
129+
def _open(cls, instance_id: int) -> Joystick:
130130
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK)
131-
p = _check_p(ffi.gc(lib.SDL_OpenJoystick(device_index), lib.SDL_CloseJoystick))
131+
p = _check_p(ffi.gc(lib.SDL_OpenJoystick(instance_id), lib.SDL_CloseJoystick))
132132
return cls(p)
133133

134134
@classmethod
@@ -184,8 +184,8 @@ def __init__(self, sdl_controller_p: Any) -> None: # noqa: ANN401
184184
self._by_instance_id[self.joystick.id] = self
185185

186186
@classmethod
187-
def _open(cls, joystick_index: int) -> GameController:
188-
return cls(_check_p(ffi.gc(lib.SDL_OpenGamepad(joystick_index), lib.SDL_CloseGamepad)))
187+
def _open(cls, instance_id: int) -> GameController:
188+
return cls(_check_p(ffi.gc(lib.SDL_OpenGamepad(instance_id), lib.SDL_CloseGamepad)))
189189

190190
@classmethod
191191
def _from_instance_id(cls, instance_id: int) -> GameController:
@@ -347,25 +347,37 @@ def init() -> None:
347347
tcod.sdl.sys.init(controller_systems)
348348

349349

350-
def _get_number() -> int:
351-
"""Return the number of attached joysticks."""
350+
def _get_instance_ids() -> list[int]:
351+
"""Return the instance IDs of all attached joysticks.
352+
353+
SDL3's ``SDL_GetJoysticks`` returns an array of instance IDs, which is what
354+
``SDL_OpenJoystick``/``SDL_OpenGamepad``/``SDL_IsGamepad`` expect. These are not
355+
contiguous device indices, so they must not be replaced with ``range``.
356+
"""
352357
init()
353358
count = ffi.new("int*")
354-
lib.SDL_GetJoysticks(count)
355-
return int(count[0])
359+
joysticks_p = lib.SDL_GetJoysticks(count) # SDL-owned SDL_JoystickID array.
360+
if not joysticks_p:
361+
return []
362+
try:
363+
return [int(joysticks_p[i]) for i in range(int(count[0]))]
364+
finally:
365+
lib.SDL_free(joysticks_p)
356366

357367

358368
def get_joysticks() -> list[Joystick]:
359369
"""Return a list of all connected joystick devices."""
360-
return [Joystick._open(i) for i in range(_get_number())]
370+
return [Joystick._open(instance_id) for instance_id in _get_instance_ids()]
361371

362372

363373
def get_controllers() -> list[GameController]:
364374
"""Return a list of all connected game controllers.
365375
366376
This ignores joysticks without a game controller mapping.
367377
"""
368-
return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGamepad(i)]
378+
return [
379+
GameController._open(instance_id) for instance_id in _get_instance_ids() if lib.SDL_IsGamepad(instance_id)
380+
]
369381

370382

371383
def _get_all() -> list[Joystick | GameController]:
@@ -374,7 +386,10 @@ def _get_all() -> list[Joystick | GameController]:
374386
If the joystick has a controller mapping then it is returned as a :any:`GameController`.
375387
Otherwise it is returned as a :any:`Joystick`.
376388
"""
377-
return [GameController._open(i) if lib.SDL_IsGamepad(i) else Joystick._open(i) for i in range(_get_number())]
389+
return [
390+
GameController._open(instance_id) if lib.SDL_IsGamepad(instance_id) else Joystick._open(instance_id)
391+
for instance_id in _get_instance_ids()
392+
]
378393

379394

380395
def joystick_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001

0 commit comments

Comments
 (0)