feat(fifa17): project owned kits with active home/away designation

Closes the server side of the FUT kit selector. Ownership stays generic in
Core (submodule bump: club_kit_assignments + GET/PUT /club/kits); this
commit adds the FIFA17 representation, the host projection and importer
support.

adapter:
* ContentKind::Kit ("kit") so kits are classified alongside player/staff/
  consumable instead of being mistaken for 0-rated players.
* Fifa17CardIdentity carries card_asset_id and team_id; RawCard keeps both
  optional because the emitted catalog writes null for non-kit definitions.
* shape_kit_item emits only the fields the client's kit path reads
  (id/resourceId/assetId/cardassetid/cardsubtypeid/itemState/owners/
  untradeable/teamid) — no attributeList, no itemType.
* itemState on the wire is the STRING token activeHomeKit/activeAwayKit;
  the 101/102 integers are the client's post-deserialisation runtime enum
  (item+0x5c) and are never emitted.
* club_stats S_KITS (0x28) now counts owned kits instead of a hard zero.

host:
* CoreKitAssignments + CoreAccess::get_active_kits (GET /club/kits),
  defaulting to no active kits so a Core without the endpoint degrades
  instead of fabricating a designation.
* handle_club classifies type=player|kit, rejects any other type with an
  empty page and outcome=unsupported_type, and now always fetches
  unpaginated from Core: kind and transfer-pile membership are host-side
  concepts Core cannot express, so filtering and pagination must both
  happen after shaping or pages come back short.

importer:
* ItemClass::Kit (cardsubtypeid == 9 and resourceId in 6_300_000..=6_400_654),
  kit counts/balances, and card_asset_id/team_id carried into the emitted
  catalog and manifest.
* a kit group missing cardassetid == 35 or teamid is DEFERRED
  (missing_kit_render_metadata) rather than defaulted; conflicting render
  metadata across instances defers as render_metadata_conflict.

staging: sold-staging-up.py seeds two owned kits (6300006 home / 6400003
away, team 21) plus both active designations so the projection can be
verified over HTTP before involving the client.
This commit is contained in:
funman300
2026-08-21 03:17:57 +00:00
parent ab62440dbf
commit db743ffd1f
13 changed files with 616 additions and 169 deletions
+69 -4
View File
@@ -149,6 +149,14 @@ SELLER_SQUAD_CARDS = [
DISPOSABLE_ITEM = "owned-a-disposable"
DISPOSABLE_CARD = "fifa17_232273" # Nelson Atiagli LB 51, rareflag 1
# Two authoritative modern kit-card definitions for one real source team. These
# are staging fixtures derived from fcc_kitcards, not synthetic FIFA identities.
KIT_TEAM_ID = 21
STAGING_KITS = [
("home", "owned-a-kit-home", "fifa17_6300006", 6_300_006),
("away", "owned-a-kit-away", "fifa17_6400003", 6_400_003),
]
READY_TIMEOUT_S = 60.0
@@ -439,6 +447,47 @@ def materialise(lay: Layout) -> None:
shutil.copy2(safe_path(src), safe_path(dst))
ok(f"FIFA17 content copied from {CONTENT_SRC} (outside production state)")
with open(safe_path(lay.cards)) as fh:
definitions = json.load(fh)
with open(safe_path(lay.catalog)) as fh:
catalog = json.load(fh)
existing = {definition["id"] for definition in definitions}
for _slot, _owned_id, card_id, resource_id in STAGING_KITS:
if card_id not in existing:
definitions.append({
"id": card_id,
"name": "Kit",
"overall": 0,
"position": "",
"nation": "",
"league": "",
"club": "",
"pace": 0,
"shooting": 0,
"passing": 0,
"dribbling": 0,
"defending": 0,
"physical": 0,
"rarity": "bronze",
"image_path": None,
})
catalog["cards"][card_id] = {
"asset_id": resource_id,
"version": 0,
"rareflag": 0,
"kind": "kit",
"subtype": 9,
"card_asset_id": 35,
"team_id": KIT_TEAM_ID,
}
with open(safe_path(lay.cards), "w") as fh:
json.dump(definitions, fh, indent=2)
fh.write("\n")
with open(safe_path(lay.catalog), "w") as fh:
json.dump(catalog, fh, indent=2)
fh.write("\n")
ok("added ownership-backed home/away kit fixtures from fcc_kitcards")
def assert_seed_cards_resolvable(lay: Layout) -> None:
"""Core's content preflight rejects any owned card whose card_id is not a loaded
@@ -448,7 +497,11 @@ def assert_seed_cards_resolvable(lay: Layout) -> None:
pack_ids = {c["id"] for c in json.load(fh)}
with open(safe_path(lay.catalog)) as fh:
catalog_ids = set(json.load(fh)["cards"])
wanted = [c for _, c in SELLER_SQUAD_CARDS] + [DISPOSABLE_CARD]
wanted = (
[card for _, card in SELLER_SQUAD_CARDS]
+ [DISPOSABLE_CARD]
+ [card for _, _, card, _ in STAGING_KITS]
)
missing_pack = sorted(set(wanted) - pack_ids)
missing_cat = sorted(set(wanted) - catalog_ids)
if missing_pack or missing_cat:
@@ -587,7 +640,11 @@ def seed_core_db(lay: Layout) -> None:
"INSERT INTO owned_cards (id, club_id, card_id, is_loan, "
"acquired_at) VALUES (?, ?, ?, 0, ?)",
[(item, SELLER_CLUB, card, TS) for item, card in SELLER_SQUAD_CARDS]
+ [(DISPOSABLE_ITEM, SELLER_CLUB, DISPOSABLE_CARD, TS)],
+ [(DISPOSABLE_ITEM, SELLER_CLUB, DISPOSABLE_CARD, TS)]
+ [
(owned_id, SELLER_CLUB, card_id, TS)
for _slot, owned_id, card_id, _resource_id in STAGING_KITS
],
)
conn.execute(
"INSERT INTO squads (id, club_id, name, formation, created_at, "
@@ -602,12 +659,20 @@ def seed_core_db(lay: Layout) -> None:
for idx, (item, _) in enumerate(SELLER_SQUAD_CARDS)
],
)
conn.executemany(
"INSERT INTO club_kit_assignments (club_id, slot, owned_card_id, updated_at) "
"VALUES (?, ?, ?, ?)",
[
(SELLER_CLUB, slot, owned_id, TS)
for slot, owned_id, _card_id, _resource_id in STAGING_KITS
],
)
finally:
conn.close()
ok(
f"seeded Seller A ({PERSONA_NAME}, persona {PERSONA_ID}, {SELLER_COINS} coins, "
f"{len(SELLER_SQUAD_CARDS)} starters + 1 disposable) and Buyer B "
f"({BUYER_COINS} coins)"
f"{len(SELLER_SQUAD_CARDS)} starters + 1 disposable + "
f"{len(STAGING_KITS)} active kits) and Buyer B ({BUYER_COINS} coins)"
)