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).
This commit is contained in:
salh
2026-06-15 16:03:43 +03:00
parent 0d7a528395
commit c2e2fbfbbc
27 changed files with 620 additions and 291 deletions
+15 -55
View File
@@ -3,54 +3,11 @@ from __future__ import annotations
import argparse
import json
import struct
import sys
from pathlib import Path
MAGIC_EXT = {
"FHM ": ".fhm", "NTXR": ".ntxr", "NDXR": ".ndxr", "NSXR": ".nsxr",
"MDLP": ".mdlp", "PLAD": ".plad", "MATE": ".mate", "NFIC": ".nfic",
"NFH\x00": ".nfh", "CAPT": ".capt", "Scen": ".scen", "ACE6": ".ace6",
"RIFF": ".wav", "SWG\x00": ".swg",
}
def parse_fhm(blob: bytes):
"""Return list of (index, offset, child_bytes) or None if not an FHM."""
if len(blob) < 0x1C or blob[:4] != b"FHM ":
return None
count = struct.unpack_from(">I", blob, 0x10)[0]
if count == 0 or count > 100000:
return []
offs_base = 0x14
size_base = offs_base + count * 4
if size_base + count * 4 > len(blob):
return []
offsets = [struct.unpack_from(">I", blob, offs_base + i * 4)[0] for i in range(count)]
sizes = [struct.unpack_from(">I", blob, size_base + i * 4)[0] for i in range(count)]
out = []
for i, (off, sz) in enumerate(zip(offsets, sizes)):
if off == 0 or off >= len(blob):
continue
end = off + sz
if end > len(blob) or end <= off:
nxt = offsets[i + 1] if i + 1 < count else len(blob)
end = min(nxt, len(blob))
if end > off:
out.append((i, off, blob[off:end]))
return out
def magic_of(blob: bytes) -> str:
return blob[:4].decode("latin-1") if len(blob) >= 4 else ""
def ext_for(blob: bytes) -> str:
return MAGIC_EXT.get(magic_of(blob), ".bin")
def safe_tag(magic: str) -> str:
return "".join(c if c.isalnum() else "_" for c in magic) or "raw"
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]:
@@ -59,15 +16,18 @@ def unpack(blob: bytes, out_dir: Path, root: Path, depth: int, max_depth: int) -
return []
recs = []
out_dir.mkdir(parents=True, exist_ok=True)
for idx, off, child in children:
magic = magic_of(child)
name = f"{idx:04d}_{safe_tag(magic)}{ext_for(child)}"
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)
rec = {"index": idx, "offset": off, "size": len(child), "magic": magic,
"path": str(path.relative_to(root)).replace("\\", "/")}
if depth < max_depth and child[:4] == b"FHM ":
nested = unpack(child, out_dir / f"{idx:04d}_FHM", root, depth + 1, max_depth)
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)
@@ -97,7 +57,7 @@ def main() -> int:
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)}"
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("\\", "/")})