fifa17-recon: consumables are STACK records, not bare items

The route was right and the body was wrong. We served bare items, the client took the
response and inserted NOTHING (the live card map held only the 11 squad players), and
the screen stayed empty with no error logged anywhere. A 200 with a well-formed body
that the consumer silently discards is the worst failure shape there is, and it is the
third time this project has hit it.

FutConsumablesSearchServerResponse resolved from the RS4 literal 0x1802222f8:
factory 0x180130a10, vtable 0x180222200, deserializer +0x08 = 0x180130d10 (6873
chars). It takes itemData(0x16b) at the root like the club list, but its ELEMENT is
not an item. It is a five-atom wrapper and exactly one of the five carries the item:

    0xbc  count             int
    0xd7  discardValue      int
    0x16a item              -> FUN_18013fe00, the item parser itself
    0x287 resourceId        int
    0x362 untradeableCount  int

Everything else goes to the value-skip handler, which is precisely why a bare item was
accepted and did nothing. It also explains the UI: FUT draws consumables as one stack
with a quantity, not as N cards, and the wrapper is that quantity.

Identical consumables are now collapsed by resourceId and counted.

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:
funman300
2026-08-05 11:34:01 -07:00
parent ccb736fa71
commit 122b7b94d2
+40 -1
View File
@@ -1295,6 +1295,43 @@ CLUB_CONSUMABLE_CATS = {
}
def _consumable_stacks(items):
"""Wrap consumables as the STACK records this response class actually reads.
RESOLVED LIVE 2026-08-05 the hard way: we served bare items here, the client
took the response and inserted NOTHING (the card map held only the 11 squad
players), and the screen stayed empty with no error anywhere.
FutConsumablesSearchServerResponse (RS4 literal 0x1802222f8, factory
0x180130a10, vtable 0x180222200, deserializer +0x08 = 0x180130d10, 6873 chars)
takes itemData(0x16b) at the root like the club list, but its ELEMENT is not an
item. It is a five-atom wrapper and only ONE of those five carries the item:
0xbc count int
0xd7 discardValue int
0x16a item -> FUN_18013fe00, the item parser itself
0x287 resourceId int
0x362 untradeableCount int
Everything else falls to the value-skip handler, which is why a bare item was
accepted and silently did nothing. That is also why FUT draws consumables as one
stack with a quantity rather than N separate cards.
Identical consumables are therefore COLLAPSED by resourceId and counted.
"""
stacks = {}
for it in items:
rid = it.get("resourceId")
s = stacks.get(rid)
if s is None:
s = stacks[rid] = {"count": 0, "discardValue": 0, "item": it,
"resourceId": rid, "untradeableCount": 0}
s["count"] += 1
if it.get("untradeable"):
s["untradeableCount"] += 1
return list(stacks.values())
def club_consumables_route(h):
seg = h.path.split("/club/consumables", 1)[-1].split("?")[0].strip("/").lower()
if not CONSUMABLES:
@@ -1315,7 +1352,9 @@ def club_consumables_route(h):
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}
stacks = _consumable_stacks(items)
log(" CONSUMABLES: %d item(s) collapsed into %d stack(s)" % (len(items), len(stacks)))
return 200, {"itemData": stacks}
def club_stats_route(h):