diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 3438cbd..89b7e77 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -1013,42 +1013,63 @@ def _counts_for(players): ] -def _club_stat_context(kind, ctx_id): - """Per-context rows for the nation / league / team tabs. +def _club_stat_context(kind): + """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. - Selecting the ENGLAND tab on the MY CLUB screen issues exactly one request: + Read out of the club-stats provider FUN_180043b90, not guessed. The per-context + 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 - tab showed nothing while the club holds 8 England players, because every row we - sent carried contextId 1. + THREE CONSEQUENCES, all of which my first attempt got wrong: - 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. + 1. One response must carry a bucket for EVERY ROW the screen will show, because + the reader iterates rows and looks up each row's own id. Keying everything to + the id in the URL, which is what I did first, fills exactly one bucket that the + screen never asks for. + 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/ lists the LEAGUES in that nation (case 3, keyed + by LEAGUE_ID) and league/ 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 - preserves contextValue. `TODO/CONFIRM` what contextId means semantically; nothing - read so far assigns it a meaning beyond that guard. + Because every response WIPES the whole map, each response only needs the buckets + for its own screen. That is also what keeps nation ids and league ids from + 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() - 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) + players = [i for i in STORE.items() if i.get("itemType") == "player"] + field = {"country": "leagueId", "league": "teamid"}.get(kind) + if not field: + return [], 0 + 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: + counts = [c for c in _counts_for(sel) if c[0] != "players"] + counts += [("kits", 0), ("badges", 0)] + rows += [{"contextId": 3, "contextValue": int(ctx), "type": t, "typeValue": int(v)} + for t, v in counts] + return rows, len(ctxs) + 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. parts = mode.split("/") 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/ renders a list of leagues keyed by LEAGUE_ID; league/ 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 - 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)) + log(" CLUBSTATS: %s -> %d rows (global players=%d, %d %s buckets)" + % (mode, len(stats), stats[0]["typeValue"], n, + {"country": "league", "league": "team"}.get(parts[0], parts[0]))) return 200, {"stat": stats} log(" CLUBSTATS: %s -> %d stat rows (players=%d)" % (mode or "(none)", len(stats), stats[0]["typeValue"]))