diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index bd88af2..ce95e79 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -955,6 +955,11 @@ ROUTES = [ # generic /club route, which answers with the FULL 28-item club list where the # client asked for STATS: wrong shape, and re-sent on every poll. (re.compile(G + r"/club/stats"), lambda m, h: club_stats_route(h)), + # MUST precede the generic /club: the client asks GET club/consumables/ + # and that path is a /club prefix, so without this line it is answered with the + # player list. LIVE 2026-08-05: it was, and the consumables screen was handed + # Cristiano Ronaldo when it asked for training cards. + (re.compile(G + r"/club/consumables"), lambda m, h: club_consumables_route(h)), (re.compile(G + r"/club"), lambda m, h: club_route(h)), ] @@ -1259,6 +1264,60 @@ def _club_stat_set(): for t, v in counts] +# GET ut/%s/club/consumables/ -- the consumables ITEM list. +# +# FOUND LIVE 2026-08-05, and only because the counts were fixed first. The client +# does not ask for consumables through club?type=; it asks here, and it asks ONLY +# once club/stats/consumables reports a non-zero count. So the counter was the gate +# and this route is the door. Before this line existed the path fell through to the +# generic /club prefix and the consumables screen was answered with the 194-card +# player list. +# +# The segment names come from the UI group table at 0x180203260 (codes 0x00 training, +# 0x01 contracts, 0x04 fitness, 0x03 healing, 0x17 playStyle, 0x18 +# managerLeagueModifier, 0x11 position). "training" and "contracts" are CONFIRMED on +# the wire; the other five are from that table and are matched case-insensitively, +# with the singular "contract" accepted because the client has used both spellings. +# +# The category numbers are fut_consumables' own, and they line up with the panel +# exactly: 0 -> Training 42, 2+3 -> Contracts 13, 4 -> Healing 21, 5 -> Fitness 6, +# 8 -> Position Modifier 20, 9 -> Chemistry Style 24. That correspondence is what +# makes an empty list here distinguishable from a wrong mapping. +CLUB_CONSUMABLE_CATS = { + "training": {0}, + "contracts": {2, 3}, + "contract": {2, 3}, + "fitness": {5}, + "healing": {4}, + "position": {8}, + "playstyle": {9}, + "managerleaguemodifier": {10}, +} + + +def club_consumables_route(h): + seg = h.path.split("/club/consumables", 1)[-1].split("?")[0].strip("/").lower() + if not CONSUMABLES: + # Flag off: the club genuinely holds none. An empty itemData is the honest + # answer and is the same shape the client already accepts elsewhere. + return 200, {"itemData": []} + import fut_consumables + shelf = fut_consumables.starter_consumables(fut_consumables.CONSUMABLE_ID_BASE) + cats = CLUB_CONSUMABLE_CATS.get(seg) + if cats is None: + # An unknown segment: serve the whole shelf rather than nothing, so a + # spelling we have not seen still shows cards instead of an empty screen, + # and log it loudly because it is a new wire fact worth acting on. + log(" CONSUMABLES: UNKNOWN category %r -- serving the whole shelf. Add it " + "to CLUB_CONSUMABLE_CATS." % seg) + items = shelf + else: + items = [i for i in shelf + if fut_consumables.BY_SUBTYPE[i["cardsubtypeid"]]["category"] in cats] + log(" CONSUMABLES: %s -> %d item(s)" % (seg or "(none)", len(items))) + return 200, {"itemData": items} + + def club_stats_route(h): mode = h.path.split("/club/stats/", 1)[-1].split("?")[0] if "/club/stats/" in h.path else "" if not CLUBSTATS: