diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 85c53a6..ea2c5d6 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -939,13 +939,110 @@ ROUTES = [ # staff,year} on the MY CLUB screen. They are suffix endpoints the request table # never lists -- the same trap as /squad/list. They were being swallowed by the # generic /club route, which answers with the FULL 28-item club list where the - # client asked for STATS: wrong shape, and re-sent on every poll. No schema is - # documented for them, so serve the proven-safe {} and let a capture refine it. - (re.compile(G + r"/club/stats"), lambda m, h: (200, {})), + # client asked for STATS: wrong shape, and re-sent on every poll. + (re.compile(G + r"/club/stats"), lambda m, h: club_stats_route(h)), (re.compile(G + r"/club"), lambda m, h: club_route(h)), ] +# ---- club stats: the CLUB STATS panel, and probably the MY CLUB tile too ------ +# GET ut/%s/club/stats/ -> FutStickerBookStats2ServerResponse, deser +# 0x180130150 (7,870 chars, read end to end). Wire schema, fully verified: +# +# {"stat":[{"contextId":int, "contextValue":int, "type":str, "typeValue":int}]} +# contextId(0xb6) INT 0x1801c79d0 +# contextValue(0xb7) INT 0x1801c79d0 +# type(0x354) STRING 0x1801c7aa0 -> copied into a 0x30-byte buffer +# typeValue(0x355) INT 0x1801c79d0 +# Unknown keys route to FUN_180135ff0 at BOTH nesting levels, so extras are inert. +# +# FIVE THINGS THAT DECIDE WHETHER THIS WORKS, all learned the hard way: +# +# 1. EVERY RESPONSE WIPES THE WHOLE MAP FIRST. Nothing accumulates. So a good body on +# one mode followed by a thin body on another ERASES the first, and the ordering of +# the client's requests would decide what survives. The fix is to serve the SAME +# COMPLETE SET for every Stats2 mode: then whichever request lands last leaves the +# map correct and ordering stops mattering. (One investigator reported this factory +# does NOT wipe; a reviewer re-read it and refuted that. The wipe is real.) +# 2. /club/stats/staff IS A DIFFERENT CLASS. It is FutStaffBonus, shape +# {"bonus":[{"type":str,"value":int}]}, NOT Stats2. Sending a {"stat":[...]} body +# there is harmless but does nothing. Its type strings are not decoded, so it keeps +# {} -- which is safe, because that parser's top-level loop exits immediately on +# END_OBJECT. It also means staff does NOT wipe the Stats2 map. +# 3. ELEMENT-LOCAL VARIABLES ARE NOT RESET BETWEEN ELEMENTS. The clears happen once +# before the array loop, not inside it, so omitting a key in element N silently +# inherits element N-1's value. ALWAYS EMIT ALL FOUR KEYS IN EVERY ELEMENT. +# 4. The storage key is contextValue ALONE. contextId is only a guard: contextId == 1 +# or 5 <= contextId <= 9 forces contextValue to 0, which is the global bucket the +# +0x800 getter reads. We use contextId 1 throughout to land everything there. +# 5. Three ids the panel READS can never be SET from here: 0x3d CONTRACTS, +# 0x3e TRAINING, 0x40 FITNESS. No type string produces them. +# +# type string -> internal id -> the on-screen row it moves (FUN_18012fd40 -> the club +# stats provider FUN_180043b90): +# players 1 PLAYERS rarePlayers 5 staff 0xa STAFF_EMPLOYED +# stadia 0x14 STADIA_OWNED balls 0x1e BALLS_EARNED kits 0x28 KITS_AVAILABLE +# badges 0x2d BADGES trophies 0x32 TROPHIES_WON +# +# WHY THIS IS NOW ALSO THE HUB-TILE CANDIDATE. The investigation concluded the MY CLUB +# tile does not read this store, but flagged that negative as BOUNDED: the interface +# comes through a QueryInterface adapter, so the vtable is assembled at runtime and +# cannot be read statically. Live evidence on 2026-08-04 points the other way. The hub +# tile reads "0 TOTAL PLAYERS" and the CLUB STATS panel reads "Players 0", the same +# quantity, both zero, while we answer {}. And FUT_CLUB_PAGE ruled out the alternative: +# we served 114 items to /club and the tile still said 0, so it is not a count of the +# list. Strong inference, not proof. This flag is the test. +# +# The test is unusually clean because the club holds 114 items and ALL of them are +# players: every other row is an honest zero. So if this works, exactly two numbers +# move (Players and Rare Players, 0 -> 114) and nothing else changes. +CLUBSTATS = os.environ.get("FUT_CLUBSTATS") == "1" + + +def _club_stat_set(): + """The complete global stat set, computed from what the club actually holds.""" + items = STORE.items() + players = [i for i in items if i.get("itemType") == "player"] + rare = [i for i in players if i.get("rareflag")] + # Gold/silver/bronze is FIFA's rating convention (75+/65-74/below), NOT something + # read out of the binary. Marked as a convention because it is one; if a tile ever + # disagrees, this is the line to doubt. + gold = [i for i in players if (i.get("rating") or 0) >= 75] + silver = [i for i in players if 65 <= (i.get("rating") or 0) < 75] + bronze = [i for i in players if 0 < (i.get("rating") or 0) < 65] + counts = [ + ("players", len(players)), + ("playersGold", len(gold)), + ("playersSilver", len(silver)), + ("playersBronze", len(bronze)), + ("rarePlayers", len(rare)), + # Honest zeros: this club holds no non-player items of any kind. + ("staff", 0), + ("stadia", 0), + ("balls", 0), + ("kits", 0), + ("badges", 0), + ("trophies", 0), + ] + # All four keys in every element -- see note 3 above. + return [{"contextId": 1, "contextValue": 0, "type": t, "typeValue": int(v)} + for t, v in counts] + + +def club_stats_route(h): + mode = h.path.split("/club/stats/", 1)[-1].split("?")[0] if "/club/stats/" in h.path else "" + if not CLUBSTATS: + return 200, {} + if mode.startswith("staff"): + # FutStaffBonus, a different class with a different shape. {} is safe and + # deliberately does not disturb the Stats2 map. + return 200, {} + stats = _club_stat_set() + log(" CLUBSTATS: %s -> %d stat rows (players=%d)" + % (mode or "(none)", len(stats), stats[0]["typeValue"])) + return 200, {"stat": stats} + + # FUT_CLUB_PAGE -- an EXPERIMENT, not a fix, aimed at the MY CLUB hub counter. # # The counter's renderer is NOT in cardsdll.dll. There is no two-number formatter of