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}