Files
AC6_recomp/tools/unpack_ac6_fhm.py
T
salh c2e2fbfbbc Add 60fps cutscene clamp for in-engine cinematics
Suspend the FPS unlock while a demo-manager Exec (DD sub_82184460 / EM sub_821856F8) ticks, so the frame-locked IngameCinematics Sequencer plays at native ~30fps instead of double speed. Adds ac6_cutscene_clamp CVar (default on).
2026-06-15 16:03:43 +03:00

91 lines
3.2 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ac6_fhm import ext_for_blob, magic_of, parse_fhm, safe_tag
def unpack(blob: bytes, out_dir: Path, root: Path, depth: int, max_depth: int) -> list[dict]:
children = parse_fhm(blob)
if children is None:
return []
recs = []
out_dir.mkdir(parents=True, exist_ok=True)
for child in children:
magic = child.magic
name = f"{child.index:04d}_{safe_tag(magic)}{ext_for_blob(child.data)}"
path = out_dir / name
path.write_bytes(child.data)
rec = {"index": child.index, "offset": child.offset,
"declared_size": child.declared_size, "size": child.size,
"magic": magic, "path": str(path.relative_to(root)).replace("\\", "/")}
if child.notes:
rec["parser_notes"] = child.notes
if depth < max_depth and child.data[:4] == b"FHM ":
nested = unpack(child.data, out_dir / f"{child.index:04d}_FHM", root, depth + 1, max_depth)
if nested:
rec["children"] = nested
recs.append(rec)
return recs
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--input", type=Path, default=Path("out") / "ac6_pac_extracted_raw" / "files")
ap.add_argument("--output", type=Path, default=Path("out") / "ac6_fhm_unpacked")
ap.add_argument("--max-depth", type=int, default=8)
args = ap.parse_args()
inp = args.input.resolve()
out_root = args.output.resolve()
out_root.mkdir(parents=True, exist_ok=True)
if inp.is_file():
sources = [inp]
else:
sources = sorted(inp.rglob("*.bin"))
manifest = []
fhm_count = leaf_count = 0
for src in sources:
blob = src.read_bytes()
stem = src.stem.split(".")[0]
if blob[:4] != b"FHM ":
# Top-level non-FHM (raw entry): copy through as a leaf.
dst = out_root / f"{stem}{ext_for_blob(blob)}"
dst.write_bytes(blob)
manifest.append({"source": src.name, "kind": "leaf", "magic": magic_of(blob),
"path": str(dst.relative_to(out_root)).replace("\\", "/")})
leaf_count += 1
continue
cdir = out_root / stem
recs = unpack(blob, cdir, out_root, 0, args.max_depth)
manifest.append({"source": src.name, "kind": "fhm", "child_count": len(recs),
"children": recs})
fhm_count += 1
def count_leaves(recs):
n = 0
for r in recs:
if "children" in r:
n += count_leaves(r["children"])
else:
n += 1
return n
total_leaves = leaf_count + sum(
count_leaves(m["children"]) for m in manifest if m["kind"] == "fhm")
(out_root / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(json.dumps({"sources": len(sources), "fhm_containers": fhm_count,
"total_leaves": total_leaves, "output": str(out_root)}, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())