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/<id>, league/<id> and team/<id>, 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -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/<nation>, league/<id>,
|
||||
# team/<id>. 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}
|
||||
|
||||
Reference in New Issue
Block a user