70a64e3709
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.
156 lines
5.2 KiB
Python
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:])
|