Files
OpenFUT/fifa17-recon/tools/fut_seed.py
T
funman300 59934b4ef0 fifa17-recon: FUT squad blocker solved + userInfo delivered
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
2026-08-03 20:47:16 -07:00

195 lines
8.4 KiB
Python

# ---------------------------------------------------------------------------
# OpenFUT / FIFA 17 UTAS -- forged squad ladder (clean-room). Imported by
# utas_server.py. Derived from CardsDLL_Win64_retail.dll (PE base 0x180000000).
#
# The prior full 11-player squad HUNG FIFA. Workflow wf_0bc80ab3 (5 agents,
# adversarially verified) proved: the deserializer PARSES our JSON fine; the
# freeze is POST-PARSE, at the per-item finalize resolve 0x180141176 (call
# singleton 0x18011a830 -> [r9+0xa08]) that fires for EVERY parsed item object
# (manager, players[].itemData, actives[]). Whether that resolve BLOCKS offline
# on an unresolvable item is UNVERIFIED -> we bisect it empirically with a ladder.
#
# The ladder (select via env FUT_SQUAD_STEP, default "s1"). Each step is one
# small change so a single FIFA relaunch isolates one variable:
# s1 zero-resolve: players are bare {index,kitNumber}, no itemData, manager=[]
# -> item deser NEVER entered, resolve fires 0 times. Tests envelope+HTTP
# framing only. Reaches hub => hang IS item-resolve. Freezes => framing.
# s1b players[0] gets an EMPTY itemData {id:0,dream:false} (still no real asset)
# -> resolve fires once on id 0. Freezes => id-0 resolve itself blocks
# offline. Reaches hub => id-0 is fine, the asset value is what matters.
# s2v0 players[0] = ONE real item, resourceId==assetId (version byte 0x00);
# club serves the same item. Renders => version 0 is correct.
# s2v1 same but version byte 0x01 (resourceId = 0x01<<24|assetId).
# s3v0 full XI with the winning version byte (default 0x00); club in lockstep.
# s3v1 full XI, version 0x01.
# resourceId decompose 0x180166ca0 CONFIRMED: assetId = resourceId & 0xffffff,
# high byte = version. Version byte value is the open question s2v0/s2v1 answer.
# ---------------------------------------------------------------------------
import os
PERSONA_ID = 33068179
ITEM_ID_BASE = 100000000
# Real FIFA17 assetIds read earlier from the live InGameDB. assetId 41 (Iniesta)
# was flagged by the verifier as possibly having NO InGameDB definition -> it is
# DROPPED from the XI until a live test confirms a replacement. (asset, rating, pos, kit)
REAL_XI = [
(20801, 94, "LW", 7), # Ronaldo -- STEP-2 uses this one
(158023, 93, "RW", 10), # Messi
(200389, 87, "GK", 1), # Oblak
(183907, 90, "CB", 5), # Boateng
(155862, 89, "CB", 4), # Ramos
(197445, 87, "LB", 2), # Alaba
(189332, 86, "LB", 3), # Alba
(182521, 88, "CM", 8), # Kroos
(183277, 88, "LM", 11), # Hazard
(176580, 92, "ST", 9), # Suarez
# (41, 88, "CM", ..) DROPPED: verifier says no InGameDB def -> stall risk
]
def player_item(asset, rating, pos, version=0x00, nation=38, team=243, league=53,
attrs=(90, 93, 82, 91, 33, 80)):
"""FULL item -- in case the card system needs more than the minimal set to
PLACE + render a real player (the minimal item rendered generic + rating 0).
Defaults are Ronaldo (Portugal 38 / Real Madrid 243 / La Liga 53)."""
rid = (version << 24) | asset
return {
"id": ITEM_ID_BASE + (asset & 0xffffff) + 1, # unique, != 0
"resourceId": rid,
"assetId": asset,
"cardassetid": asset,
"definitionId": rid, # some FUT APIs key on definitionId
"cardsubtypeid": 0, # 0..3 => PLAYER
"itemType": "player",
"rareflag": 1,
"rating": rating,
"preferredPosition": pos, # STRING enum "GK"/"CB"/...
"nation": nation,
"teamid": team,
"leagueId": league,
"playStyle": 250,
"attributeList": [{"index": i, "value": v} for i, v in enumerate(attrs)],
"itemState": "free",
"owners": 1,
"untradeable": True,
"contract": 7,
"fitness": 99,
"loans": 0,
"discardValue": 0,
"statsList": [],
"lifetimeStats": [],
}
def _base_squad():
"""Envelope shared by every step: valid formation/custom/kicktakers, but
players are bare {index,kitNumber} (index is the direct hash bucket key,
MUST be unique 0..22) and manager empty -> zero item-deser calls by default."""
return {
"id": 0,
"personaId": PERSONA_ID, # must equal logged-in persona (0x18014659c)
"squadName": "OpenFUT",
"formation": "f442",
"squadType": "REGULAR_SQUAD",
"chemistry": 100,
"starRating": 5,
"captain": 0,
"changed": 0,
"manager": [],
"actives": [],
"custom": "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
"50,50,0,50,40,65,0,65,50,50,1]",
"players": [{"index": i, "kitNumber": 0} for i in range(23)],
"kicktakers": [{"index": i, "id": 0, "dream": False} for i in range(5)],
}
def _put_item(squad, index, itemdata, kit=0):
squad["players"][index] = {"index": index, "itemData": itemdata, "kitNumber": kit}
def make_squad(step="s1"):
"""Return (squad, club) for a ladder step. club is {"itemData":[...]}."""
step = step.lower()
squad = _base_squad()
club_items = []
if step == "s0":
# ABSOLUTE minimal squad: no custom (only field re-parsed by a sub-reader,
# 0x1801c8270 -- prime suspect for the reader EOF-spin), empty players &
# kicktakers arrays. Reaches hub => envelope OK, spin is custom/players/
# kicktakers -> add back one at a time. Freezes => any squad object spins.
squad.pop("custom", None)
squad["players"] = []
squad["kicktakers"] = []
elif step == "s1":
pass # bare players, empty club
elif step == "s1b":
_put_item(squad, 0, {"id": 0, "dream": False}) # one empty item, no asset
elif step in ("s2v0", "s2v1"):
version = 0x00 if step.endswith("v0") else 0x01
asset, rating, pos, kit = REAL_XI[0] # Ronaldo
it = player_item(asset, rating, pos, version)
_put_item(squad, 0, it, kit)
squad["captain"] = it["id"]
club_items = [it]
elif step in ("s3v0", "s3v1"):
version = 0x00 if step.endswith("v0") else 0x01
for slot, (asset, rating, pos, kit) in enumerate(REAL_XI):
it = player_item(asset, rating, pos, version)
_put_item(squad, slot, it, kit)
club_items.append(it)
squad["captain"] = club_items[0]["id"]
else:
raise ValueError("unknown FUT_SQUAD_STEP=%r (s1|s1b|s2v0|s2v1|s3v0|s3v1)" % step)
return squad, {"itemData": club_items}
def squad_rating(squad):
"""Squad rating = mean of the rated players actually placed (0 when the squad
is empty, e.g. the s1 zero-resolve ladder step)."""
ratings = [(pl.get("itemData") or {}).get("rating", 0)
for pl in squad.get("players", [])]
ratings = [r for r in ratings if isinstance(r, int) and r > 0]
return sum(ratings) // len(ratings) if ratings else 0
def squad_summary(squad):
"""One FutSquadList element (deser 0x180141fc0, verified 2026-08-03).
Exactly six atoms are recognised; everything else is SKIP'd:
rating 0x274 int (scalar getter 0x1801c79d0)
chemistry 0x81 int (scalar getter 0x1801c79d0)
formation 0x12b STRING (string getter 0x1801c7aa0 -> enum conv 0x180166590)
id 0x15c int
squadName 0x2d3 STRING
squadType 0x2d6 STRING (string getter 0x1801c7aa0 -> enum conv 0x1801668e0)
NOTE: formation/squadType are STRINGS here, exactly as in the full-squad parser
0x18013d1f0 -- they go through the same 0x1801c7aa0 + converter pair. (The
rebuild plan's "<int>" for those two was wrong; feeding ints to a string getter
is the classic type-mismatch freeze at 0x1801c7f1a.)
"""
return {
"rating": squad_rating(squad),
"chemistry": int(squad.get("chemistry", 0)),
"formation": squad.get("formation", "f442"),
"id": int(squad.get("id", 0)),
"squadName": squad.get("squadName", "OpenFUT"),
"squadType": squad.get("squadType", "REGULAR_SQUAD"),
}
# Selected at import time from the environment (default s1 = the zero-resolve test).
STEP = os.environ.get("FUT_SQUAD_STEP", "s1")
SQUAD, CLUB = make_squad(STEP)
# Back-compat exports for utas_server.
USER_LIST = {"user": []}