Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions tcod/sdl/joystick.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -347,25 +347,35 @@ 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)
Comment thread
pinghedm marked this conversation as resolved.
Outdated


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]:
"""Return a list of all connected game controllers.

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]:
Expand All @@ -374,7 +384,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
Expand Down
Loading