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:
funman300
2026-08-12 19:23:19 +00:00
parent 63f02c4fb1
commit a51947562c
8 changed files with 1380 additions and 9 deletions
+99
View File
@@ -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,
}