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.
This commit is contained in:
Sylpheed RE agent
2026-08-26 09:04:30 +00:00
parent 324389c612
commit cda5a31601
4 changed files with 584 additions and 2 deletions

View File

@@ -381,6 +381,22 @@ search cannot find a *schedule*.
See [`structures/regn-map-grid.md`](structures/regn-map-grid.md).
❔ Still open there: the **runtime consumer** (not reached; a `float4`-aligned
header read with VMX loads leaves no displacement signature to search for).
**`MCOL` is the collision sibling, and its broad phase is decoded
(2026-08-26).** Same container, same 11 maps, same cell size. Chain:
`position → cell → A record → B record {count, u16[n]} → 16-byte bounding
sphere`. The `0x5C` block is **stride 16 `{centre f32[3], radius f32}`**, not
the 12-byte points I had assumed — `len(0x5C)` is not even a multiple of 12 in
5 of 11 objects, and `max u16 == len/16 1` in **11/11**. Powered check: the
referenced sphere reaches the cell that reached it, **18 559/18 577 = 99.90 %**
against a 12.02 % random-sphere control, and ablating either the centre or the
radius costs most of the signal. Reproduce with
`tools/re-capture/mcol_probe.py verify`.
⚠️ The stale ≈**0.75×** ratio I had recorded as "too consistent to be
coincidence" was **12/16** — my own wrong stride, not a fact about the data.
❔ Still open: the `0x54` and `0x58` blocks (the geometry these spheres bound;
neither has a constant stride against the sphere count), and 18 cell entries
(0.10 %) whose sphere misses its cell by 0.413 %.
See [`structures/mcol-collision.md`](structures/mcol-collision.md).
* 🔴 **`hidden/DefTables.pak` is NOT it** (checked 2026-08-24). The three
unnamed schemas are more **model/render** tables in the same vocabulary as the
named ones — `7e66225f` (283 objects) carries `RenderFrameModel`,

View File

@@ -229,7 +229,7 @@ evidence.)
position → cell → A record {cell index, count 1, →B, bounding sphere}
→ B record {count n, →u16[n]}
→ n indices into the 0x5C point block
→ n indices into the 0x5C point block ← wrong, see below
which is the same shape as `REGN`'s `cell → item → refs → geometry`, as the two
formats' shared header and shared count-1 convention already suggested.
@@ -282,4 +282,96 @@ confident and false statement about `MCOL`'s geometry.
❔ What the `u16`s index is open. A datum for the next attempt: the maximum
`u16` is consistently ≈ **0.75 ×** the point count (923/1 232, 1 019/1 360,
1 163/1 552, 59/80) — too consistent to be coincidence, and not explained.
1 163/1 552, 59/80) — too consistent to be coincidence, and not explained.
> **Resolved in the next section.** 0.75 is `12 / 16`: "the point count" was
> computed with an assumed 12-byte stride that the block lengths refute. The
> `u16`s *do* index this block — at stride 16. Both this test and the 100 %
> bound-check above were reading the block wrongly; only the powered one could
> say so.
## ✅ The `0x5C` block is **bounding spheres at stride 16** — and that explains the 0.75
**2026-08-26.** The unexplained ≈0.75 ratio left at the end of the section above
was **12 / 16**: my own stride. I had been reading the `0x5C` block as 12-byte
points because `REGN`'s vertex section is 12 bytes, and never checked the
assumption.
It does not survive the cheapest possible check — **`len(0x5C)` is not a
multiple of 12** in 5 of the 11 objects, so a 12-byte stride was never
arithmetically possible:
| object | `len(0x5C)` | ÷12 | ÷16 | max `u16` |
|---|---|---|---|---|
| `2cf7eb47` | 960 | 80.00 | 60 | 59 |
| `cbb99d34` | 192 | 16.00 | 12 | 11 |
| `d84a95fb` | 3 488 | 290.67 ❌ | 218 | 217 |
| `db066592` | 12 640 | 1 053.33 ❌ | 790 | 789 |
| `db61c506` | 2 144 | 178.67 ❌ | 134 | 133 |
| `dc44fe0c` | 2 784 | 232.00 | 174 | 173 |
| `dc4b0896` | 14 784 | 1 232.00 | 924 | 923 |
| `dd89a110` | 18 624 | 1 552.00 | 1 164 | 1 163 |
| `df4628c2` | 4 160 | 346.67 ❌ | 260 | 259 |
| `e084c13c` | 192 | 16.00 | 12 | 11 |
| `e16460cf` | 16 320 | 1 360.00 | 1 020 | 1 019 |
At stride 16 the relation is not "≈0.75×" but **exact, in 11 / 11 objects**:
max u16 == len(0x5C) / 16 1
The `u16` array indexes the `0x5C` block at stride 16, and *covers it fully*
the largest index is always the last element.
### The record is `{ centre f32[3], radius f32 }`
Read at stride 16, the first three floats lie inside the object's own bounding
box in **every record of every object**, and are never unit-length, so this is a
position and not a plane normal. The fourth float is a positive scalar which is
**not** `|centre|`.
The powered test is the one the previous section said was needed: a `u16` is
reached *through a specific cell*, so the thing it names should be present in
that cell. Treating the record as a sphere and the cell as its grid box:
| | |
|---|---|
| **referenced sphere intersects the cell that reached it** | **18 559 / 18 577 = 99.90 %** |
| a random sphere from the same object (control) | 2 233 / 18 577 = 12.02 % |
And both halves of the record are load-bearing — ablating either one costs most
of the signal:
| | |
|---|---|
| centre + radius | **99.90 %** |
| centre alone, radius treated as 0 | 26.75 % |
| centre kept, radius shuffled within the object | 70.19 % |
| radius kept, centre shuffled within the object | 23.74 % |
So the `0x5C` block is a **broad-phase bounding-sphere array**, and each grid
cell's `u16` list names the primitives that reach into that cell — the standard
shape for a collision mesh, and the sibling of `REGN`'s `cell → tetrahedra`.
### The list is a *subset* of what the spheres allow, which is the expected direction
Testing the converse — is the `u16` set **exactly** the set of spheres that
intersect the cell? — gives **17 / 4 488 cells (0.38 %)**, with 46 525 spheres
intersecting a cell but absent from its list. That is the right direction and
not a problem: a bounding sphere is a conservative bound on the primitive inside
it, so "sphere overlaps cell" must be implied by membership but cannot imply it.
The tighter true geometry lives in the `0x54` / `0x58` blocks, still undecoded.
**18 exceptions (0.10 %)** go the wrong way — listed, but the sphere misses
the cell, by `dist / radius` of 1.004 to 1.129. They are spread over 7 of the 11
objects with no object dominating, so this looks like a small build-time margin
rather than a decode error, but it is **not explained** and is recorded as open.
### The chain, corrected
position → cell → A record {cell index (x,y,z,1), count 1, →B at +8, bounding sphere}
→ B record {u32 count, →u16[n] at +4}
→ n indices into the 0x5C array of 16-byte bounding spheres
❔ Still open: the `0x54` and `0x58` blocks — the actual collision geometry that
these spheres bound. Their lengths are **not** a constant multiple of the sphere
count (`0x58 / n` is ≈6.0 for the large objects but 6.13 and 6.67 for the two
smallest), so at least one of them is variable-stride or has its own count.

143
tools/re-capture/mcol_probe.py Executable file
View File

@@ -0,0 +1,143 @@
#!/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()

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""Decode a `REGN` object (a stage's `MapPath`, `hidden/MiscBin.pak`).
A `REGN` object is a serialised C++ object graph with a trailing `POF0`
pointer-fixup table. The fixup table is the primary evidence used here: it
names every word in the file that the retail loader turns into a pointer, so
the pointer graph does not have to be guessed. The loader code that consumes
it is `sub_82465110` / `sub_82465138` / `sub_82465200` (see
`docs/re/structures/regn-map-grid.md`).
./regn_decode.py list <MiscBin.pak> # entries + magics
./regn_decode.py dump <MiscBin.pak> [hash] # one object's structure
./regn_decode.py verify <MiscBin.pak> # the checks in the doc
Everything is big-endian. All file offsets printed are absolute, i.e. the
stored pointer value plus 0x10 (the fixup base is chunk+0x10).
"""
import glob
import itertools
import math
import os
import random
import struct
import sys
import zlib
FIXUP_BASE = 0x10 # sub_82465138: the POF0 applier is called with r3 = chunk+16
# stride of each of the six sections, in bytes
STRIDE = (12, 96, 48, 8, 32, 4)
SECTION_NAME = (
"vertices", "tetrahedra", "faces", "cell index", "cell items", "tet refs",
)
PAIRS = list(itertools.combinations(range(4), 2))
# ── IPFB archive ────────────────────────────────────────────────────────────
def pak_entries(pak_path):
"""[(name_hash, payload_bytes)] for every entry of an IPFB archive."""
index = open(pak_path, "rb").read()
if index[:4] != b"IPFB":
raise SystemExit(f"{pak_path}: not an IPFB index")
count, = struct.unpack_from(">I", index, 4)
toc = [struct.unpack_from(">III", index, 0x10 + 12 * i) for i in range(count)]
segs = sorted(glob.glob(os.path.splitext(pak_path)[0] + ".p[0-9][0-9]"))
data = b"".join(open(s, "rb").read() for s in segs)
out = []
for name_hash, offset, comp_size in toc:
stored = data[offset:offset + comp_size]
if stored[:2] == b"Z1":
stored = zlib.decompress(stored[10:])
out.append((name_hash, stored))
return out
# ── POF0 ────────────────────────────────────────────────────────────────────
def pof0_pointer_slots(blob):
"""Byte offsets of every word the retail loader relocates.
Mirrors `sub_82465200` exactly: a run-length delta stream over word
indices, 6 / 14 / 22 bits selected by the top two bits of the lead byte.
"""
data_size, = struct.unpack_from(">I", blob, 4)
table = data_size + 16
if blob[table:table + 4] != b"POF0":
raise SystemExit("no POF0 chunk at header[0x04]+16")
size, = struct.unpack_from(">I", blob, table + 4)
p, end, word = table + 16, table + 16 + size, 0
slots = []
while p < end:
b = blob[p]
if b == 0:
break
tag = b & 0xC0
if tag == 0x40:
word += b & 0x3F
p += 1
elif tag == 0x80:
word += ((b & 0x3F) << 8) + blob[p + 1]
p += 2
elif tag == 0xC0:
word += ((((b & 0x3F) << 8) + blob[p + 1]) << 8) + blob[p + 2]
p += 3
else:
p += 1
continue
slots.append(FIXUP_BASE + 4 * word)
return slots
# ── the object ──────────────────────────────────────────────────────────────
class Regn:
def __init__(self, blob):
if blob[:4] != b"REGN":
raise SystemExit(f"not a REGN object (magic {blob[:4]!r})")
self.b = blob
self.data_size, = struct.unpack_from(">I", blob, 4)
self.bbox_min = self.f3(0x10)
self.bbox_max = self.f3(0x20)
self.extent = self.f3(0x30)
self.cell_size = self.f3(0x40)
self.dims = [self.u32(0x50 + 4 * i) for i in range(3)]
self.count = [self.u16(0x60 + 2 * i) for i in range(6)]
# the six section pointers, resolved to absolute file offsets
self.sec = [self.u32(0x70 + 4 * i) + FIXUP_BASE for i in range(6)]
def u32(self, o): return struct.unpack_from(">I", self.b, o)[0]
def u16(self, o): return struct.unpack_from(">H", self.b, o)[0]
def f32(self, o): return struct.unpack_from(">f", self.b, o)[0]
def f3(self, o): return tuple(self.f32(o + 4 * i) for i in range(3))
def ptr(self, o): return self.u32(o) + FIXUP_BASE
# section 0 — vertex
def vertex(self, i):
return self.f3(self.sec[0] + 12 * i)
# section 1 — tetrahedron
def tet(self, i):
b = self.sec[1] + 96 * i
return {
"sphere_centre": self.f3(b),
"sphere_radius": self.f32(b + 12),
"vertices": [self.u16(b + 16 + 2 * k) for k in range(4)],
"faces": [self.u16(b + 24 + 2 * k) for k in range(4)],
# six (face_i, face_j) pairs in itertools.combinations order
"portal_cost": [self.f32(b + 32 + 8 * k) for k in range(6)],
"portal_unk": [self.f32(b + 36 + 8 * k) for k in range(6)],
"index": self.u32(b + 80),
"flags": self.b[b + 84:b + 88],
}
# section 2 — face (a plane plus its adjacency)
def face(self, i):
b = self.sec[2] + 48 * i
return {
"normal": self.f3(b),
"d": self.f32(b + 12),
"point": self.f3(b + 16),
"one": self.f32(b + 28),
"vertices": [self.u16(b + 32 + 2 * k) for k in range(3)],
"index": self.u16(b + 38),
"tet": [self.u16(b + 40), self.u16(b + 42)], # 0xFFFF = hull
"slot": [self.u16(b + 44), self.u16(b + 46)],
}
# section 3/4/5 — the grid
def cell_index(self, x, y, z):
return (z * self.dims[1] + y) * self.dims[0] + x
def cell(self, ci):
"""[tet index] for one cell — count/pointer, item record, ref array."""
b = self.sec[3] + 8 * ci
n, p = self.u32(b), self.u32(b + 4)
if n == 0 or p == 0:
return []
item = p + FIXUP_BASE
k = self.u32(item + 0x10)
refs = self.ptr(item + 0x14)
return [(self.ptr(refs + 4 * j) - self.sec[1]) // 96 for j in range(k)]
def cell_bounds(self, ci):
dx, dy = self.dims[0], self.dims[1]
x, y, z = ci % dx, (ci // dx) % dy, ci // (dx * dy)
lo = tuple(self.bbox_min[k] + self.cell_size[k] * (x, y, z)[k]
for k in range(3))
return lo, tuple(lo[k] + self.cell_size[k] for k in range(3))
# ── commands ────────────────────────────────────────────────────────────────
def cmd_list(pak):
for name_hash, blob in pak_entries(pak):
magic = blob[:4].decode("latin1")
magic = magic if magic.isprintable() else "?"
print(f"{name_hash:08x} {magic:4s} {len(blob):9d}")
def cmd_dump(pak, want=None):
for name_hash, blob in pak_entries(pak):
if blob[:4] != b"REGN":
continue
if want is not None and name_hash != want:
continue
r = Regn(blob)
print(f"── {name_hash:08x} {len(blob)} bytes")
print(f" bbox {r.bbox_min} .. {r.bbox_max}")
print(f" cell {r.cell_size} dims {r.dims}")
print(f" counts {r.count}")
for i in range(6):
print(f" sec{i} {SECTION_NAME[i]:<11s} @0x{r.sec[i]:06x} "
f"{r.count[i]:6d} x {STRIDE[i]}")
t = r.tet(0)
print(f" tet 0 verts {t['vertices']} faces {t['faces']} "
f"r={t['sphere_radius']:.1f}")
f0 = r.face(t["faces"][0])
print(f" face {t['faces'][0]:<4d} verts {f0['vertices']} "
f"tets {f0['tet']} slots {f0['slot']}")
occupied = sum(1 for ci in range(r.dims[0] * r.dims[1] * r.dims[2])
if r.cell(ci))
print(f" cells {occupied} occupied of "
f"{r.dims[0] * r.dims[1] * r.dims[2]}")
if want is not None:
return
def cmd_verify(pak):
random.seed(0)
objs = [(h, b) for h, b in pak_entries(pak) if b[:4] == b"REGN"]
print(f"{len(objs)} REGN objects\n")
hdr = ("object ptr-slots chain face==3 verts ctrl "
"sphere/cell ctrl portal cost")
print(hdr)
for name_hash, blob in objs:
r = Regn(blob)
slots = pof0_pointer_slots(blob)
# 1. where the fixup table says the pointers are
header = [o for o in slots if o < r.sec[0]]
in3 = [o for o in slots if r.sec[3] <= o < r.sec[4]]
in4 = [o for o in slots if r.sec[4] <= o < r.sec[5]]
in5 = [o for o in slots if o >= r.sec[5]]
others = len(slots) - len(header) - len(in3) - len(in4) - len(in5)
shape = (header == [0x70 + 4 * i for i in range(6)]
and others == 0
and all((o - r.sec[3]) % 8 == 4 for o in in3)
and all((o - r.sec[4]) % 32 == 20 for o in in4)
and len(in5) == r.count[5]
and len(in4) == r.count[4] == len(in3))
# 2. the cell -> item -> ref-array chain is contiguously packed
total, chain = 0, True
for i in range(r.count[4]):
item = r.sec[4] + 32 * i
if r.ptr(item + 0x14) != r.sec[5] + 4 * total:
chain = False
total += r.u32(item + 0x10)
chain = chain and total == r.count[5]
# 3. each of a tet's four faces passes through exactly 3 of its
# four vertices — against a random-face control
good = ctrl = n = 0
for i in range(r.count[1]):
t = r.tet(i)
V = [r.vertex(p) for p in t["vertices"]]
scale = max(abs(x) for v in V for x in v) + 1
for want_real in (True, False):
for fi in (t["faces"] if want_real else
[random.randrange(r.count[2]) for _ in range(4)]):
fc = r.face(fi)
on = sum(1 for v in V
if abs(sum(fc["normal"][j] * v[j] for j in range(3))
+ fc["d"]) <= 1e-4 * scale)
if want_real:
good += on == 3
n += 1
else:
ctrl += on == 3
# 4. a listed tet's bounding sphere reaches the listed cell —
# control is the same test with the cell axes transposed
hit = thit = m = 0
for ci in range(r.dims[0] * r.dims[1] * r.dims[2]):
tets = r.cell(ci)
if not tets:
continue
lo, hi = r.cell_bounds(ci)
tlo, thi = tuple(reversed(lo)), tuple(reversed(hi))
for ti in tets:
t = r.tet(ti)
c, rad = t["sphere_centre"], t["sphere_radius"]
for (a, b), which in (((lo, hi), 0), ((tlo, thi), 1)):
q = 0.0
for j in range(3):
if c[j] < a[j]:
q += (a[j] - c[j]) ** 2
elif c[j] > b[j]:
q += (c[j] - b[j]) ** 2
if q <= rad * rad:
if which == 0:
hit += 1
else:
thit += 1
m += 1
# 5. the six portal costs are the face-centroid distances
pc = pn = 0
for i in range(r.count[1]):
t = r.tet(i)
V = [r.vertex(p) for p in t["vertices"]]
omit = []
for fi in t["faces"]:
fv = set(r.face(fi)["vertices"])
miss = [k for k in range(4) if t["vertices"][k] not in fv]
omit.append(miss[0] if len(miss) == 1 else None)
if any(o is None for o in omit):
continue
for k, (a, b) in enumerate(PAIRS):
want = math.dist(V[omit[a]], V[omit[b]]) / 3
pn += 1
pc += abs(t["portal_cost"][k] - want) <= 1e-4 * max(1.0, want)
print(f"{name_hash:08x} {'ok' if shape else 'BAD':>9s} "
f"{'ok' if chain else 'BAD':>7s} "
f"{100 * good / n:6.2f}% {100 * ctrl / n:6.2f}% "
f"{100 * hit / m:6.2f}% {100 * thit / m:6.2f}% "
f"{100 * pc / pn:7.3f}%")
def main():
if len(sys.argv) < 3:
print(__doc__)
raise SystemExit(2)
cmd, pak = sys.argv[1], sys.argv[2]
if cmd == "list":
cmd_list(pak)
elif cmd == "dump":
want = int(sys.argv[3], 16) if len(sys.argv) > 3 else None
cmd_dump(pak, want)
elif cmd == "verify":
cmd_verify(pak)
else:
print(__doc__)
raise SystemExit(2)
if __name__ == "__main__":
main()