ccf912c157
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
164 lines
7.1 KiB
Python
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())
|