#!/usr/bin/env python3 """ profile-exporter/export_profile.py Reads FIFA 23 local profile/settings files and exports what can be read. FIFA 23 uses the Frostbite FBCHUNKS container format for saves. This exporter handles: - fifasetup.ini → plain text, fully readable - ProfileOptions → FBCHUNKS binary (partial parse of header + string scan) - Settings* → FBCHUNKS binary (contains "Personal Settings 1" label) - filesystemcache/ → JSON files, fully readable - anadius.cfg → Valve KeyValues format, contains persona info Usage: python3 export_profile.py [--output out.json] """ import struct import json import re import sys from pathlib import Path from datetime import datetime STEAM_PFX = Path.home() / ".local/share/Steam/steamapps/compatdata/3887260930/pfx" UMU_PFX = Path.home() / "Games/umu/fifa23-tools/pfx" GAME_DIR = Path("/mnt/games/FIFA 23") DOCS_PATHS = [ STEAM_PFX / "drive_c/users/steamuser/Documents/FIFA 23", UMU_PFX / "drive_c/users/steamuser/Documents/FIFA 23", ] def find_docs() -> Path | None: for p in DOCS_PATHS: if p.exists(): return p return None def read_ini(path: Path) -> dict: out = {} for line in path.read_text(errors="replace").splitlines(): line = line.strip() if "=" in line and not line.startswith(";") and not line.startswith("#"): k, _, v = line.partition("=") out[k.strip()] = v.strip().strip('"') return out def parse_fbchunks_header(data: bytes) -> dict: """Extract what we can from the FBCHUNKS container without a full format spec.""" if not data[:8] == b"FBCHUNKS": return {"error": "not FBCHUNKS"} info: dict = {"magic": "FBCHUNKS", "size_bytes": len(data)} # The label string appears at offset 0x12 in Settings files # Try to pull the first null-terminated ASCII string after offset 8 try: label_start = 0x12 end = data.index(b"\x00", label_start) label = data[label_start:end].decode("ascii", errors="ignore") if label: info["label"] = label except (ValueError, IndexError): pass # String-scan for recognisable names/values strings = re.findall(rb"[ -~]{6,}", data) info["readable_strings_sample"] = [s.decode("ascii") for s in strings[:40]] return info def read_filesystem_cache(docs: Path) -> dict: cache_dir = docs / "filesystemcache" out = {} if not cache_dir.exists(): return out for f in cache_dir.rglob("*"): if f.is_file(): try: out[str(f.relative_to(cache_dir))] = json.loads(f.read_text()) except Exception: out[str(f.relative_to(cache_dir))] = f.read_text(errors="replace") return out def parse_anadius_cfg(path: Path) -> dict: """Parse Valve KeyValues (VDF) format anadius.cfg for persona/user info.""" out: dict = {} current_section: str | None = None for line in path.read_text(errors="replace").splitlines(): line = line.strip() if line.startswith('"') and line.endswith('"') and "{" not in line: # lone quoted string — section name coming next current_section = line.strip('"') elif line.startswith('"') and "\t" in line: parts = [p.strip().strip('"') for p in line.split("\t") if p.strip()] if len(parts) == 2: key, val = parts[0], parts[1] section_key = f"{current_section}.{key}" if current_section else key out[section_key] = val return out def export_settings_files(docs: Path) -> list: settings_dir = docs / "settings" entries = [] if not settings_dir.exists(): return entries for f in sorted(settings_dir.iterdir()): if f.is_file(): data = f.read_bytes() entry: dict = { "file": f.name, "size_bytes": len(data), "mtime": datetime.fromtimestamp(f.stat().st_mtime).isoformat(), } if data[:8] == b"FBCHUNKS": entry["format"] = "FBCHUNKS" entry["header"] = parse_fbchunks_header(data) elif data and all(0x20 <= b < 0x7F or b in (0x09, 0x0A, 0x0D) for b in data[:256]): entry["format"] = "text" entry["content"] = f.read_text(errors="replace")[:2000] else: entry["format"] = "binary" entries.append(entry) return entries def main(): out_path = None if "--output" in sys.argv: idx = sys.argv.index("--output") out_path = Path(sys.argv[idx + 1]) docs = find_docs() result: dict = { "exported_at": datetime.utcnow().isoformat() + "Z", "docs_path": str(docs) if docs else None, } # fifasetup.ini if docs: ini_path = docs / "fifasetup.ini" if ini_path.exists(): result["fifasetup_ini"] = read_ini(ini_path) # anadius.cfg (persona / user info) anadius_cfg = GAME_DIR / "anadius.cfg" if anadius_cfg.exists(): result["anadius_cfg"] = parse_anadius_cfg(anadius_cfg) # Settings/ save files if docs: result["settings_files"] = export_settings_files(docs) # filesystemcache/ if docs: result["filesystem_cache"] = read_filesystem_cache(docs) output = json.dumps(result, indent=2, ensure_ascii=False) if out_path: out_path.write_text(output) print(f"Written to {out_path}") else: print(output) if __name__ == "__main__": main()