#!/usr/bin/env python3 """Virtual Xbox-360 gamepad over /dev/uinput (OpenFUT FIFA-17 recon). FIFA 17 runs under Proton and reads input via evdev/SDL (a real controller), NOT via X11 XTEST — so xdotool keystrokes never reach it. This creates a kernel-level virtual pad whose events are indistinguishable from hardware, so FIFA's native gamepad path picks them up. Prototype (pure ctypes, no deps) to PROVE the approach; port to a Rust driver once confirmed. ./vgamepad.py daemon create the pad + hold it open, read commands from the FIFO /tmp/vpad.fifo until killed ./vgamepad.py [...] send command(s) to the running daemon, e.g. ./vgamepad.py a (A / confirm) ./vgamepad.py b (B / back) ./vgamepad.py up down left right ./vgamepad.py lb rb start back guide NOTE: FIFA enumerates controllers at launch, so the daemon must be running BEFORE FIFA starts (or FIFA relaunched) for the pad to be seen. """ import os, sys, time, struct, fcntl FIFO = "/tmp/vpad.fifo" UINPUT = "/dev/uinput" # ---- ioctl numbers (x86_64) ------------------------------------------------ UI_SET_EVBIT = 0x40045564 UI_SET_KEYBIT = 0x40045565 UI_SET_ABSBIT = 0x40045567 UI_DEV_CREATE = 0x5501 UI_DEV_DESTROY = 0x5502 EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03 SYN_REPORT = 0 BUS_USB = 0x03 # Xbox-360 button codes BTN = { "a": 0x130, "b": 0x131, "x": 0x133, "y": 0x134, "lb": 0x136, "rb": 0x137, "back": 0x13a, "start": 0x13b, "guide": 0x13c, "l3": 0x13d, "r3": 0x13e, } ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ = 0, 1, 2, 3, 4, 5 ABS_HAT0X, ABS_HAT0Y = 0x10, 0x11 STICKS = [ABS_X, ABS_Y, ABS_RX, ABS_RY] # -32768..32767 TRIGGERS = [ABS_Z, ABS_RZ] # 0..255 HATS = [ABS_HAT0X, ABS_HAT0Y] # -1..1 # d-pad direction -> (hat axis, value) DPAD = { "up": (ABS_HAT0Y, -1), "down": (ABS_HAT0Y, 1), "left": (ABS_HAT0X, -1), "right": (ABS_HAT0X, 1), } def _ev(fd, etype, code, value): # struct input_event { timeval time(16); u16 type; u16 code; s32 value; } os.write(fd, struct.pack("llHHi", 0, 0, etype, code, value)) def _syn(fd): _ev(fd, EV_SYN, SYN_REPORT, 0) def create_device(): fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK) fcntl.ioctl(fd, UI_SET_EVBIT, EV_KEY) fcntl.ioctl(fd, UI_SET_EVBIT, EV_ABS) fcntl.ioctl(fd, UI_SET_EVBIT, EV_SYN) for code in BTN.values(): fcntl.ioctl(fd, UI_SET_KEYBIT, code) for ax in STICKS + TRIGGERS + HATS: fcntl.ioctl(fd, UI_SET_ABSBIT, ax) # legacy uinput_user_dev: name[80], input_id{bus,vendor,product,version}(u16*4), # ff_effects_max(u32), absmax/min/fuzz/flat[64] each s32 name = b"Microsoft X-Box 360 pad".ljust(80, b"\0") idv = struct.pack("HHHH", BUS_USB, 0x045e, 0x028e, 0x0114) ff = struct.pack("I", 0) absmax = [0] * 64; absmin = [0] * 64; absfuzz = [0] * 64; absflat = [0] * 64 for ax in STICKS: absmax[ax] = 32767; absmin[ax] = -32768; absflat[ax] = 128 for ax in TRIGGERS: absmax[ax] = 255; absmin[ax] = 0 for ax in HATS: absmax[ax] = 1; absmin[ax] = -1 payload = (name + idv + ff + struct.pack("64i", *absmax) + struct.pack("64i", *absmin) + struct.pack("64i", *absfuzz) + struct.pack("64i", *absflat)) os.write(fd, payload) fcntl.ioctl(fd, UI_DEV_CREATE) time.sleep(0.3) # let udev create /dev/input/eventN + jsN return fd def do(fd, cmd): cmd = cmd.strip().lower() if not cmd: return if cmd in BTN: _ev(fd, EV_KEY, BTN[cmd], 1); _syn(fd); time.sleep(0.08) _ev(fd, EV_KEY, BTN[cmd], 0); _syn(fd) elif cmd in DPAD: ax, val = DPAD[cmd] _ev(fd, EV_ABS, ax, val); _syn(fd); time.sleep(0.10) _ev(fd, EV_ABS, ax, 0); _syn(fd) elif cmd.startswith("hold_") and cmd[5:] in BTN: # hold_lb etc. (no auto-release) _ev(fd, EV_KEY, BTN[cmd[5:]], 1); _syn(fd) elif cmd.startswith("rel_") and cmd[4:] in BTN: _ev(fd, EV_KEY, BTN[cmd[4:]], 0); _syn(fd) else: sys.stderr.write("unknown cmd: %s\n" % cmd) time.sleep(0.12) def daemon(): if os.path.exists(FIFO): os.unlink(FIFO) os.mkfifo(FIFO) fd = create_device() sys.stderr.write("[vgamepad] device created, listening on %s\n" % FIFO) sys.stderr.flush() try: while True: with open(FIFO, "r") as f: # blocks until a writer sends a line for line in f: for cmd in line.split(): do(fd, cmd) finally: try: fcntl.ioctl(fd, UI_DEV_DESTROY) except Exception: pass os.close(fd) if os.path.exists(FIFO): os.unlink(FIFO) def send(cmds): if not os.path.exists(FIFO): sys.stderr.write("!! daemon not running (no %s). Start: vgamepad.py daemon\n" % FIFO) sys.exit(2) with open(FIFO, "w") as f: f.write(" ".join(cmds) + "\n") print("sent: %s" % " ".join(cmds)) if __name__ == "__main__": if len(sys.argv) < 2: print(__doc__) sys.exit(1) if sys.argv[1] == "daemon": daemon() else: send(sys.argv[1:])