From 16fdccf4243c32de2af4ced4b140d17eb7310a84 Mon Sep 17 00:00:00 2001 From: pinghedm <8351465+pinghedm@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:45:53 -0400 Subject: [PATCH 1/4] Fix SDL3 joystick enumeration using device indices as instance IDs (#182) * 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 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kyle Benesch <4b796c65+github@gmail.com> --- CHANGELOG.md | 4 ++++ tcod/sdl/joystick.py | 30 +++++++++++++++++++----------- 2 files changed, 23 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..3466756a 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,22 @@ 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 = _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]: """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 +370,7 @@ 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 +379,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 f180ef42874205053ade20f2ffd20e92bb214a9b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 3 Jun 2026 18:53:33 -0700 Subject: [PATCH 2/4] Prepare 21.2.1 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f9c702..9b128a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [21.2.1] - 2026-06-04 + ### Deployment - PyPy wheels switched from PyPy 3.10 to PyPy 3.11. From 612ea9ff87d8475c5cdab18dd1703b46afdbd1d1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 3 Jun 2026 20:49:02 -0700 Subject: [PATCH 3/4] Remove liskin/gh-problem-matcher-wrap action --- .github/workflows/python-package.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8407fc37..ab36e812 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -47,10 +47,7 @@ jobs: - name: Install typing dependencies run: pip install mypy pytest -r requirements.txt - name: Mypy - uses: liskin/gh-problem-matcher-wrap@v3 - with: - linters: mypy - run: mypy --show-column-numbers + run: mypy sdist: runs-on: ubuntu-latest From 00dcb1d7937229dcdc563d344ce8ed51ecabe669 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 3 Jun 2026 20:49:42 -0700 Subject: [PATCH 4/4] Add missing token for GitHub CLI --- .github/workflows/release-on-tag.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index d2e52c2f..b12ade30 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -26,3 +26,5 @@ jobs: id: create_release # https://cli.github.com/manual/gh_release_create run: gh release create "${GITHUB_REF_NAME}" --verify-tag --notes-file release_body.md + env: + GH_TOKEN: ${{ github.token }}