#!/usr/bin/env python3 """Deterministic FIFA 17 base-card seed generator. Reads the committed, evidenced FIFA 17 data and produces: 1. the FIFA 17 card-definition *identity catalog* (openfut-adapter-fifa17 data): CardDefinitionId -> {asset_id, version=0}. `resource_id == asset_id` for version 0. 2. (optional) semantic base CardDefinitions for OpenFUT Core content. Base cards only (version 0). No specials — pool.json proves only base identities. CardDefinitionId is opaque + deterministic: `fifa17_` (per-game namespace). Core treats it as opaque; the FIFA meaning stays in the catalog. Sources (all committed, non-sensitive): fifa17-recon/data/pool.json real player assets (id, rating, pos, nation, league, team, attrs[pace,sho,pas,dri,def,phy], gk) fifa17-recon/data/roster.json names (id -> first/last/common) fifa17-recon/data/tables/{leagues,nations,teams}.json entity id -> name Determinism: output is sorted by asset_id and JSON-stable; running twice on the same source yields identical bytes. `--check` regenerates and diffs against the committed files, exiting non-zero on drift. """ import argparse import hashlib import json import os import sys GENERATOR_VERSION = 1 SCHEMA_VERSION = 1 GAME = "fifa17" RECON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "fifa17-recon") def _load(path): with open(path, encoding="utf-8") as f: return json.load(f) def _table_map(path, id_key, name_key): doc = _load(path) return {row[id_key]: row[name_key] for row in doc["rows"]} def tier(overall): return "gold" if overall >= 75 else "silver" if overall >= 65 else "bronze" def build(recon): pool = _load(os.path.join(recon, "data", "pool.json")) roster = {r["id"]: r for r in _load(os.path.join(recon, "data", "roster.json"))} leagues = _table_map(os.path.join(recon, "data", "tables", "leagues.json"), "leagueid", "leaguename") nations = _table_map(os.path.join(recon, "data", "tables", "nations.json"), "nationid", "nationname") teams = _table_map(os.path.join(recon, "data", "tables", "teams.json"), "teamid", "teamname") catalog_cards = {} definitions = [] skipped = [] seen_assets = set() for p in sorted(pool, key=lambda x: x["id"]): asset = p["id"] if asset in seen_assets: skipped.append((asset, "duplicate_asset_id")) continue seen_assets.add(asset) if asset > 0x00FFFFFF: skipped.append((asset, "asset_id_exceeds_24_bits")) continue card_id = "fifa17_%d" % asset # Catalog entry needs only the asset id (identity); always emitted. catalog_cards[card_id] = {"asset_id": asset, "version": 0} # Semantic definition needs a name + resolvable entities; skip+report if not. r = roster.get(asset) name = None if r: common = (r.get("common") or "").strip() name = common or (" ".join(x for x in [r.get("first", ""), r.get("last", "")] if x)).strip() if not name: skipped.append((asset, "no_roster_name")) continue nation = nations.get(p["nation"]) league = leagues.get(p["league"]) club = teams.get(p["team"]) if not nation or not league or not club: missing = [k for k, v in (("nation", nation), ("league", league), ("club", club)) if not v] skipped.append((asset, "unresolved_entity:" + ",".join(missing))) continue a = p["attrs"] # [pace, shooting, passing, dribbling, defending, physical] definitions.append({ "id": card_id, "name": name, "overall": p["rating"], "position": p["pos"], "nation": nation, "league": league, "club": club, "pace": a[0], "shooting": a[1], "passing": a[2], "dribbling": a[3], "defending": a[4], "physical": a[5], "rarity": tier(p["rating"]), "image_path": None, }) catalog = { "schema_version": SCHEMA_VERSION, "game": GAME, "_provenance": { "generator": "scripts/seed_fifa17_cards.py", "generator_version": GENERATOR_VERSION, "sources": ["fifa17-recon/data/pool.json"], "base_cards_only": True, }, "cards": {cid: catalog_cards[cid] for cid in sorted(catalog_cards)}, } definitions.sort(key=lambda d: d["id"]) return catalog, definitions, skipped, len(pool) def dumps(obj): return json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=False) + "\n" def main(): ap = argparse.ArgumentParser() ap.add_argument("--recon", default=os.path.normpath(RECON)) ap.add_argument("--catalog-out", required=True) ap.add_argument("--defs-out", help="optional semantic CardDefinition JSON (Core content)") ap.add_argument("--check", action="store_true", help="fail if committed output would change") args = ap.parse_args() catalog, definitions, skipped, pool_rows = build(args.recon) catalog_bytes = dumps(catalog) defs_bytes = dumps(definitions) reasons = {} for _, why in skipped: reasons[why.split(":")[0]] = reasons.get(why.split(":")[0], 0) + 1 report = { "pool_rows": pool_rows, "catalog_entries": len(catalog["cards"]), "definitions_seeded": len(definitions), "skipped_definitions": len(skipped), "skip_reasons": reasons, "catalog_sha256": hashlib.sha256(catalog_bytes.encode()).hexdigest()[:16], } print(json.dumps(report, indent=2), file=sys.stderr) if args.check: drift = False for path, want in [(args.catalog_out, catalog_bytes)] + ( [(args.defs_out, defs_bytes)] if args.defs_out else [] ): have = open(path, encoding="utf-8").read() if os.path.exists(path) else None if have != want: print("DRIFT: %s is stale (regenerate)" % path, file=sys.stderr) drift = True sys.exit(1 if drift else 0) with open(args.catalog_out, "w", encoding="utf-8") as f: f.write(catalog_bytes) if args.defs_out: with open(args.defs_out, "w", encoding="utf-8") as f: f.write(defs_bytes) print("wrote %s (%d cards)" % (args.catalog_out, len(catalog["cards"])), file=sys.stderr) if __name__ == "__main__": main()