# --------------------------------------------------------------------------- # 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, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from fut_account import ACCOUNT # single source of truth for identity # Back-compat snapshot; prefer ACCOUNT.persona_id in new code. PERSONA_ID = ACCOUNT.persona_id 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 is INERT in FIFA 17: it is not in the atom table at all, so # the client's key hash never matches and it routes straight to the value-SKIP # handler 0x180135ff0 -- same class as the itemDbVersion/checkServerDbVersion # keys proven phantom in blaze_responder. Kept (harmless, and other FIFA # versions do use it) but it is NOT read here; the live key is resourceId. "definitionId": rid, "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": ACCOUNT.persona_id, # must equal logged-in persona (0x18014659c) "squadName": ACCOUNT.squad_name, "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 "" 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", ACCOUNT.squad_name), "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": []}