Two follow-ups on yesterday's^Wthis morning's CollisionSet write-up.
1. The _cmesh <-> render-model link, which I recorded as UNTESTED because
matching stems against .xbg object names covered 4 of 158. The disc keeps
only one build manifest, so that corpus was never going to answer it. The
right corpus is the GameResourceID field of the DefTables / GP_MAIN_GAME
records -- 480 distinct values. Against those, with a control that shuffles
the characters of each stem:
ship/mob stems prefixed by a real resource id 108/112 = 96.4%
same stems, characters shuffled (control) 0/112 = 0.0%
asteroid stems prefixed (expected none) 0/46
So a CollisionSet entry is <GameResourceID>[_<part>]_cmesh. The 0/46 on
asteroids matters as much as the 108/112: a test that fired on everything
would be the bound-check hazard again.
2. The world unit. Sweeping every pak for a name carrying a kilometre figure
returns mapmesh_box_500km.col/.rgn and nothing else -- 162 references, all to
that one pair. The reading rests on a single filename with no corroborating
instance anywhere in the data, so no static test can settle it; marking it
blocked on the oracle rather than leaving it as an open static question.
My objection's premise did survive: rou_e010 is a real GameResourceID and
e010_ADAN_Attacker_S is in the stage tables, so the 133-unit mesh does belong
to a craft the game calls an attacker. Whether the trailing _S means "small"
is a further guess (there are _EX4 / _HF / _HF_Wayne variants), so it stays
suggestive rather than evidence.
196 lines
7.2 KiB
Python
Executable File
196 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Decode `CollisionSet_*.bin` from `hidden/MiscBin.pak`.
|
|
|
|
All 18 of them are byte-identical, so the per-stage naming is nominal: there is
|
|
one shared library of 158 named per-object collision meshes (ships, stations,
|
|
asteroids), separate from the per-map `MCOL` hull.
|
|
|
|
./collisionset.py list <MiscBin.pak> # the 158 meshes
|
|
./collisionset.py verify <MiscBin.pak> # the checks in the doc
|
|
./collisionset.py link <extract root> # cmesh names <-> GameResourceID
|
|
./collisionset.py obj <MiscBin.pak> <name> <out.obj>
|
|
|
|
A record is
|
|
|
|
u32 size bytes that follow this word's own 8-byte prefix
|
|
u32 name_len 16, 24 or 32 -- the name field, padded to a multiple of 8
|
|
char name[name_len]
|
|
u32 vertex_count
|
|
u32 triangle_count
|
|
f32 vertex[3] x vertex_count
|
|
u32 index[3] x triangle_count -- note u32, not the u16 MCOL uses
|
|
|
|
and the next record begins at `offset + 8 + size`.
|
|
"""
|
|
|
|
import collections
|
|
import math
|
|
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
|
|
|
|
|
|
def meshes(pak_path, which="CollisionSet_S01.bin"):
|
|
"""[(name, vertices, triangles)] for one CollisionSet blob."""
|
|
blob = dict(pak_entries(pak_path))[name_hash(which)]
|
|
u32 = lambda o: struct.unpack_from(">I", blob, o)[0]
|
|
out, p = [], 0
|
|
while p < len(blob):
|
|
size, nlen = u32(p), u32(p + 4)
|
|
name = blob[p + 8:p + 8 + nlen].split(b"\0")[0].decode("latin-1")
|
|
nv, nt = u32(p + 8 + nlen), u32(p + 12 + nlen)
|
|
vb = p + 16 + nlen
|
|
tb = vb + 12 * nv
|
|
verts = [struct.unpack_from(">fff", blob, vb + 12 * i) for i in range(nv)]
|
|
tris = [struct.unpack_from(">III", blob, tb + 12 * i) for i in range(nt)]
|
|
expect = 16 + nlen + 12 * nv + 12 * nt - 8
|
|
out.append((name, verts, tris, size == expect))
|
|
p += 8 + size
|
|
return out, len(blob), p
|
|
|
|
|
|
def cmd_list(pak):
|
|
ms, _, _ = meshes(pak)
|
|
print(f"{'name':>28} {'verts':>6} {'tris':>6} {'bbox diagonal':>14}")
|
|
for name, v, t, _ in ms:
|
|
lo = [min(q[k] for q in v) for k in range(3)]
|
|
hi = [max(q[k] for q in v) for k in range(3)]
|
|
print(f"{name:>28} {len(v):6d} {len(t):6d} {math.dist(lo, hi):14.1f}")
|
|
print(f"\n{len(ms)} meshes, {sum(len(t) for _, _, t, _ in ms)} triangles")
|
|
return True
|
|
|
|
|
|
def cmd_verify(pak):
|
|
blobs = dict(pak_entries(pak))
|
|
tags = ["test", "Tutorial"] + ["S%02d" % i for i in range(1, 17)]
|
|
distinct = {blobs[name_hash(f"CollisionSet_{t}.bin")] for t in tags}
|
|
print(f"CollisionSet blobs: {len(tags)} distinct contents: {len(distinct)}")
|
|
|
|
ms, total, consumed = meshes(pak)
|
|
print(f"records: {len(ms)} bytes consumed: {consumed}/{total}"
|
|
f" ({'EXACT' if consumed == total else 'MISMATCH'})")
|
|
print(f"size word == 16 + name_len + 12*nv + 12*nt - 8 : "
|
|
f"{sum(ok for _, _, _, ok in ms)}/{len(ms)}")
|
|
|
|
in_range = sum(all(max(t) < len(v) for t in tris) if tris else 1
|
|
for _, v, tris, _ in ms)
|
|
print(f"every triangle index < vertex count : {in_range}/{len(ms)}")
|
|
|
|
closed = e2 = etot = 0
|
|
for _, v, tris, _ in ms:
|
|
ec = collections.Counter()
|
|
for t in tris:
|
|
for k in range(3):
|
|
a, b = t[k], t[(k + 1) % 3]
|
|
ec[(min(a, b), max(a, b))] += 1
|
|
etot += len(ec)
|
|
e2 += sum(1 for n in ec.values() if n == 2)
|
|
closed += all(n == 2 for n in ec.values())
|
|
print(f"edges shared by exactly two triangles : "
|
|
f"{e2}/{etot} = {100*e2/etot:.2f}%")
|
|
print(f"fully manifold meshes : {closed}/{len(ms)}")
|
|
return (len(distinct) == 1 and consumed == total
|
|
and all(ok for _, _, _, ok in ms) and in_range == len(ms))
|
|
|
|
|
|
def cmd_obj(pak, want, out):
|
|
for name, v, tris, _ in meshes(pak)[0]:
|
|
if name != want:
|
|
continue
|
|
with open(out, "w") as fh:
|
|
fh.write(f"# CollisionSet {name} -- {len(v)} vertices, {len(tris)} triangles\n")
|
|
for q in v:
|
|
fh.write("v %.4f %.4f %.4f\n" % q)
|
|
for t in tris:
|
|
fh.write(f"f {t[0]+1} {t[1]+1} {t[2]+1}\n")
|
|
print(f"{out}: {len(v)} vertices, {len(tris)} triangles")
|
|
return True
|
|
raise SystemExit(f"no mesh named {want!r}")
|
|
|
|
|
|
def cmd_link(root):
|
|
"""Are the mesh names the game's own resource ids? With a control."""
|
|
import random
|
|
import re
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from unitgroup import tag_hash
|
|
|
|
grid = tag_hash("GameResourceID")
|
|
ids = set()
|
|
for pak in (os.path.join(root, "hidden", "DefTables.pak"),
|
|
os.path.join(root, "dat", "GP_MAIN_GAME_E.pak")):
|
|
for _, blob in pak_entries(pak):
|
|
p = _idxd(blob)
|
|
if not p:
|
|
continue
|
|
for key, _, voff in p["fields"]:
|
|
if key == grid:
|
|
v = _pool(p, voff)
|
|
if v:
|
|
ids.add(v)
|
|
|
|
stems = [re.sub(r"_(cmesh|c)$", "", n)
|
|
for n, _, _, _ in meshes(os.path.join(root, "hidden", "MiscBin.pak"))[0]]
|
|
ship = [s for s in stems if s[:4] in ("rou_", "rob_", "mob_")]
|
|
rock = [s for s in stems if s not in ship]
|
|
|
|
pre = sum(any(s.startswith(i) for i in ids) for s in ship)
|
|
exact = sum(s in ids for s in ship)
|
|
random.seed(3)
|
|
ctl = 0
|
|
for s in ship:
|
|
t = list(s)
|
|
random.shuffle(t)
|
|
ctl += any("".join(t).startswith(i) for i in ids)
|
|
|
|
print(f"distinct GameResourceID values : {len(ids)}")
|
|
print(f"ship/mob stems prefixed by a resource id: {pre}/{len(ship)} "
|
|
f"= {100*pre/len(ship):.1f}% (exact match {exact})")
|
|
print(f"same stems, characters shuffled (control): {ctl}/{len(ship)} "
|
|
f"= {100*ctl/len(ship):.1f}%")
|
|
print(f"asteroid stems prefixed (expected none) : "
|
|
f"{sum(any(s.startswith(i) for i in ids) for s in rock)}/{len(rock)}")
|
|
return pre / len(ship) > 0.9 and ctl == 0
|
|
|
|
|
|
def _idxd(b):
|
|
if b[:4] != b"IDXD" or len(b) < 0x10:
|
|
return None
|
|
n = struct.unpack_from(">I", b, 4)[0]
|
|
if not 0 < n <= 200_000:
|
|
return None
|
|
recs, o = [(None,) + struct.unpack_from(">III", b, 0x0C)], 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 = struct.unpack_from(">I", b, o)[0]
|
|
o += 4
|
|
if o + 12 * m + 4 > len(b):
|
|
return None
|
|
return dict(recs=recs,
|
|
fields=[struct.unpack_from(">III", b, o + 12 * i) for i in range(m)],
|
|
pool=o + 12 * m + 4, raw=b)
|
|
|
|
|
|
def _pool(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")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 3:
|
|
raise SystemExit(__doc__)
|
|
cmd, pak = sys.argv[1], sys.argv[2]
|
|
fn = {"list": cmd_list, "verify": cmd_verify, "link": cmd_link}.get(cmd)
|
|
ok = cmd_obj(pak, sys.argv[3], sys.argv[4]) if cmd == "obj" else fn(pak)
|
|
print("\nPASS" if ok else "\nFAIL")
|
|
sys.exit(0 if ok else 1)
|