"""SDL Joystick Support.
.. versionadded:: 13.8
"""
from __future__ import annotations
import enum
from typing import Any, ClassVar, Final, Literal
from weakref import WeakValueDictionary
import tcod.sdl.sys
from tcod.cffi import ffi, lib
from tcod.sdl._internal import _check, _check_int, _check_p
_HAT_DIRECTIONS: dict[int, tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = {
int(lib.SDL_HAT_CENTERED): (0, 0),
int(lib.SDL_HAT_UP): (0, -1),
int(lib.SDL_HAT_RIGHT): (1, 0),
int(lib.SDL_HAT_DOWN): (0, 1),
int(lib.SDL_HAT_LEFT): (-1, 0),
int(lib.SDL_HAT_RIGHTUP): (1, -1),
int(lib.SDL_HAT_RIGHTDOWN): (1, 1),
int(lib.SDL_HAT_LEFTUP): (-1, -1),
int(lib.SDL_HAT_LEFTDOWN): (-1, 1),
}
[docs]
class ControllerAxis(enum.IntEnum):
"""The standard axes for a game controller."""
INVALID = int(lib.SDL_GAMEPAD_AXIS_INVALID)
LEFTX = int(lib.SDL_GAMEPAD_AXIS_LEFTX)
""""""
LEFTY = int(lib.SDL_GAMEPAD_AXIS_LEFTY)
""""""
RIGHTX = int(lib.SDL_GAMEPAD_AXIS_RIGHTX)
""""""
RIGHTY = int(lib.SDL_GAMEPAD_AXIS_RIGHTY)
""""""
TRIGGERLEFT = int(lib.SDL_GAMEPAD_AXIS_LEFT_TRIGGER)
""""""
TRIGGERRIGHT = int(lib.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)
""""""
[docs]
class Joystick:
"""A low-level SDL joystick.
.. seealso::
https://wiki.libsdl.org/CategoryJoystick
"""
_by_instance_id: ClassVar[WeakValueDictionary[int, Joystick]] = WeakValueDictionary()
"""Currently opened joysticks."""
def __init__(self, sdl_joystick_p: Any) -> None: # noqa: ANN401
"""Wrap an SDL joystick C pointer."""
self.sdl_joystick_p: Final = sdl_joystick_p
"""The CFFI pointer to an SDL_Joystick struct."""
self.axes: Final[int] = _check_int(lib.SDL_GetNumJoystickAxes(self.sdl_joystick_p), failure=-1)
"""The total number of axes."""
self.balls: Final[int] = _check_int(lib.SDL_GetNumJoystickBalls(self.sdl_joystick_p), failure=-1)
"""The total number of trackballs."""
self.buttons: Final[int] = _check_int(lib.SDL_GetNumJoystickButtons(self.sdl_joystick_p), failure=-1)
"""The total number of buttons."""
self.hats: Final[int] = _check_int(lib.SDL_GetNumJoystickHats(self.sdl_joystick_p), failure=-1)
"""The total number of hats."""
self.name: Final[str] = str(ffi.string(lib.SDL_GetJoystickName(self.sdl_joystick_p)), encoding="utf-8")
"""The name of this joystick."""
self.guid: Final[str] = self._get_guid()
"""The GUID of this joystick."""
self.id: Final[int] = _check(lib.SDL_GetJoystickID(self.sdl_joystick_p))
"""The instance ID of this joystick. This is not the same as the device ID."""
self._keep_alive: Any = None
"""The owner of this objects memory if this object does not own itself."""
self._by_instance_id[self.id] = self
@classmethod
def _open(cls, instance_id: int) -> Joystick:
tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK)
p = _check_p(ffi.gc(lib.SDL_OpenJoystick(instance_id), lib.SDL_CloseJoystick))
return cls(p)
@classmethod
def _from_instance_id(cls, instance_id: int) -> Joystick:
return cls._by_instance_id[instance_id]
[docs]
def __eq__(self, other: object) -> bool:
"""Return True if `self` and `other` refer to the same joystick."""
if isinstance(other, Joystick):
return self.id == other.id
return NotImplemented
[docs]
def __hash__(self) -> int:
"""Return the joystick id as a hash."""
return hash(self.id)
def _get_guid(self) -> str:
guid_str = ffi.new("char[33]")
lib.SDL_GUIDToString(lib.SDL_GetJoystickGUID(self.sdl_joystick_p), guid_str, len(guid_str))
return str(ffi.string(guid_str), encoding="ascii")
[docs]
def get_axis(self, axis: int) -> int:
"""Return the raw value of `axis` in the range -32768 to 32767."""
return int(lib.SDL_GetJoystickAxis(self.sdl_joystick_p, axis))
[docs]
def get_ball(self, ball: int) -> tuple[int, int]:
"""Return the values (delta_x, delta_y) of `ball` since the last poll."""
xy = ffi.new("int[2]")
_check(lib.SDL_GetJoystickBall(self.sdl_joystick_p, ball, xy, xy + 1))
return int(xy[0]), int(xy[1])
[docs]
def get_hat(self, hat: int) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]:
"""Return the direction of `hat` as (x, y). With (-1, -1) being in the upper-left."""
return _HAT_DIRECTIONS[lib.SDL_GetJoystickHat(self.sdl_joystick_p, hat)]
[docs]
class GameController:
"""A standard interface for an Xbox 360 style game controller."""
_by_instance_id: ClassVar[WeakValueDictionary[int, GameController]] = WeakValueDictionary()
"""Currently opened controllers."""
def __init__(self, sdl_controller_p: Any) -> None: # noqa: ANN401
"""Wrap an SDL controller C pointer."""
self.sdl_controller_p: Final = sdl_controller_p
self.joystick: Final = Joystick(lib.SDL_GetGamepadJoystick(self.sdl_controller_p))
"""The :any:`Joystick` associated with this controller."""
self.joystick._keep_alive = self.sdl_controller_p # This objects real owner needs to be kept alive.
self._by_instance_id[self.joystick.id] = self
@classmethod
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:
return cls._by_instance_id[instance_id]
[docs]
def get_axis(self, axis: ControllerAxis) -> int:
"""Return the state of the given `axis`.
The state is usually a value from -32768 to 32767, with positive values towards the lower-right direction.
Triggers have the range of 0 to 32767 instead.
"""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, axis))
[docs]
def __eq__(self, other: object) -> bool:
"""Return True if `self` and `other` are both controllers referring to the same joystick."""
if isinstance(other, GameController):
return self.joystick.id == other.joystick.id
return NotImplemented
[docs]
def __hash__(self) -> int:
"""Return the joystick id as a hash."""
return hash(self.joystick.id)
# These could exist as convenience functions, but the get_X functions are probably better.
@property
def _left_x(self) -> int:
"""Return the position of this axis (-32768 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_LEFTX))
@property
def _left_y(self) -> int:
"""Return the position of this axis (-32768 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_LEFTY))
@property
def _right_x(self) -> int:
"""Return the position of this axis (-32768 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_RIGHTX))
@property
def _right_y(self) -> int:
"""Return the position of this axis (-32768 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_RIGHTY))
@property
def _trigger_left(self) -> int:
"""Return the position of this trigger (0 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_LEFT_TRIGGER))
@property
def _trigger_right(self) -> int:
"""Return the position of this trigger (0 to 32767)."""
return int(lib.SDL_GetGamepadAxis(self.sdl_controller_p, lib.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
@property
def _a(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_SOUTH))
@property
def _b(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_EAST))
@property
def _x(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_WEST))
@property
def _y(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_NORTH))
@property
def _back(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_BACK))
@property
def _guide(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_GUIDE))
@property
def _start(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_START))
@property
def _left_stick(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_LEFT_STICK))
@property
def _right_stick(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_RIGHT_STICK))
@property
def _left_shoulder(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER))
@property
def _right_shoulder(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER))
@property
def _dpad(self) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]:
return (
lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_DPAD_RIGHT)
- lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_DPAD_LEFT),
lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_DPAD_DOWN)
- lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_DPAD_UP),
) # type: ignore[return-value] # Boolean math has predictable values
@property
def _misc1(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_MISC1))
@property
def _paddle1(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1))
@property
def _paddle2(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1))
@property
def _paddle3(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2))
@property
def _paddle4(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2))
@property
def _touchpad(self) -> bool:
"""Return True if this button is held."""
return bool(lib.SDL_GetGamepadButton(self.sdl_controller_p, lib.SDL_GAMEPAD_BUTTON_TOUCHPAD))
[docs]
def init() -> None:
"""Initialize SDL's joystick and game controller subsystems."""
controller_systems = tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER
if tcod.sdl.sys.Subsystem(lib.SDL_WasInit(controller_systems)) == controller_systems:
return # Already initialized
tcod.sdl.sys.init(controller_systems)
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*")
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]]]
[docs]
def get_joysticks() -> list[Joystick]:
"""Return a list of all connected joystick devices."""
return [Joystick._open(instance_id) for instance_id in _get_instance_ids()]
[docs]
def get_controllers() -> list[GameController]:
"""Return a list of all connected game controllers.
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)]
def _get_all() -> list[Joystick | GameController]:
"""Return a list of all connected joystick or controller devices.
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(instance_id) if lib.SDL_IsGamepad(instance_id) else Joystick._open(instance_id)
for instance_id in _get_instance_ids()
]
[docs]
def joystick_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001
"""Check or set joystick event polling.
.. seealso::
https://wiki.libsdl.org/SDL3/SDL_SetJoystickEventsEnabled
"""
if new_state is True:
lib.SDL_SetJoystickEventsEnabled(True) # noqa: FBT003
elif new_state is False:
lib.SDL_SetJoystickEventsEnabled(False) # noqa: FBT003
return lib.SDL_JoystickEventsEnabled()
[docs]
def controller_event_state(new_state: bool | None = None) -> bool: # noqa: FBT001
"""Check or set game controller event polling.
.. seealso::
https://wiki.libsdl.org/SDL3/SDL_SetGamepadEventsEnabled
"""
if new_state is True:
lib.SDL_SetGamepadEventsEnabled(True) # noqa: FBT003
elif new_state is False:
lib.SDL_SetGamepadEventsEnabled(False) # noqa: FBT003
return lib.SDL_GamepadEventsEnabled()