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
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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}
|
||||
|
||||
|
||||
# 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": []}
|
||||
Reference in New Issue
Block a user