Files
OpenFUT/fifa17-recon/tools/vgamepad.py
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).

Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
  0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
  GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
  reads identity/rating/face from a resolved record at item+0x10, filled
  by a lookup (0x18011cca0) in the FUT item-definition std::map at
  CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
  (JSON fields routed to the skip handler). Owned items don't auto-trigger
  a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
  ready; the fetch trigger lives in the packed FIFA17.exe.

New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
2026-08-01 20:24:30 -07: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:])