Skip to content

Instantly share code, notes, and snippets.

@artizirk
Last active March 22, 2024 07:34
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save artizirk/b407ba86feb7f0227654f8f5f1541413 to your computer and use it in GitHub Desktop.
Save artizirk/b407ba86feb7f0227654f8f5f1541413 to your computer and use it in GitHub Desktop.
Pure Python implementation for reading Xbox controller inputs without extra libs
#!/usr/bin/env python3
""" XInput Game Controller APIs
Pure Python implementation for reading Xbox controller inputs without extra libs
Copyright (C) 2020 by Arti Zirk <arti.zirk@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
"""
from ctypes import WinDLL, WinError, Structure, POINTER, byref, c_ubyte
from ctypes.util import find_library
from ctypes.wintypes import DWORD, WORD, SHORT
# for some reason wintypes.BYTE is defined as signed c_byte and as c_ubyte
BYTE = c_ubyte
# Max number of controllers supported
XUSER_MAX_COUNT = 4
class XINPUT_BUTTONS(Structure):
"""Bit-fields of XINPUT_GAMEPAD wButtons"""
_fields_ = [
("DPAD_UP", WORD, 1),
("DPAD_DOWN", WORD, 1),
("DPAD_LEFT", WORD, 1),
("DPAD_RIGHT", WORD, 1),
("START", WORD, 1),
("BACK", WORD, 1),
("LEFT_THUMB", WORD, 1),
("RIGHT_THUMB", WORD, 1),
("LEFT_SHOULDER", WORD, 1),
("RIGHT_SHOULDER", WORD, 1),
("_reserved_1_", WORD, 1),
("_reserved_1_", WORD, 1),
("A", WORD, 1),
("B", WORD, 1),
("X", WORD, 1),
("Y", WORD, 1)
]
def __repr__(self):
r = []
for name, type, size in self._fields_:
if "reserved" in name:
continue
r.append("{}={}".format(name, getattr(self, name)))
args = ', '.join(r)
return f"XINPUT_GAMEPAD({args})"
class XINPUT_GAMEPAD(Structure):
"""Describes the current state of the Xbox 360 Controller.
https://docs.microsoft.com/en-us/windows/win32/api/xinput/ns-xinput-xinput_gamepad
wButtons is a bitfield describing currently pressed buttons
"""
_fields_ = [
("wButtons", XINPUT_BUTTONS),
("bLeftTrigger", BYTE),
("bRightTrigger", BYTE),
("sThumbLX", SHORT),
("sThumbLY", SHORT),
("sThumbRX", SHORT),
("sThumbRY", SHORT),
]
def __repr__(self):
r = []
for name, type in self._fields_:
r.append("{}={}".format(name, getattr(self, name)))
args = ', '.join(r)
return f"XINPUT_GAMEPAD({args})"
class XINPUT_STATE(Structure):
"""Represents the state of a controller.
https://docs.microsoft.com/en-us/windows/win32/api/xinput/ns-xinput-xinput_state
dwPacketNumber: State packet number. The packet number indicates whether
there have been any changes in the state of the controller. If the
dwPacketNumber member is the same in sequentially returned XINPUT_STATE
structures, the controller state has not changed.
"""
_fields_ = [
("dwPacketNumber", DWORD),
("Gamepad", XINPUT_GAMEPAD)
]
def __repr__(self):
return f"XINPUT_STATE(dwPacketNumber={self.dwPacketNumber}, Gamepad={self.Gamepad})"
class XInput:
"""Minimal XInput API wrapper"""
def __init__(self):
# https://docs.microsoft.com/en-us/windows/win32/xinput/xinput-versions
# XInput 1.4 is available only on Windows 8+.
# Older Windows versions are End Of Life anyway.
lib_name = "XInput1_4.dll"
lib_path = find_library(lib_name)
if not lib_path:
raise Exception(f"Couldn't find {lib_name}")
self._XInput_ = WinDLL(lib_path)
self._XInput_.XInputGetState.argtypes = [DWORD, POINTER(XINPUT_STATE)]
self._XInput_.XInputGetState.restype = DWORD
def GetState(self, dwUserIndex):
state = XINPUT_STATE()
ret = self._XInput_.XInputGetState(dwUserIndex, byref(state))
if ret:
raise WinError(ret)
return state.dwPacketNumber, state.Gamepad
if __name__ == "__main__":
xi = XInput()
from time import sleep
for x in range(XUSER_MAX_COUNT):
try:
print(f"Reading input from controller {x}")
print(xi.GetState(x))
except Exception as e:
print(f"Controller {x} not available: {e}")
print("Reading all inputs from gamepad 0")
while True:
print(xi.GetState(0), end=" \r")
sleep(0.016)
@Intedai
Copy link

Intedai commented Jul 9, 2023

Thanks, I've learned a lot from this.

@artizirk
Copy link
Author

artizirk commented Jul 9, 2023

fyi, if anyone wants to also support non standard controllers then you also have to implement DirectInput. Xbox Controller does support DI but the trigger buttons dont work so you are forced to use Xinput.

@blakie1225
Copy link

Hey I have made a fork of this code that adds in guide button detection but I wasn't able to merge it cleanly with your code with my limited knowledge. I am not sure how to merge the code so that only the GetState function is needed because of the extra struct that i added. Any help would be appreciated. Also here is a link to the forum post that i found about the guide button detection https://forums.tigsource.com/index.php?topic=26792.0

@artizirk
Copy link
Author

Hi @blakie1225. That's a pretty cool hack to read the guide button. Looks like the Guide button is 11th bit of the wButtons. (0x0400 from hex to binary shows that). In the XINPUT_BUTTONS(Structure) 11th field (after RIGHT_SHOULDER) is currently named as _reserved_1_, rename that to GUIDE. After that you can use XINPUT_BUTTONS inside the XINPUT_GAMEPAD_SPECIAL structure similarly as is done with the normal gamepad structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment