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/mcol_probe.py
Sylpheed RE agent 4f0d21f50d re: MCOL's 0x5C block is bounding spheres at stride 16 -- the 0.75 was 12/16
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.
2026-08-26 09:04:30 +00:00

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()