Files
OpenFUT/fifa17-recon/tools/fut_clubitems.py
T
funman300 ccf912c157 fifa17-recon: club items crashed the client -- unestablished fields, and too wide a blast radius
The game hung and then crashed on the first equippables fetch. My fault twice over.

CAUSE, primary. I copied teamid, leagueid and value straight out of the fcc row as
extras. `value` appears elsewhere as an OBJECT member (displayGroup {"value": ...}),
and a scalar where an object is expected is the type-desync busy loop at 0x1801c7f1a
-- which presents exactly as "the game is taking its time" and then dies. Omission is
safe; an unestablished field is not. That is this project's own rule and I broke it
for three fields that were not needed to draw a card. All three are gone.

CAUSE, contributing. The last request before the crash was type=equippables&count=11
and we answered with 30 items spanning FIVE unverified cardsubtypeids at once: the
widest possible blast radius for a wrong shape, and it tells you nothing about which
subtype was wrong. equippables now answers [] until the subtypes are confirmed one
family at a time, and shelf() takes a families= filter so a test can serve exactly one.

Adds FUT_CLUBITEMS=probe:<family>, which serves one item per candidate subtype for a
single family, so the screen names the correct subtype instead of me guessing a third
time. Eight items, one family, one question.

The flag already defaulted off, so a plain restart cannot serve any of this.

439 + 414 checks green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-05 11:59:38 -07:00

164 lines
7.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Club items: balls, stadia, badges, kits and league logos.
Built from the game's OWN tables, dumped read-only into data/tables/ by db_dump.py:
fcc_balls 42 rows carddbid 8120194+ cardassetid 37
fcc_stadium 78 rows carddbid 6200000+ cardassetid 36
fcc_badgecards 656 rows carddbid 6000000+ cardassetid 39
fcc_kitcards 1482 rows carddbid 6300000+ cardassetid 35
fcc_leaguelogos 44 rows carddbid 8010000+ cardassetid 40
TWO ID COLUMNS, AND THEY ARE NOT INTERCHANGEABLE. Every fcc_ row carries BOTH
carddbid and cardassetid. carddbid is the database key the merge would use;
cardassetid is the ART id the card draws from. fut_store._item copies resourceId
into cardassetid, which is right for players and wrong for every other family --
that is exactly what produced the green "NOT FOUND" placeholder on consumables
(external/ion_fut/artAssets/.../notfound.swf) until it was fixed on 2026-08-05.
So this module sets both explicitly and never lets one default to the other.
WHAT IS NOT KNOWN YET, AND IS NOT GUESSED HERE
----------------------------------------------
cardtype 9 (the club-item family) has NO arm in the merge FUN_180141660: no table
query and no miss-fill. So unlike a player or a coach, a club item's identity does
NOT come from the local card DB, and a wrong id cannot announce itself. The
cardsubtypeid values that reach cardtype 9 are the eight-value set
{30, 31, 145, 146, 147, 148, 149, 150}, and WHICH of those means ball versus
stadium versus badge is assigned nowhere in the 149 dumped tables.
Rather than guess, SUBTYPE is a per-family constant below with an explicit
"unverified" marker, and probe_shelf() serves one item per candidate subtype so the
screen itself can say which is which. The counts do not need any of this: a count is
just a number, which is why counts come first.
THE COUNT IS THE GATE. Proven on consumables the same day: the client does not ask
for an item list until club/stats reports a non-zero count for that family. The
CLUB tab reads global stat ids 1 (players), 0x1e (balls), 0x28 (kits), 0x14
(stadia), 0x0a (staff) and 0x32 (trophies), so making those non-zero is what makes
the client reveal the item route it uses. Nothing here should be believed to work
until that route is observed in the log.
"""
import json
import os
_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "tables")
CLUBITEM_ID_BASE = 960000000 # distinct from save 1e8, sweep 9e8, consumables 9.4e8
# (table, art id, stat id, stat name, UNVERIFIED cardsubtypeid)
FAMILIES = [
("balls", "fcc_balls.json", 37, 0x1E, "balls", 149),
("stadia", "fcc_stadium.json", 36, 0x14, "stadia", 148),
("badges", "fcc_badgecards.json", 39, 0x2E, "badgeDBid", 145),
("kits", "fcc_kitcards.json", 35, 0x28, "kits", 146),
("leaguelogos", "fcc_leaguelogos.json", 40, 0x2F, "leagueLogos", 150),
]
# Every cardsubtypeid known to reach cardtype 9. Used by probe_shelf().
CARDTYPE9_SUBTYPES = (30, 31, 145, 146, 147, 148, 149, 150)
# How many of each family the starter club owns. Small on purpose: the point is to
# make the counter non-zero so the client asks, not to hand anyone a collection.
STARTER_N = {"balls": 6, "stadia": 4, "badges": 8, "kits": 8, "leaguelogos": 4}
def _rows(fname):
try:
with open(os.path.join(_DATA, fname)) as f:
return json.load(f).get("rows") or []
except (IOError, ValueError):
return []
def _item(item_id, carddbid, cardassetid, subtype, extra=None):
"""One club item. Deliberately narrow: no rating, no position, no attributes,
no nation, no league, no team. A club item has none of those, and sending a
field the family does not have is how a wrong shape gets accepted and does
nothing."""
it = {
"id": item_id,
"resourceId": carddbid,
"assetId": carddbid,
"cardassetid": cardassetid, # THE ART ID, never a copy of resourceId
"cardsubtypeid": subtype,
"itemType": "club", # UNOBSERVED on the wire; see module docstring
"itemState": "free",
"owners": 1,
"untradeable": False,
}
if extra:
it.update(extra)
return it
def shelf(next_id=CLUBITEM_ID_BASE, families=None):
"""The starter club-item shelf, {family: [item]}.
`families` limits which are built. The combined `equippables` view is what
crashed the client: 30 items across FIVE unverified subtypes in one response is
the widest possible blast radius for a wrong shape. One family at a time is the
only way to learn which subtype is wrong.
"""
out, nid = {}, next_id
for name, table, art, _sid, _sname, subtype in FAMILIES:
if families is not None and name not in families:
out[name] = []
continue
rows = _rows(table)
picked = []
for r in rows[:STARTER_N.get(name, 4)]:
cid = r.get("carddbid")
if not cid:
continue
# NO EXTRAS. An earlier version copied teamid/leagueid/value straight
# out of the fcc row and the game hung and then CRASHED on the first
# equippables fetch (2026-08-05). `value` is the prime suspect: it
# appears elsewhere as an OBJECT member (displayGroup {"value": ...}),
# and a scalar where an object is expected is the type-desync busy loop
# at 0x1801c7f1a, which reads exactly like "the game is taking its time"
# and then dies. Omission is safe; an unestablished field is not. None of
# the three was needed to draw a card.
picked.append(_item(nid, cid, r.get("cardassetid", art), subtype))
nid += 1
out[name] = picked
return out
def counts(next_id=CLUBITEM_ID_BASE):
"""[(stat name, count)] for the club panel."""
s = shelf(next_id)
return [(sname, len(s.get(name, [])))
for name, _t, _a, _sid, sname, _st in FAMILIES]
def probe_shelf(family, next_id=CLUBITEM_ID_BASE):
"""One item per CANDIDATE cardsubtypeid, same carddbid, for the live oracle.
Which of {30,31,145..150} means which family is unknown and unguessable from the
dumped tables. Serving all eight and looking at the screen is the cheapest way to
find out, and unlike a sweep it is READABLE: the family that draws real artwork
names its own subtype.
"""
entry = next((f for f in FAMILIES if f[0] == family), None)
if entry is None:
return []
_n, table, art, _sid, _sname, _st = entry
rows = _rows(table)
if not rows:
return []
r = rows[0]
return [_item(next_id + i, r.get("carddbid"), r.get("cardassetid", art), st)
for i, st in enumerate(CARDTYPE9_SUBTYPES)]
if __name__ == "__main__":
s = shelf()
for name, items in s.items():
print("%-12s %2d item(s)" % (name, len(items)))
if items:
i = items[0]
print(" resourceId=%-9s cardassetid=%-4s subtype=%s"
% (i["resourceId"], i["cardassetid"], i["cardsubtypeid"]))
print("\ncounts:", counts())