fifa17-recon: the consumables item route, found live -- GET club/consumables/<category>
The counter WAS the gate, and fixing it produced the request within seconds:
11:23:36 GET /ut/game/fifa17/club/consumables/training
11:23:40 GET /ut/game/fifa17/club/consumables/contracts
Neither is club?type=, and neither is the /consumables/%s template we had been
hunting in the binary. The client asks here, and it asks ONLY once
club/stats/consumables reports a non-zero count. Two rounds of item-shape work went
unrequested for want of a counter.
Worse, the path is a /club prefix, so it fell through to the generic route: the
consumables screen was answered with the 194-card PLAYER list. It asked for training
cards and got Cristiano Ronaldo.
Now routed above the generic /club, serving the shelf filtered by category. The
segment names come from the UI group table at 0x180203260; training and contracts are
CONFIRMED on the wire, the other five are from that table and matched case
insensitively, with singular "contract" accepted because the client has used both.
An unknown segment serves the whole shelf and logs loudly rather than showing an empty
screen, since a new spelling is a wire fact worth catching.
Per-category counts match the on-screen panel exactly: training 42, contracts 13,
fitness 6, healing 21, position 20, playStyle 24, managerLeague 0. That correspondence
is what makes an empty list distinguishable from a wrong mapping.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -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/<category>
|
||||
# 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/<category> -- 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:
|
||||
|
||||
Reference in New Issue
Block a user