From 2f8a6512dbc01b3db2c519a470654de711c38532 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 5 Aug 2026 11:19:29 -0700 Subject: [PATCH] fifa17-recon: answer the CONSUMABLES panel with consumable counts, not player counts The tab was empty because we answered the wrong question. The client asks GET club/stats/consumables 41 times a session; we replied with the PLAYER stat set (players 205, playersGold 189 ...), which that panel does not read. Confirmed on screen: seven categories present and selectable, every one reading 0. Now appends 14 consumables* rows counted from the SHELF. The shelf, not STORE.items(): the consumables we serve are a synthetic overlay never granted into the save, so counting the store gives fourteen zeros, which on screen is byte-identical to failure and would have made the experiment unreadable. Counts match the independently derived expectation exactly: 126 total, 21 healing, 7 player contracts, 21 player training, 3 player fitness, 20 position, 21 GK training, 6 manager contracts, 19 playstyle. Safe by construction: the vocabulary is an ATOM switch (FUN_18012fd40, 40 arms, default return 0), so an unrecognised name is inert rather than fatal, and the rows are APPENDED -- the player, nation and league rows that drive the working screens are untouched. Zero rows unless FUT_CONSUMABLES is armed. Default ON because answering the consumables panel with player counts is wrong by inspection rather than a judgement call. FUT_CONSUM_STATS=0 reverts. 439 + 414 checks green. The open question this sets up: whether a non-zero count makes the client request an item list at all. If it does, the log names the route and /consumables/%s is settled for free. If the numbers move and no request follows, the panel renders from counts alone and the 126-item shelf was never needed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW --- fifa17-recon/tools/utas_server.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index f768ecf..bd88af2 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -1163,6 +1163,48 @@ def _club_stat_context(kind): +# FUT_CONSUM_STATS: answer the CONSUMABLES panel with CONSUMABLE counts. +# +# LIVE 2026-08-05, and this is the whole reason the tab was empty. The client asks +# GET club/stats/consumables 41 times a session and we answered it with the PLAYER +# stat set (players 205, playersGold 189 ...). The panel reads a different set of +# names entirely, so it was told about footballers when it asked about contracts. +# The player screenshot is unambiguous: seven categories -- Training, Contracts, +# Fitness, Healing, Chemistry Style, Manager League, Position Modifier -- every one +# reading 0, on a tab that is present and selectable. +# +# The vocabulary is not a string table: FUN_18012fd40 looks the `type` string up in +# the ATOM table and switches on 40 atom ids, default `return 0`. An unrecognised +# name is therefore INERT, not fatal, which is what makes appending these rows safe. +# +# COUNT THE SHELF, NOT THE STORE. The consumables we serve are a synthetic overlay +# and are never granted into the save, so STORE.items() holds none of them and +# counting it yields fourteen zeros -- which looks exactly like failure on screen and +# would make the experiment unreadable. +# +# Default ON: answering the consumables panel with player counts is wrong by +# inspection, not a judgement call. Set FUT_CONSUM_STATS=0 to go back. +CONSUM_STATS = os.environ.get("FUT_CONSUM_STATS", "1") == "1" + + +def _consumable_stat_rows(): + """[(stat name, count)] for the consumables panel, counted from the shelf.""" + if not (CONSUM_STATS and CONSUMABLES): + return [] + try: + import fut_consumables + import fut_club_stats + except Exception as e: # never break the panel over this + log(" CLUBSTATS: consumable rows unavailable (%s)" % e) + return [] + shelf = fut_consumables.starter_consumables(fut_consumables.CONSUMABLE_ID_BASE) + g = fut_club_stats.global_counts(shelf) + rows = [(fut_club_stats.VOCAB[sid], v) for sid, v in sorted(g.items()) + if sid >= 0x3C and sid in fut_club_stats.VOCAB] + log(" CLUBSTATS: %d consumable row(s) from a shelf of %d" % (len(rows), len(shelf))) + return rows + + def _club_stat_set(): """The complete global stat set, computed from what the club actually holds.""" items = STORE.items() @@ -1211,6 +1253,7 @@ def _club_stat_set(): ("trophiesFeaturedOnline", 0), # 0x36 ("trophiesSeasonOffline", 0), # 0x37 ] + counts += _consumable_stat_rows() # All four keys in every element -- see note 3 above. return [{"contextId": 1, "contextValue": 0, "type": t, "typeValue": int(v)} for t, v in counts]