This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/miscbin_names.py
Sylpheed RE agent fa5a51ac78 re: name-resolve all 40 MiscBin entries, and demonstrate the REGN<->MCOL pairing
The names live outside MiscBin: they are the MapPath / MapMesh /
CollisionMeshes field values of the per-stage StageResource object (IDXD schema
3c9ae32e, in every GP_MAIN_GAME_<lang>.pak), and each hashes with the ordinary
pak name_hash straight to a TOC entry.  40/40 resolve, no collisions -- the 11
REGN as <stem>.rgn, the 11 MCOL as <stem>.col, and the 18 remaining blobs as
CollisionSet_S01..S16 / _Tutorial / _test.bin.  The .pe string table at 651540
was the way in: MapMesh and MapPath sit adjacent there.

This upgrades the pairing claim.  The first section of mcol-collision.md could
only say REGN and MCOL had matching *distributions* of bbox and cell size, and
flagged that as not an object-to-object link.  A phase record names one .rgn and
one .col, and all 11/11 pairs share a stem and agree exactly on both.

The names also check the format work from outside it: mapmesh_box_500km.col is
the object decoded here as 8 vertices and 12 triangles spanning exactly
+-250000, and its name says that cube is 500 km across -- so one world unit is
one metre, and a wrong stride could not have produced a box that measures what
its own filename claims.  70 of the 87 phases use it: most stages' only
collision is the arena wall, and _AsteroidVolume_ names the rest.

Still open: the 18 CollisionSet_*.bin are named but not decoded (all exactly
1675148 bytes), and CMapColliderBridge in the RTTI names the runtime consumer
without following it into the code.
2026-08-26 09:21:00 +00:00

141 lines
5.1 KiB
Python
Executable File

#!/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_<lang>.pak`), and each one
hashes with the ordinary pak `name_hash` to a `MiscBin` TOC entry.
./miscbin_names.py <extract root> # the table
./miscbin_names.py <extract root> --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)