59934b4ef0
The client now issues PUT /squad and the hub renders coins, record and the
squad roster. Three separate root causes, all verified live.
Squad blocker (the long-standing "client never sends PUT /squad"):
AddPlayerToSquad, GetSquads and SelectSquadById issue ZERO network requests
(pure local model reads/mutations, FutSquadServiceImpl vtable 0x180233ff0);
only SaveCurrentSquad writes, and it is unguarded. The client simply needed a
populated ACTIVE squad model, which arrives via the massinfo `squad` member.
No response of ours was ever being rejected.
userMassInfo is NOT required to be {}:
0x180174630 is a FLAT {userInfo, squad, settings, userData} body -- the old
"wrapper key is user" note was wrong, and the historical freeze was the
malformed squad member, not the envelope.
clubNameChangeAllowed must be false:
sending true advertises a club-rename flow whose UI model is never populated;
the client shows a naming prompt and dies confirming it (ACCESS_VIOLATION
reading 0x0 at FIFA17.exe+0x71b8651, 4/4 runs, no CardsDLL frame and no request
in flight). Isolated by a single-variable run; guarded by a contract check.
Endpoint/schema corrections found in live traffic, invisible to static analysis:
* GET ut/%s/squad/list is a real endpoint and must return {"squad":[...]},
not the active-squad object (the /list suffix is appended by the caller, so
it never appeared in the request table)
* PUT lands on ut/%s/squad/<id>, not a bare ut/%s/squad
* userInfo currencies are read as name/funds/finalFunds/active -- there is no
"value" key, so coins always rendered 0
* squad-list elements take STRING formation/squadType, not ints
* the CardsDLL script-API thunk<->name table was off by one (AddPlayerToSquad
is 0x18004aa70; 0x18004aff0 is GetPotentialChemistry_Club)
FUT_MASSINFO / FUT_USERINFO ladders keep every step of the bisect reproducible.
Contract suite 311 -> 358 checks. Tooling added: PyGhidra harness (Ghidra's
Java/OSGi script path is broken on this box), minidump reader, live code grabber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Diff the atoms the userInfo deserializer (0x18013ec10) can consume against the
|
|
keys utas_server.user_info() actually sends, and flag STRING-typed fields we omit
|
|
(a NULL string pointer is exactly the crash class seen in FUN_180084f90)."""
|
|
import re, sys, os
|
|
|
|
sys.path.insert(0, "/home/alex/Documents/OpenFUT/fifa17-recon/tools")
|
|
os.environ.setdefault("FUT_PROFILE", "/tmp/claude-1000/-home-alex-Documents-OpenFUT/"
|
|
"4cf26d25-8cee-4db3-ad9e-9fd1838020eb/scratchpad/diffprof.json")
|
|
|
|
atoms = {}
|
|
for line in open("/home/alex/Documents/OpenFUT/fifa17-recon/docs/fut_atoms.tsv"):
|
|
p = line.rstrip("\n").split("\t")
|
|
if len(p) >= 3:
|
|
atoms[int(p[0])] = p[2]
|
|
|
|
src = open("/tmp/ghidra_fut/userinfo.txt").read()
|
|
|
|
# Every comparison against the key-id variable, plus switch cases.
|
|
ids = set()
|
|
for m in re.finditer(r"iVar\d+ == (0x[0-9a-f]+|\d+)", src):
|
|
ids.add(int(m.group(1), 0))
|
|
for m in re.finditer(r"case (0x[0-9a-f]+|\d+):", src):
|
|
ids.add(int(m.group(1), 0))
|
|
for m in re.finditer(r"caseD_([0-9a-f]+)", src):
|
|
ids.add(int(m.group(1), 16))
|
|
|
|
# getter used per id -> type. 0x1801c7aa0 = STRING, 0x1801c79d0 = int,
|
|
# 0x1801c7a40/0x1801c7b40 = bool/other scalars.
|
|
GET = {"1801c7aa0": "str", "1801c79d0": "int"}
|
|
typed = {}
|
|
for m in re.finditer(r"(?:iVar\d+ == |case )(0x[0-9a-f]+|\d+)\)?:?\s*\{?\s*\n((?:.*\n){0,6})", src):
|
|
try:
|
|
i = int(m.group(1), 0)
|
|
except ValueError:
|
|
continue
|
|
blk = m.group(2)
|
|
for g, t in GET.items():
|
|
if g in blk:
|
|
typed[i] = t
|
|
break
|
|
|
|
from utas_server import user_info # noqa: E402
|
|
sent = set(user_info().keys())
|
|
|
|
known = {i: atoms.get(i, "?") for i in sorted(ids) if i in atoms}
|
|
print("userInfo deser consumes %d named atoms; user_info() sends %d keys\n"
|
|
% (len(known), len(sent)))
|
|
|
|
missing = [(i, n, typed.get(i, "")) for i, n in known.items() if n not in sent]
|
|
extra = sorted(sent - set(known.values()))
|
|
|
|
print("=== parsed by the client but NOT sent by us (%d) ===" % len(missing))
|
|
for i, n, t in sorted(missing, key=lambda x: (x[2] != "str", x[1])):
|
|
print(" %-32s atom %#-6x %s" % (n, i, ("<-- STRING" if t == "str" else t)))
|
|
|
|
print("\n=== we send but the deser does not name (harmless SKIPs) ===")
|
|
print(" " + ", ".join(extra))
|