09db5413cd
The importer already knows every definition's kind, so it writes the mapping Core needs to correct a club imported before the taxonomy existed. Applied to the real 1989-item club: 20 rows corrected (17 consumables + 3 staff), 1966 players already right, 0 unmatched definitions.
186 lines
7.9 KiB
Rust
186 lines
7.9 KiB
Rust
//! `openfut-import-fifa17` — real FIFA 17 profile import.
|
|
//!
|
|
//! This phase implements the read-only analysis and `--emit-content` (public
|
|
//! content pack + host catalog + private manifest). `--apply` is recognised but
|
|
//! deliberately refuses (and exits nonzero) so no Core DB / identity-store
|
|
//! writes happen before the apply phase lands.
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
use std::collections::BTreeSet;
|
|
use std::path::Path;
|
|
use std::process::ExitCode;
|
|
|
|
use openfut_import_fifa17::apply::{apply, plan_apply, ApplyPaths};
|
|
use openfut_import_fifa17::{
|
|
analyze, emit_content, fingerprint, load_roster, model::Profile, Entities,
|
|
};
|
|
|
|
fn main() -> ExitCode {
|
|
match run() {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
eprintln!("import-fifa17: {e:#}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print_help() {
|
|
eprintln!(
|
|
"openfut-import-fifa17 --profile <fifa17_profile.json> [options]\n\
|
|
\n\
|
|
Read-only analysis of a real FIFA 17 Python profile for a faithful Core\n\
|
|
import, with optional public content emission. Exits nonzero on unsafe blockers.\n\
|
|
\n\
|
|
--profile <path> (required) source fifa17_profile.json\n\
|
|
--roster <path> roster.json for names (default fifa17-recon/data/roster.json)\n\
|
|
--tables <dir> tables dir (default fifa17-recon/data/tables)\n\
|
|
--defer-conflict <rid> explicitly approve deferring a reviewed resourceId conflict (repeatable)\n\
|
|
--emit-content <dir> write the production content pack + host catalog + private manifest\n\
|
|
--apply apply across the identity store + Core (requires --emit-content)\n\
|
|
--allow-deferred-players-for-staging import despite deferred players (STAGED only; never default)\n\
|
|
--core-bin <path> openfut-core binary for the generic import transaction\n\
|
|
--core-db <url> DATABASE_URL for the target Core DB\n\
|
|
--core-data <dir> DATA_DIR base catalog for Core (default data)\n\
|
|
--identity-store <path> openfut-identity JSON store to seed\n"
|
|
);
|
|
}
|
|
|
|
fn run() -> Result<ExitCode> {
|
|
let mut profile_path: Option<String> = None;
|
|
let mut roster_path = "fifa17-recon/data/roster.json".to_string();
|
|
let mut tables_dir = "fifa17-recon/data/tables".to_string();
|
|
let mut emit_dir: Option<String> = None;
|
|
let mut approved: BTreeSet<i64> = BTreeSet::new();
|
|
let mut do_apply = false;
|
|
let mut allow_staging = false;
|
|
let mut core_bin: Option<String> = None;
|
|
let mut core_db: Option<String> = None;
|
|
let mut core_data = "data".to_string();
|
|
let mut identity_store: Option<String> = None;
|
|
|
|
let mut args = std::env::args().skip(1);
|
|
while let Some(a) = args.next() {
|
|
match a.as_str() {
|
|
"--profile" => profile_path = Some(args.next().context("--profile needs a path")?),
|
|
"--roster" => roster_path = args.next().context("--roster needs a path")?,
|
|
"--tables" => tables_dir = args.next().context("--tables needs a dir")?,
|
|
"--emit-content" => emit_dir = Some(args.next().context("--emit-content needs a dir")?),
|
|
"--defer-conflict" => {
|
|
let rid = args.next().context("--defer-conflict needs a resourceId")?;
|
|
approved.insert(
|
|
rid.parse::<i64>()
|
|
.with_context(|| format!("--defer-conflict {rid} is not an integer"))?,
|
|
);
|
|
}
|
|
"--dry-run" => {}
|
|
"--apply" => do_apply = true,
|
|
"--allow-deferred-players-for-staging" => allow_staging = true,
|
|
"--core-bin" => core_bin = Some(args.next().context("--core-bin needs a path")?),
|
|
"--core-db" => core_db = Some(args.next().context("--core-db needs a DATABASE_URL")?),
|
|
"--core-data" => core_data = args.next().context("--core-data needs a dir")?,
|
|
"--identity-store" => {
|
|
identity_store = Some(args.next().context("--identity-store needs a path")?)
|
|
}
|
|
"-h" | "--help" => {
|
|
print_help();
|
|
return Ok(ExitCode::SUCCESS);
|
|
}
|
|
other => bail!("unknown argument: {other} (try --help)"),
|
|
}
|
|
}
|
|
let profile_path = profile_path.context("--profile <path> is required (try --help)")?;
|
|
|
|
let raw =
|
|
std::fs::read(&profile_path).with_context(|| format!("reading profile {profile_path}"))?;
|
|
let profile = Profile::from_json_str(&String::from_utf8_lossy(&raw))?;
|
|
let roster = load_roster(&roster_path)?;
|
|
let entities = Entities::from_tables_dir(&tables_dir)?;
|
|
|
|
let report = analyze(&profile, &roster, &entities, &approved);
|
|
print!("{report}");
|
|
|
|
if let Some(dir) = &emit_dir {
|
|
if report.has_blockers() {
|
|
bail!("not emitting content: unsafe blockers present (resolve them first)");
|
|
}
|
|
let fp = fingerprint(&raw);
|
|
let sum = emit_content(&report, Path::new(dir), &fp)?;
|
|
println!("\nEMITTED (fingerprint {fp})");
|
|
println!(
|
|
" content pack : {} ({} definitions)",
|
|
sum.content_pack.display(),
|
|
sum.definitions
|
|
);
|
|
println!(
|
|
" host catalog : {} ({} entries)",
|
|
sum.host_catalog.display(),
|
|
sum.catalog_entries
|
|
);
|
|
println!(
|
|
" manifest : {} (supported_instances={} deferred_instances={})",
|
|
sum.manifest.display(),
|
|
sum.supported_instances,
|
|
sum.deferred_instances
|
|
);
|
|
println!(
|
|
" reclassify : {} (run `openfut-core reclassify <file>` to correct \
|
|
content_kind on a club imported before the taxonomy existed)",
|
|
sum.reclassify.display()
|
|
);
|
|
println!(
|
|
" non-player : {} definition(s), {} instance(s) (consumable/staff)",
|
|
sum.non_player_definitions, sum.non_player_instances
|
|
);
|
|
}
|
|
|
|
if do_apply {
|
|
let dir = emit_dir
|
|
.as_deref()
|
|
.context("--apply requires --emit-content <dir> (it reads the emitted content pack)")?;
|
|
if report.has_blockers() {
|
|
bail!("not applying: unsafe blockers present (resolve them first)");
|
|
}
|
|
let raw_value: serde_json::Value =
|
|
serde_json::from_slice(&raw).context("parse profile JSON for squad wire")?;
|
|
let fp = fingerprint(&raw);
|
|
let plan = plan_apply(&report, &raw_value, &fp)?;
|
|
let base = Path::new(dir);
|
|
let paths = ApplyPaths {
|
|
core_bin: core_bin
|
|
.context("--apply requires --core-bin <openfut-core binary>")?
|
|
.into(),
|
|
core_db_url: core_db.context("--apply requires --core-db <DATABASE_URL>")?,
|
|
content_pack: base.join("content/fifa17-production-cards.json"),
|
|
data_dir: core_data,
|
|
identity_store: identity_store
|
|
.context("--apply requires --identity-store <path>")?
|
|
.into(),
|
|
request_out: base.join("manifest/fifa17-core-import-request.json"),
|
|
completion_out: base.join("manifest/fifa17-import-completion.json"),
|
|
};
|
|
let outcome = apply(&plan, &paths, allow_staging)?;
|
|
println!(
|
|
"\nAPPLIED (core_outcome={} staging={})",
|
|
outcome.core_outcome, outcome.staging
|
|
);
|
|
println!(
|
|
" identity mappings installed: {} ; watermark: {}",
|
|
outcome.mappings_installed, outcome.watermark
|
|
);
|
|
println!(
|
|
" supported instances: {} ; deferred instances: {}",
|
|
outcome.supported_instances, outcome.deferred_instances
|
|
);
|
|
println!(" request : {}", paths.request_out.display());
|
|
println!(" completion : {}", paths.completion_out.display());
|
|
}
|
|
|
|
if report.has_blockers() {
|
|
eprintln!("import-fifa17: FAILED — unsafe blockers present (see above)");
|
|
Ok(ExitCode::FAILURE)
|
|
} else {
|
|
Ok(ExitCode::SUCCESS)
|
|
}
|
|
}
|