fifa17-recon: club stats -- key the buckets the way the READER looks them up
Second correction in an hour, and this one comes from reading the provider instead of
reasoning about it. The per-context getter is (+0x7f8)(store, contextValue, typeId),
and contextValue comes from THE UI ROW, not from the URL:
case 3: uVar7 = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID")
bronze = (+0x7f8)(store, uVar7, 2)
silver = (+0x7f8)(store, uVar7, 3)
gold = (+0x7f8)(store, uVar7, 4)
publish "PLAYERS_EMPLOYED", gold + silver + bronze
rare/kits/badges = (+0x7f8)(store, uVar7, 5 / 0x28 / 0x2d)
case 4: keyed by "TEAM_ID"; reads 1 (players), 0x28 (kits), 0x2e (badgeDBid)
Three things my previous commit got wrong:
1. It keyed every row to the id in the URL. The reader iterates the SCREEN'S ROWS and
looks up each row's own id, so one response must carry a bucket per row. Keying to
the URL id fills exactly one bucket the screen never asks for, which is why the
ENGLAND tab still showed zeros after the "fix".
2. PLAYERS_EMPLOYED is COMPUTED as gold + silver + bronze in the per-context cases and
is never read from the store, so sending `players` (type id 1) does nothing there.
The tier counts are mandatory.
3. The screens NEST: country/<id> lists the LEAGUES in that nation (case 3, LEAGUE_ID)
and league/<id> lists the TEAMS (case 4, TEAM_ID). That matches the live navigation
exactly: selecting ENGLAND produced Premier League / Championship / League One /
League Two.
Because every response wipes the whole map, each response only needs its own screen's
buckets, which also avoids a real collision: the storage key is contextValue alone, so
nation 14 and league 14 would otherwise share a bucket.
Live output now:
country/14 -> 41 rows, 5 league buckets
league 13 gold=17 -> PLAYERS_EMPLOYED=17 (Premier League)
league 19 gold=26, league 53 gold=55, ...
league/13 -> 6 team buckets {5:20, 21:15, 22:11, 240:21, 241:30, 243:17}
Recorded as a method note: two rounds of reasoning about this endpoint produced two
wrong bodies, and reading twenty lines of the provider produced the right one. The
question "what does the reader look up" is answerable and was not asked.
392 + 61 checks green, zero tracebacks. 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:
@@ -1013,42 +1013,63 @@ def _counts_for(players):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _club_stat_context(kind, ctx_id):
|
def _club_stat_context(kind):
|
||||||
"""Per-context rows for the nation / league / team tabs.
|
"""Per-context rows, keyed the way the READER actually looks them up.
|
||||||
|
|
||||||
LIVE 2026-08-04, and this is a correction to the first version of this route.
|
Read out of the club-stats provider FUN_180043b90, not guessed. The per-context
|
||||||
Selecting the ENGLAND tab on the MY CLUB screen issues exactly one request:
|
getter is `(+0x7f8)(store, contextValue, typeId)`, and the crucial detail is where
|
||||||
|
contextValue comes from: THE UI ROW, not the URL.
|
||||||
|
|
||||||
GET /ut/game/fifa17/club/stats/country/14 (14 = England)
|
case 3: uVar7 = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID")
|
||||||
|
bronze = (+0x7f8)(store, uVar7, 2)
|
||||||
|
silver = (+0x7f8)(store, uVar7, 3)
|
||||||
|
gold = (+0x7f8)(store, uVar7, 4)
|
||||||
|
publish "PLAYERS_EMPLOYED", gold + silver + bronze
|
||||||
|
rare = (+0x7f8)(store, uVar7, 5)
|
||||||
|
kits = (+0x7f8)(store, uVar7, 0x28)
|
||||||
|
badges = (+0x7f8)(store, uVar7, 0x2d)
|
||||||
|
case 4: keyed by "TEAM_ID"; reads 1 (players), 0x28 (kits), 0x2e (badgeDBid)
|
||||||
|
|
||||||
and NO item-list request, so that tab is driven entirely by per-nation stats. The
|
THREE CONSEQUENCES, all of which my first attempt got wrong:
|
||||||
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
|
1. One response must carry a bucket for EVERY ROW the screen will show, because
|
||||||
5 <= contextId <= 9 FORCES contextValue to 0, which is the global bucket the
|
the reader iterates rows and looks up each row's own id. Keying everything to
|
||||||
+0x800 getter reads. The per-nation view reads the +0x7f8 getter keyed by the
|
the id in the URL, which is what I did first, fills exactly one bucket that the
|
||||||
nation id instead. So a body with contextId 1 can only ever populate the global
|
screen never asks for.
|
||||||
bucket, no matter what contextValue says, and the per-context tabs stay empty.
|
2. `PLAYERS_EMPLOYED` is COMPUTED as gold + silver + bronze. It is never read from
|
||||||
|
the store in the per-context cases, so sending `players` (type id 1) does
|
||||||
|
nothing there. The tier counts are mandatory, not decoration.
|
||||||
|
3. The screens NEST: country/<id> lists the LEAGUES in that nation (case 3, keyed
|
||||||
|
by LEAGUE_ID) and league/<id> lists the TEAMS (case 4, keyed by TEAM_ID). That
|
||||||
|
matches the live navigation exactly: selecting ENGLAND produced a Premier
|
||||||
|
League / Championship / League One / League Two list.
|
||||||
|
|
||||||
contextId 3 is used here purely because it is OUTSIDE the guard and therefore
|
Because every response WIPES the whole map, each response only needs the buckets
|
||||||
preserves contextValue. `TODO/CONFIRM` what contextId means semantically; nothing
|
for its own screen. That is also what keeps nation ids and league ids from
|
||||||
read so far assigns it a meaning beyond that guard.
|
colliding: the storage key is contextValue alone, so nation 14 and league 14 would
|
||||||
|
otherwise share a bucket. One kind per response, no collision.
|
||||||
|
|
||||||
|
contextId 3 is used because it is OUTSIDE the guard (contextId == 1, or 5..9,
|
||||||
|
force contextValue to 0) and therefore preserves contextValue. `TODO/CONFIRM`
|
||||||
|
whether contextId carries further meaning; nothing read so far gives it one.
|
||||||
"""
|
"""
|
||||||
items = STORE.items()
|
players = [i for i in STORE.items() if i.get("itemType") == "player"]
|
||||||
if kind == "country":
|
field = {"country": "leagueId", "league": "teamid"}.get(kind)
|
||||||
sel = [i for i in items if i.get("itemType") == "player" and i.get("nation") == ctx_id]
|
if not field:
|
||||||
elif kind == "league":
|
return [], 0
|
||||||
sel = [i for i in items if i.get("itemType") == "player" and i.get("leagueId") == ctx_id]
|
ctxs = sorted({i.get(field) for i in players if i.get(field) is not None})
|
||||||
|
rows = []
|
||||||
|
for ctx in ctxs:
|
||||||
|
sel = [i for i in players if i.get(field) == ctx]
|
||||||
|
if field == "teamid":
|
||||||
|
counts = [("players", len(sel)), ("kits", 0), ("badgeDBid", 0)]
|
||||||
else:
|
else:
|
||||||
sel = [i for i in items if i.get("itemType") == "player" and i.get("teamid") == ctx_id]
|
counts = [c for c in _counts_for(sel) if c[0] != "players"]
|
||||||
rows = [{"contextId": 3, "contextValue": int(ctx_id), "type": t, "typeValue": int(v)}
|
counts += [("kits", 0), ("badges", 0)]
|
||||||
for t, v in _counts_for(sel)]
|
rows += [{"contextId": 3, "contextValue": int(ctx), "type": t, "typeValue": int(v)}
|
||||||
# kits/badges are per-context too (case 3 reads KITS_AVAILABLE and BADGES_AVAILABLE
|
for t, v in counts]
|
||||||
# through the same +0x7f8 getter). We own none, so these are honest zeros.
|
return rows, len(ctxs)
|
||||||
rows += [{"contextId": 3, "contextValue": int(ctx_id), "type": t, "typeValue": 0}
|
|
||||||
for t in ("kits", "badges")]
|
|
||||||
return rows, len(sel)
|
|
||||||
|
|
||||||
|
|
||||||
def _club_stat_set():
|
def _club_stat_set():
|
||||||
@@ -1096,10 +1117,14 @@ def club_stats_route(h):
|
|||||||
# wipes the whole map first, so anything left out of this body is erased.
|
# wipes the whole map first, so anything left out of this body is erased.
|
||||||
parts = mode.split("/")
|
parts = mode.split("/")
|
||||||
if len(parts) >= 2 and parts[1].isdigit():
|
if len(parts) >= 2 and parts[1].isdigit():
|
||||||
ctx_rows, n = _club_stat_context(parts[0], int(parts[1]))
|
# The id in the URL says which screen we are on, NOT which bucket to fill.
|
||||||
|
# country/<n> renders a list of leagues keyed by LEAGUE_ID; league/<n> renders
|
||||||
|
# a list of teams keyed by TEAM_ID. So fill every bucket that screen can show.
|
||||||
|
ctx_rows, n = _club_stat_context(parts[0])
|
||||||
stats = stats + ctx_rows
|
stats = stats + ctx_rows
|
||||||
log(" CLUBSTATS: %s -> %d rows (global players=%d, context %s=%d players=%d)"
|
log(" CLUBSTATS: %s -> %d rows (global players=%d, %d %s buckets)"
|
||||||
% (mode, len(stats), stats[0]["typeValue"], parts[0], int(parts[1]), n))
|
% (mode, len(stats), stats[0]["typeValue"], n,
|
||||||
|
{"country": "league", "league": "team"}.get(parts[0], parts[0])))
|
||||||
return 200, {"stat": stats}
|
return 200, {"stat": stats}
|
||||||
log(" CLUBSTATS: %s -> %d stat rows (players=%d)"
|
log(" CLUBSTATS: %s -> %d stat rows (players=%d)"
|
||||||
% (mode or "(none)", len(stats), stats[0]["typeValue"]))
|
% (mode or "(none)", len(stats), stats[0]["typeValue"]))
|
||||||
|
|||||||
Reference in New Issue
Block a user