fix(fifa17): send a club item's real wire assetId, not its carddbid

A club item's `assetId` (record +0x20) is family specific and is NOT the
carddbid: per the client's own tables a kit carries the art class from
fcc_kitcards.assetid - 14 for the 63xxxxx home/third band, 15 for the 64xxxxx
away band - a badge carries its team id, and a ball and stadium their own
asset number. The catalog shipped `asset_id`, the carddbid, in that slot.

Measured on the live client with both kits resident: record +0x20 held
6300006 (home) and 6400003 (away) where the table says 14 and 15, while every
other field - resourceId, cardassetid 35, category 2/3, teamid 21, year 0,
itemState 101/102 - already matched. Operator reports both pre-match kit tiles
rendering identically. assetId is the only field that diverges from the
client's own data, and an assetId that is not a valid kit art class cannot
resolve to distinct art.

`resource_id` is derived from `asset_id`, and every home kit shares art class
14, so the two cannot be the same field: catalogs now carry an optional
`club_asset_id`, defaulting to `asset_id` so a catalog predating the field and
every non-club kind are unchanged. resolve_kit emits it as the wire `assetId`.

Fixed at the source too - scripts/sold-staging-up.py emitted asset_id as the
wire assetId for all four club families, so a re-emit would have regressed it.

Field-offset note: club_items.json's _record_map is authoritative and my
earlier working note had these transposed - assetId is +0x20 and cardassetid
is +0x1c, not the reverse.

Adds tools/live/diff_kit_records.py, which byte-diffs the two resident kit
records and names the fields the decoded clone query consumes.

Staging wire now reads assetId 14/15 with cardassetid 35 on both
squad.actives and /club?type=equippables. Workspace 1250 passed, 0 failed.
Client re-parse still to be confirmed visually.
This commit is contained in:
funman300
2026-08-24 20:35:58 +00:00
parent 6bbc0eaf4f
commit 74768693ec
4 changed files with 227 additions and 17 deletions
+61
View File
@@ -41,6 +41,20 @@ pub struct Fifa17CardIdentity {
/// FIFA card-art class. Players default to `asset_id`; kit definitions carry
/// the verified `fcc_kitcards.cardassetid` value (`35`).
pub card_asset_id: u32,
/// The wire `assetId` for a CLUB item (record `+0x20`), which is family
/// specific and is NOT the carddbid: a kit carries the art class from
/// `fcc_kitcards.assetid` (`14` home/third band, `15` away band), a badge
/// carries its team id, a stadium and a ball their own asset number.
///
/// Distinct from [`Self::asset_id`], which for these definitions is the
/// carddbid and is what `resource_id` is derived from — so the two cannot be
/// the same field. Shipping the carddbid here is what left the client
/// holding `assetId 6300006` at record `+0x20` where its own table says
/// `14`, with both pre-match kit tiles rendering identically.
///
/// Defaults to `asset_id` when a catalog does not specify it, which is the
/// pre-existing behaviour and is correct for every non-club kind.
pub club_asset_id: u32,
/// Source team id for a club kit, or a manager's real club. Zero for content
/// kinds that do not use it.
pub team_id: i64,
@@ -206,6 +220,9 @@ struct RawCard {
/// Separate card-art id for non-player definitions; absent → `asset_id`.
#[serde(default)]
card_asset_id: Option<u32>,
/// Wire `assetId` for a club item; defaults to `asset_id`. See
/// [`Fifa17CardIdentity::club_asset_id`].
club_asset_id: Option<u32>,
/// Source team id for a kit or manager definition; absent → `0`.
#[serde(default)]
team_id: Option<i64>,
@@ -287,6 +304,7 @@ impl Fifa17CardCatalog {
kind: ContentKind::from_str(&rc.kind),
subtype: rc.subtype,
card_asset_id: rc.card_asset_id.unwrap_or(rc.asset_id),
club_asset_id: rc.club_asset_id.unwrap_or(rc.asset_id),
team_id: rc.team_id.unwrap_or(0),
category: rc.category.unwrap_or(0),
year: rc.year.unwrap_or(0),
@@ -380,6 +398,49 @@ mod tests {
assert_eq!(cat.lookup("card_missing"), None);
}
/// A club item's wire `assetId` is family specific and is NOT the carddbid.
///
/// Regression: the catalog shipped `asset_id` (the carddbid) as the wire
/// `assetId`, so the client held `assetId 6300006` at record `+0x20` where
/// its own `fcc_kitcards` says `14`, and both pre-match kit tiles rendered
/// identically. `resource_id` is derived from `asset_id`, and every home kit
/// shares art class 14, so the two genuinely cannot be one field.
#[test]
fn club_items_carry_their_own_wire_asset_id_distinct_from_the_carddbid() {
let cat = Fifa17CardCatalog::from_json_str(
r#"{"schema_version":1,"game":"fifa17","cards":{
"fifa17_6300006":{"asset_id":6300006,"kind":"kit","subtype":9,
"card_asset_id":35,"club_asset_id":14,"team_id":21,"category":2,"year":0},
"fifa17_6400003":{"asset_id":6400003,"kind":"kit","subtype":9,
"card_asset_id":35,"club_asset_id":15,"team_id":21,"category":3,"year":0},
"fifa17_20801":{"asset_id":20801}
}}"#,
)
.unwrap();
let home = cat.lookup("fifa17_6300006").unwrap();
let away = cat.lookup("fifa17_6400003").unwrap();
// resourceId stays the carddbid — it is what the staff/kit merge keys on.
assert_eq!(home.resource_id, 6300006);
assert_eq!(away.resource_id, 6400003);
// The card frame art is shared by the whole kit family.
assert_eq!(home.card_asset_id, 35);
assert_eq!(away.card_asset_id, 35);
// The art class is what distinguishes home from away on the wire.
assert_eq!(home.club_asset_id, 14);
assert_eq!(away.club_asset_id, 15);
assert_ne!(
home.club_asset_id, away.club_asset_id,
"home and away must not present the same assetId"
);
// Absent: defaults to asset_id, which is correct for every non-club kind
// and preserves the behaviour of a catalog that predates the field.
let player = cat.lookup("fifa17_20801").unwrap();
assert_eq!(player.club_asset_id, 20801);
}
/// The non-player definition fields a consumable needs, and the ABSENCE that
/// must stay an absence: a defaulted `amount` would draw "-1" on the card and
/// a defaulted `contract` would invent the number of matches a card grants.