#!/usr/bin/env python3 """Resolve every `hidden/MiscBin.pak` entry to its on-disc name. The names are not in `MiscBin` itself -- they are the values of the `MapPath`, `MapMesh` and `CollisionMeshes` fields of the per-stage `StageResource` objects (IDXD schema `3c9ae32e`, in every `dat/GP_MAIN_GAME_.pak`), and each one hashes with the ordinary pak `name_hash` to a `MiscBin` TOC entry. ./miscbin_names.py # the table ./miscbin_names.py --pairs # check each .rgn/.col pair `MapPath` names the `REGN` navigation mesh and `MapMesh` the `MCOL` collision mesh of one *phase* of one stage, so the pair is what links the two formats -- an object-to-object link, not the matching-header-distributions argument that `structures/mcol-collision.md` had to settle for before. """ import collections import glob import os import struct import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from regn_decode import pak_entries from unitgroup import name_hash, tag_hash SCHEMA = 0x3C9AE32E KEYS = ("MapPath", "MapMesh", "CollisionMeshes") TAGS = {tag_hash(k): k for k in KEYS} def be32(b, o): return struct.unpack_from(">I", b, o)[0] def parse_idxd(b): """Records and fields of an IDXD object; None if it is not one.""" if b[:4] != b"IDXD" or len(b) < 0x10: return None n = be32(b, 4) if not 0 < n <= 200_000: return None recs = [(None,) + struct.unpack_from(">III", b, 0x0C)] o = 0x18 if o + 16 * (n - 1) + 4 > len(b): return None for _ in range(n - 1): recs.append(struct.unpack_from(">IIII", b, o)) o += 16 m = be32(b, o) o += 4 if o + 12 * m + 4 > len(b): return None fields = [struct.unpack_from(">III", b, o + 12 * i) for i in range(m)] return dict(schema=be32(b, 8), recs=recs, fields=fields, pool=o + 12 * m + 4, raw=b) def pool_str(p, off): b, base = p["raw"], p["pool"] if base + off >= len(b): return None return b[base + off:b.index(b"\0", base + off)].decode("latin-1") def stage_phases(root): """(phase record name, {key: value}) for every stage-resource phase.""" out = [] for pak in sorted(glob.glob(os.path.join(root, "dat", "GP_MAIN_GAME_*.pak"))): for _, blob in pak_entries(pak): p = parse_idxd(blob) if not p or p["schema"] != SCHEMA: continue for _, noff, fb, fe in p["recs"]: d = {TAGS[k]: pool_str(p, vo) for k, _, vo in p["fields"][fb:fe] if k in TAGS} if d: out.append((pool_str(p, noff) if noff is not None else None, d)) return out def resolve(root): misc = dict(pak_entries(os.path.join(root, "hidden", "MiscBin.pak"))) seen = collections.OrderedDict() for _, d in stage_phases(root): for k in KEYS: if d.get(k): seen.setdefault(d[k], k) table = [] for nm, kind in seen.items(): h = name_hash(nm) blob = misc.get(h) table.append((nm, h, kind, blob[:4].decode("latin-1") if blob else None)) return misc, table def cmd_table(root): misc, table = resolve(root) print(f"{'name':>30} {'name_hash':>10} {'magic':>5} {'field':>15}") for nm, h, kind, magic in sorted(table, key=lambda r: (r[3] or "", r[0])): print(f"{nm:>30} {h:10x} {magic or ' --':>5} {kind:>15}") got = {h for _, h, _, m in table if m} print(f"\nMiscBin entries {len(misc)} resolved {len(got)} " f"unresolved {len(set(misc) - got)}") return len(set(misc) - got) == 0 def cmd_pairs(root): """Each phase names a .rgn and a .col; check they describe the same volume.""" misc, _ = resolve(root) f32 = lambda b, o: struct.unpack_from(">f", b, o)[0] u16 = lambda b, o: struct.unpack_from(">H", b, o)[0] pairs = collections.OrderedDict() for _, d in stage_phases(root): if d.get("MapPath") and d.get("MapMesh"): pairs.setdefault((d["MapPath"], d["MapMesh"]), 0) pairs[(d["MapPath"], d["MapMesh"])] += 1 ok = 0 print(f"{'stem':>30} {'phases':>7} {'bbox':>6} {'cell':>6} verts/tris") for (rgn, col), n in pairs.items(): R, C = misc[name_hash(rgn)], misc[name_hash(col)] stem = rgn.rsplit(".", 1)[0] == col.rsplit(".", 1)[0] bb = all(f32(R, 0x10 + 4 * i) == f32(C, 0x10 + 4 * i) and f32(R, 0x20 + 4 * i) == f32(C, 0x20 + 4 * i) for i in range(3)) cc = all(f32(R, 0x40 + 4 * i) == f32(C, 0x40 + 4 * i) for i in range(3)) ok += stem and bb and cc print(f"{rgn.rsplit('.', 1)[0]:>30} {n:7d} {'yes' if bb else 'NO':>6} " f"{'yes' if cc else 'NO':>6} {u16(C, 0x50)}/{u16(C, 0x52)}") print(f"\npairs agreeing on stem, bbox and cell size: {ok}/{len(pairs)}") return ok == len(pairs) if __name__ == "__main__": if len(sys.argv) < 2: raise SystemExit(__doc__) root = sys.argv[1] good = cmd_pairs(root) if "--pairs" in sys.argv else cmd_table(root) print("\nPASS" if good else "\nFAIL") sys.exit(0 if good else 1)