From 9ee21afb56f43c66a9b8ad02ccacdb54f52d82a3 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 4 Aug 2026 14:33:01 -0700 Subject: [PATCH] fifa17-recon: club stats -- populate the PER-CONTEXT buckets, not just the global one LIVE 2026-08-04, and this corrects the body I shipped an hour ago. Selecting the ENGLAND tab on the MY CLUB screen issues exactly one request: 14:30:32 GET /ut/game/fifa17/club/stats/country/14 (14 = England) and NO item-list request. So that tab is driven entirely by per-nation stats, and it showed nothing while the club holds 8 England players. THE GUARD WAS THE BUG. In deser 0x180130150, contextId == 1 or 5 <= contextId <= 9 FORCES contextValue to 0, which is the global bucket that the +0x800 getter reads. The per-nation view reads the +0x7f8 getter keyed by the NATION ID instead. Every row I sent carried contextId 1, so no matter what contextValue said, everything landed in the global bucket and the per-context tabs could never see it. I had the guard written down in my own comment and still sent a body that tripped it on every row. Now, for country/, league/ and team/, the response carries the global rows AND per-context rows keyed by that id, computed from the club's real nation/leagueId/ teamid fields: country/14 -> players 8, playersGold 8, rarePlayers 8, silver/bronze/kits/badges 0 Both sets ride in the SAME response because every response wipes the whole map first, so anything left out is erased rather than merged. contextId 3 is used purely because it is OUTSIDE the guard and therefore preserves contextValue. TODO/CONFIRM what contextId means semantically; nothing read so far gives it a meaning beyond that guard. Also verified rather than assumed this round: all 11 type strings we emit resolve correctly against docs/fut_atoms.tsv (players 0x238, rarePlayers 0x272, stadia 0x2d7, balls 0x4f, kits 0x17c, badges 0x4b, trophies 0x340 ...), 0 mismatches. So the strings were never the failure. Does NOT claim to fix the MY CLUB hub counter, which remains the open question in S19. This fixes the nation/league tabs, which is a different and now-understood symptom. 392 checks green. Still behind FUT_CLUBSTATS, default off. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW --- fifa17-recon/tools/utas_server.py | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 915c842..3438cbd 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -1000,6 +1000,57 @@ ROUTES = [ CLUBSTATS = os.environ.get("FUT_CLUBSTATS") == "1" +def _counts_for(players): + """The per-collection counts, from a list of player items.""" + # Gold/silver/bronze is FIFA's rating convention (75+/65-74/under), NOT read out of + # the binary. If a tile ever disagrees, this is the line to doubt. + return [ + ("players", len(players)), + ("playersGold", len([i for i in players if (i.get("rating") or 0) >= 75])), + ("playersSilver", len([i for i in players if 65 <= (i.get("rating") or 0) < 75])), + ("playersBronze", len([i for i in players if 0 < (i.get("rating") or 0) < 65])), + ("rarePlayers", len([i for i in players if i.get("rareflag")])), + ] + + +def _club_stat_context(kind, ctx_id): + """Per-context rows for the nation / league / team tabs. + + LIVE 2026-08-04, and this is a correction to the first version of this route. + Selecting the ENGLAND tab on the MY CLUB screen issues exactly one request: + + GET /ut/game/fifa17/club/stats/country/14 (14 = England) + + and NO item-list request, so that tab is driven entirely by per-nation stats. The + tab showed nothing while the club holds 8 England players, because every row we + sent carried contextId 1. + + THE GUARD IS THE WHOLE POINT. In the deserializer, contextId == 1 or + 5 <= contextId <= 9 FORCES contextValue to 0, which is the global bucket the + +0x800 getter reads. The per-nation view reads the +0x7f8 getter keyed by the + nation id instead. So a body with contextId 1 can only ever populate the global + bucket, no matter what contextValue says, and the per-context tabs stay empty. + + contextId 3 is used here purely because it is OUTSIDE the guard and therefore + preserves contextValue. `TODO/CONFIRM` what contextId means semantically; nothing + read so far assigns it a meaning beyond that guard. + """ + items = STORE.items() + if kind == "country": + sel = [i for i in items if i.get("itemType") == "player" and i.get("nation") == ctx_id] + elif kind == "league": + sel = [i for i in items if i.get("itemType") == "player" and i.get("leagueId") == ctx_id] + else: + sel = [i for i in items if i.get("itemType") == "player" and i.get("teamid") == ctx_id] + rows = [{"contextId": 3, "contextValue": int(ctx_id), "type": t, "typeValue": int(v)} + for t, v in _counts_for(sel)] + # kits/badges are per-context too (case 3 reads KITS_AVAILABLE and BADGES_AVAILABLE + # through the same +0x7f8 getter). We own none, so these are honest zeros. + rows += [{"contextId": 3, "contextValue": int(ctx_id), "type": t, "typeValue": 0} + for t in ("kits", "badges")] + return rows, len(sel) + + def _club_stat_set(): """The complete global stat set, computed from what the club actually holds.""" items = STORE.items() @@ -1039,6 +1090,17 @@ def club_stats_route(h): # deliberately does not disturb the Stats2 map. return 200, {} stats = _club_stat_set() + # Per-context modes carry an id in the URL: country/, league/, + # team/. Those tabs read a bucket keyed by that id, so the global rows above + # are invisible to them. Both sets go in the SAME response because every response + # wipes the whole map first, so anything left out of this body is erased. + parts = mode.split("/") + if len(parts) >= 2 and parts[1].isdigit(): + ctx_rows, n = _club_stat_context(parts[0], int(parts[1])) + stats = stats + ctx_rows + log(" CLUBSTATS: %s -> %d rows (global players=%d, context %s=%d players=%d)" + % (mode, len(stats), stats[0]["typeValue"], parts[0], int(parts[1]), n)) + return 200, {"stat": stats} log(" CLUBSTATS: %s -> %d stat rows (players=%d)" % (mode or "(none)", len(stats), stats[0]["typeValue"])) return 200, {"stat": stats}