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:
funman300
2026-08-04 14:33:01 -07:00
parent d2bbb4d378
commit 9ee21afb56
+62
View File
@@ -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}