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.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user