Files
OpenFUT/fifa17-recon/docker/fifa17-python/tools/vgamepad.py
T
root 70a64e3709 fifa17-python: commit working FUT backend deployment (client/server split)
Freeze the running offline FUT backend into version control as
fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a
fresh checkout:

* OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders
  (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is
  required for remote mode (compose and entrypoint fail without it)
* docker-compose.yml reproducing the frozen baseline container exactly
  (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart)
* .env.example / .env for site config - the LAN IP is never hardcoded in source
* tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10,
  verified byte-identical to the running container at freeze time
* client_arm.sh (the 105 client-side arming counterpart)
* Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying
* docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record,
  restore instructions and rebuild-equivalence procedure

Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored.
The live container is untouched pending the .105 launcher audit.
2026-08-10 23:54:04 +00:00

156 lines
5.2 KiB
Python

#!/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 <cmd> [...] 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:])