Files
OpenFUT/openfut-import-fifa17/src/model.rs
T
funman300 db743ffd1f 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.
2026-08-21 03:17:57 +00:00

120 lines
3.8 KiB
Rust

//! Serde model of the real FIFA 17 Python profile (`fifa17_profile.json`).
//! Only the fields the importer reasons about are modelled; every field is
//! permissive (`Option`/`default`) because the source is external and unknown
//! fields are ignored. Nothing here is written back.
use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Profile {
#[serde(rename = "personaId", default)]
pub persona_id: i64,
#[serde(rename = "personaName", default)]
pub persona_name: String,
#[serde(rename = "clubName", default)]
pub club_name: String,
#[serde(rename = "clubAbbr", default)]
pub club_abbr: String,
#[serde(default)]
pub coins: i64,
/// Next wire id to issue (issue-then-increment: `fut_store.py` assigns this
/// value then increments). The allocation watermark to preserve.
#[serde(rename = "nextItemId", default)]
pub next_item_id: i64,
#[serde(default)]
pub items: Vec<Item>,
#[serde(default)]
pub squads: Vec<Squad>,
/// Unconsumed pack entitlements (`unopenedPackIds`), seeded into Core as
/// generic entitlements on import.
#[serde(rename = "unopenedPackIds", default)]
pub unopened_pack_ids: Vec<i64>,
}
impl Profile {
pub fn from_json_str(raw: &str) -> Result<Self> {
serde_json::from_str(raw).context("parsing fifa17_profile.json")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Item {
/// The per-INSTANCE wire id (preserved on import).
pub id: i64,
/// The per-DEFINITION versioned id: `resourceId = (version << 24) | assetId`.
#[serde(rename = "resourceId", default)]
pub resource_id: i64,
#[serde(rename = "assetId", default)]
pub asset_id: Option<i64>,
#[serde(rename = "itemType", default)]
pub item_type: String,
#[serde(default)]
pub rareflag: Option<i64>,
#[serde(default)]
pub rating: Option<i64>,
#[serde(rename = "preferredPosition", default)]
pub preferred_position: Option<String>,
#[serde(default)]
pub nation: Option<i64>,
#[serde(default)]
pub teamid: Option<i64>,
#[serde(rename = "leagueId", default)]
pub league_id: Option<i64>,
#[serde(rename = "attributeList", default)]
pub attribute_list: Option<Vec<Attr>>,
/// FIFA `cardsubtypeid` — the consumable family / staff role selector. Absent
/// for player cards; present for consumables and staff.
#[serde(default)]
pub cardsubtypeid: Option<i64>,
/// Consumable ART id (small id), distinct from `resourceId`. Permissive.
#[serde(default)]
pub cardassetid: Option<i64>,
/// Consumable stack size (`amount`). Permissive.
#[serde(default)]
pub amount: Option<i64>,
/// Staff/contract `contract` count. Permissive.
#[serde(default)]
pub contract: Option<i64>,
/// Owned-item lifecycle state (`activeHomeKit` / `activeAwayKit` for kits).
#[serde(rename = "itemState", default)]
pub item_state: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Attr {
pub index: i64,
pub value: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Squad {
#[serde(default)]
pub formation: String,
#[serde(rename = "squadName", default)]
pub squad_name: String,
#[serde(default)]
pub captain: Option<i64>,
/// Opaque staff refs; each is `{ "id": <wire id>, ... }`.
#[serde(default)]
pub manager: Vec<serde_json::Value>,
#[serde(default)]
pub players: Vec<SquadSlot>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SquadSlot {
pub index: i64,
#[serde(rename = "itemData", default)]
pub item_data: SlotItem,
#[serde(rename = "kitNumber", default)]
pub kit_number: Option<i64>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SlotItem {
/// Wire id of the owned instance in this slot; `0` = empty slot.
#[serde(default)]
pub id: i64,
}