Files
funman300 1fb664710a docs: add FLE bridge direction pivot and squad-injection test tooling
Adds the app-centric FLE bridge direction (direction.md) replacing the
Blaze-backend route as primary plan, plus a corrected foundational test
procedure and snapshot/diff/apply tooling for reverse-engineering FLE's
Freeze Lineup write mechanism. Also picks up prior untracked research docs
(status-review, fut-integration-options, fifa23-startup-flow, track-c) and
existing capture/export tooling that hadn't been committed yet.
2026-06-30 13:19:27 -07:00

171 lines
5.5 KiB
Python

#!/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()