The unexplained ~0.75 ratio left at the end of the last iteration was my own
stride. I had read the block as 12-byte points because REGN's vertex section
is 12 bytes, and never checked it: len(0x5C) is not a multiple of 12 in 5 of
the 11 objects, so that stride was never arithmetically possible.
At stride 16 the relation is exact in 11/11 -- max u16 == len(0x5C)/16 - 1 --
and the record reads as {centre f32[3], radius f32}. Powered test, since a
u16 is reached through a specific grid cell: the sphere it names reaches that
cell in 18 559/18 577 = 99.90%, against a 12.02% random-sphere control. Both
fields carry signal (centre alone 26.75%, radius shuffled 70.19%).
The converse -- is the list *exactly* the intersecting set? -- is 0.38%, which
is the expected direction: a bounding sphere is conservative, so membership
implies overlap but not the reverse. The tighter geometry is in 0x54/0x58,
still undecoded. 18 entries (0.10%) go the wrong way and are recorded as open.
tools/re-capture/regn_decode.py is copied unchanged from auto/regn-reader so
the probe's POF0 reader is the known-good one rather than a second copy.
144 lines
4.9 KiB
Python
Executable File
144 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Check the `MCOL` decode in `docs/re/structures/mcol-collision.md`.
|
|
|
|
`MCOL` (`hidden/MiscBin.pak`, 11 objects) shares `REGN`'s container, so the
|
|
`POF0` reader is imported from `regn_decode.py` rather than duplicated.
|
|
|
|
./mcol_probe.py stride <MiscBin.pak> # the 0x5C block is stride 16, not 12
|
|
./mcol_probe.py cells <MiscBin.pak> # the u16s name spheres in their cell
|
|
./mcol_probe.py verify <MiscBin.pak> # both, with pass/fail
|
|
|
|
The `cells` check is the powered one: a `u16` is reached *through* a specific
|
|
grid cell, so the sphere it names must reach that cell. A bound-check ("is it
|
|
a valid index?") cannot fail here and is deliberately not used -- see the
|
|
hazard section of the doc.
|
|
"""
|
|
|
|
import math
|
|
import os
|
|
import random
|
|
import struct
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from regn_decode import pak_entries, pof0_pointer_slots, FIXUP_BASE
|
|
|
|
SPHERE = 16 # stride of the 0x5C record: centre f32[3] then radius f32
|
|
|
|
|
|
def u32(b, o): return struct.unpack_from(">I", b, o)[0]
|
|
def f32(b, o): return struct.unpack_from(">f", b, o)[0]
|
|
def ptr(b, o): return u32(b, o) + FIXUP_BASE
|
|
|
|
|
|
def objects(pak):
|
|
return [(h, b) for h, b in pak_entries(pak) if b[:4] == b"MCOL"]
|
|
|
|
|
|
class Mcol:
|
|
def __init__(self, blob):
|
|
self.b = blob
|
|
self.p54, self.p58, self.p5c, self.p74 = (ptr(blob, o)
|
|
for o in (0x54, 0x58, 0x5C, 0x74))
|
|
self.sphere_bytes = self.p54 - self.p5c
|
|
self.n = self.sphere_bytes // SPHERE
|
|
self.bbox_min = [f32(blob, 0x10 + 4 * i) for i in range(3)]
|
|
self.cell_size = [f32(blob, 0x40 + 4 * i) for i in range(3)]
|
|
|
|
def sphere(self, i):
|
|
o = self.p5c + SPHERE * i
|
|
return [f32(self.b, o + 4 * k) for k in range(3)], f32(self.b, o + 12)
|
|
|
|
def cell_box(self, cx, cy, cz):
|
|
lo = [self.bbox_min[k] + self.cell_size[k] * (cx, cy, cz)[k] for k in range(3)]
|
|
return lo, [lo[k] + self.cell_size[k] for k in range(3)]
|
|
|
|
def cells(self):
|
|
"""(cell box, [u16]) for every A record that carries a cell index."""
|
|
b = self.b
|
|
for slot in sorted(s for s in set(pof0_pointer_slots(b)) if s >= self.p74):
|
|
rec = slot - 8
|
|
if b[rec + 3] != 1: # the other interleaved array
|
|
continue
|
|
pb = ptr(b, slot)
|
|
if pb + 8 > len(b):
|
|
continue
|
|
count, refs = u32(b, pb), ptr(b, pb + 4)
|
|
if count > 100_000 or refs + 2 * count > len(b):
|
|
continue
|
|
idx = list(struct.unpack_from(">%dH" % count, b, refs)) if count else []
|
|
yield self.cell_box(b[rec], b[rec + 1], b[rec + 2]), idx
|
|
|
|
|
|
def dist2(c, lo, hi):
|
|
"""Squared distance from a point to an axis-aligned box (0 if inside)."""
|
|
d = 0.0
|
|
for k in range(3):
|
|
if c[k] < lo[k]:
|
|
d += (lo[k] - c[k]) ** 2
|
|
elif c[k] > hi[k]:
|
|
d += (c[k] - hi[k]) ** 2
|
|
return d
|
|
|
|
|
|
def cmd_stride(pak):
|
|
print(f"{'object':>8} {'len(0x5C)':>10} {'/12':>10} {'/16':>6} {'max u16':>8}")
|
|
ok12 = ok16 = total = 0
|
|
for h, blob in objects(pak):
|
|
m = Mcol(blob)
|
|
top = max((v for _, idx in m.cells() for v in idx if v < m.n), default=-1)
|
|
div12 = m.sphere_bytes % 12 == 0
|
|
print(f"{h:08x} {m.sphere_bytes:10d} "
|
|
f"{m.sphere_bytes / 12:10.2f}{'' if div12 else ' *'} "
|
|
f"{m.n:6d} {top:8d}"
|
|
f"{' max == n-1' if top == m.n - 1 else ''}")
|
|
total += 1
|
|
ok12 += div12
|
|
ok16 += top == m.n - 1
|
|
print(f"\nlength divisible by 12: {ok12}/{total} "
|
|
f"max u16 == len/16 - 1: {ok16}/{total}")
|
|
return ok16 == total and ok12 < total
|
|
|
|
|
|
def cmd_cells(pak, seed=12345):
|
|
random.seed(seed)
|
|
hit = ctl = tot = 0
|
|
for _, blob in objects(pak):
|
|
m = Mcol(blob)
|
|
spheres = [m.sphere(i) for i in range(m.n)]
|
|
for (lo, hi), idx in m.cells():
|
|
for v in idx:
|
|
if v >= m.n:
|
|
continue
|
|
tot += 1
|
|
c, r = spheres[v]
|
|
hit += dist2(c, lo, hi) <= r * r
|
|
c2, r2 = spheres[random.randrange(m.n)]
|
|
ctl += dist2(c2, lo, hi) <= r2 * r2
|
|
print(f"referenced sphere reaches its own cell : {hit}/{tot} = {100*hit/tot:.2f}%")
|
|
print(f"random sphere, same object (control) : {ctl}/{tot} = {100*ctl/tot:.2f}%")
|
|
return hit / tot > 0.99 and ctl / tot < 0.25
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
raise SystemExit(__doc__)
|
|
cmd, pak = sys.argv[1], sys.argv[2]
|
|
if cmd == "stride":
|
|
ok = cmd_stride(pak)
|
|
elif cmd == "cells":
|
|
ok = cmd_cells(pak)
|
|
elif cmd == "verify":
|
|
a = cmd_stride(pak)
|
|
print()
|
|
b = cmd_cells(pak)
|
|
ok = a and b
|
|
else:
|
|
raise SystemExit(__doc__)
|
|
print("\nPASS" if ok else "\nFAIL")
|
|
sys.exit(0 if ok else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|