#!/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) # CORRECTED 2026-08-06. Every previous subtype was inside the 0x91..0x96 block, which # is TROPHIES: FUN_180108c00 computes subtype = tournamentType + 0x91, and FUN_1800fed90 # is the only function in the binary whose case set is exactly {0x91..0x96}. So all five # families were pointed at the trophy range. # # Kits, stadia and badges are NOT cardtype 9. FUN_1800d8330 has # `case 9: case 10: case 0xb: return 7`, and cardtype 7 DOES have a resolver: manager # vtable +0x498 = FUN_180119bd0, reached from FUN_1800f6c40 when item+0x4c == 7, called # with (subtype, teamid, assetId). That matters for testing: CARD_SYSTEM.md said a wrong # club-item id "cannot announce itself", and for these three that is false. A wrong # teamid produces a visibly wrong TeamName_Abbr15_ caption, which is why kits go first. FAMILIES = [ ("balls", "fcc_balls.json", 37, 0x1E, "balls", 30), ("stadia", "fcc_stadium.json", 36, 0x14, "stadia", 10), ("badges", "fcc_badgecards.json", 39, 0x2E, "badgeDBid", 11), ("kits", "fcc_kitcards.json", 35, 0x28, "kits", 9), ("leaguelogos", "fcc_leaguelogos.json", 40, 0x2F, "leagueLogos", 31), ] # Candidate set for probe_shelf(). The old set {30,31,145..150} could NOT have answered # the question for kits, stadia or badges, because 9, 10 and 11 were not in it: the # probe route the docs preferred would have spent a launch and returned nothing for # three of the five families. CARDTYPE9_SUBTYPES = (9, 10, 11, 30, 31) # 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, teamid=None, extra=None): """One club item. Deliberately narrow: no rating, no position, no attributes, no nation, no league. 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, "itemState": "free", "owners": 1, "untradeable": False, } # KIT (9) and BADGE (11) display as