feat(import): identity import API + FIFA17 real-profile dry-run importer
openfut-identity: - insert_existing_mapping(game,kind,core_id,external_id): preserve an existing external wire id instead of minting; idempotent for an identical mapping, rejects conflicting forward/reverse with IdError::Conflict, persists atomically. - persisted per-scope allocator watermark (set_watermark/watermark_for) so a future mint continues past the source high-water even across burned-id gaps; next id = max(base_floor, live_max+1, watermark). Backward-compatible on-disk format (legacy bare [Row] still loads). +4 tests (10 total). openfut-import-fifa17 (new): read-only dry-run analysis of a real FIFA17 Python profile for a faithful Core import. Enforces disjoint item-class balance; proposes profile-derived CardDefinitions keyed fifa17_<resourceId> (base vs versioned never collapse) with a resourceId-group consistency gate (hard-fail on disagreement, never pick a winner) and honest buildability (roster name + version formula + metadata, never fabricated); plans owned-instance identity (preserve Python wire ids, preserve nextItemId watermark); checks active-squad coverage. --apply/--emit-content refuse to write in this phase. 11 tests. Real profile (33068179/CAGE) dry-run: 1982 items balance (1962 players + 17 consumables + 3 staff); 1681 supported defs (155 base + 1535 versioned), 9 NoName unsupported, 1 hard conflict (resourceId 169193: one of 4 copies has a divergent nation/team/league); 1949 importable player instances, watermark 100004617 -> first new alloc 100004617; active squad f433 fully supported. fmt + clippy -D warnings clean.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
//! 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>,
|
||||
}
|
||||
|
||||
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>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
Reference in New Issue
Block a user