From e5c6c27e6a69dada924560894f7f0e0bd2bd56de Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 07:53:13 +0000 Subject: [PATCH 1/3] =?UTF-8?q?re:=20regn=5Fdecode.py=20=E2=80=94=20read?= =?UTF-8?q?=20a=20REGN=20object=20through=20its=20own=20POF0=20fixup=20tab?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retail deserialiser (sub_82465110 / sub_82465138 / sub_82465200) relocates a chunk with a fixup base of chunk+0x10, and the POF0 table it walks is an exact list of which words are pointers. Decoding that table gives the pointer graph with no guessing. `verify` reproduces every number quoted in the doc, each against a control: pointer-slot shape, reference-array packing, face/vertex incidence vs a random face, cell agreement vs transposed axes, and the portal-cost identity. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- tools/re-capture/regn_decode.py | 331 ++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100755 tools/re-capture/regn_decode.py diff --git a/tools/re-capture/regn_decode.py b/tools/re-capture/regn_decode.py new file mode 100755 index 00000000..e4e20cff --- /dev/null +++ b/tools/re-capture/regn_decode.py @@ -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 # entries + magics + ./regn_decode.py dump [hash] # one object's structure + ./regn_decode.py verify # 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() From 50625b5a9e90172ad756b833d5b07585080b3431 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 07:53:13 +0000 Subject: [PATCH 2/3] =?UTF-8?q?re:=20REGN=20is=20a=20tetrahedral=20navigat?= =?UTF-8?q?ion=20mesh=20=E2=80=94=20the=20cell=E2=86=92geometry=20link,=20?= =?UTF-8?q?decoded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found the reader. It is the deserialiser, not a consumer, and it answers the question twice: the fixup base is chunk+0x10 (82465198 addi r3,r31,16), so every offset previously recorded was read 16 bytes early — which is why twenty correlation tests sat at chance — and the POF0 table names every pointer word in the file. Six sections, not four. cell {count,item*} → item {n@+0x10, refs*@+0x14} → array of pointers into section 1 → a 96-byte tetrahedron. Section 2 is a face: plane, its three vertices, the two tetrahedra either side (0xFFFF = hull) and their face slots. All 11 objects: face passes through exactly 3 of its tet's 4 vertices in 253 722/253 722 (random control 0.07–2.2 %); portal cost == face-centroid distance in 380 460/380 460; sphere reaches its cell 98.7–100 % vs 18–28 % with transposed axes. Refuted and kept: 'REGN'/'MCOL' are never built as constants in the executable (0x474E occurs zero times in 1.87 M instructions), so no magic-dispatch site exists; and "zero portal-pair float marks a hull edge" shows no lift at all. Still open: the runtime consumer of the grid, the second portal float, and the four flag bytes at tetrahedron +0x54. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 21 ++ docs/re/INDEX.md | 2 +- docs/re/structures/regn-map-grid.md | 299 +++++++++++++++++++++++++++- 3 files changed, 316 insertions(+), 6 deletions(-) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c0253eba..f6b28681 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -359,6 +359,27 @@ search cannot find a *schedule*. index-shaped field in the cell payload has been followed into all three sections and checked for spatial agreement against controls — nothing above chance. Settling it needs the PE code that reads `REGN`, not more correlation. + ✅✅ **2026-08-26 — SETTLED, and `REGN` is a tetrahedral navigation mesh.** The + reader is the deserialiser `sub_82465110`/`sub_82465138`/`sub_82465200`, and it + gives the answer twice over: the fixup base is **`chunk + 0x10`** + (`82465198 addi r3, r31, 16`), so **every offset above was read 16 bytes + early** — which is why twenty correlation tests returned chance — and the + `POF0` table is an exact list of which words are pointers, so the pointer + graph needs no guessing. There are **six** sections, not four. A cell holds + `{count, item*}`; the item holds `{n @+0x10, refs* @+0x14}`; `refs` is an + array of pointers into section 1, which is a **96-byte tetrahedron** + (bounding sphere, 4 vertex indices, 4 face indices, 6 face-pair portal costs). + Section 2 is a **face**: plane + its 3 vertices + the two tetrahedra either + side (`0xFFFF` = hull) + their face slots. Checks, all 11 objects: each face + passes through exactly 3 of its tetrahedron's 4 vertices in **253 722/253 722** + (random control 0.07–2.2 %); portal cost == face-centroid distance in + **380 460/380 460**; a listed tetrahedron's sphere reaches its cell in + 98.7–100 % against 18–28 % for transposed axes. 🔴 Also recorded: **`'REGN'` + and `'MCOL'` are never built as constants in the executable** — `0x474E` + occurs zero times in 1.87 M instructions — so no magic-dispatch site exists. + ❔ Still open: the runtime *consumer* of the grid, the second float of each + portal pair, and the 4 flag bytes at tetrahedron `+0x54`. Tool: + `tools/re-capture/regn_decode.py`. See [`structures/regn-map-grid.md`](structures/regn-map-grid.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 diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index c5e1cd48..fd120640 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -101,7 +101,7 @@ files, which is how the same ground got covered twice. | [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue | | [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value | | [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — | -| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a per-map spatial grid (and `MCOL` beside it) | ✅ CONFIRMED for the header, which self-checks on all 11 objects on | +| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it) | ✅ CONFIRMED — tet mesh + face adjacency + portal costs + uniform grid; decoded from the file's own `POF0` fixup table | | [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the | | [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — | | [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; | diff --git a/docs/re/structures/regn-map-grid.md b/docs/re/structures/regn-map-grid.md index 94821e96..ee8401ff 100644 --- a/docs/re/structures/regn-map-grid.md +++ b/docs/re/structures/regn-map-grid.md @@ -1,8 +1,16 @@ -# `REGN` — a per-map spatial grid (and `MCOL` beside it) +# `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it) -**Status: ✅ `CONFIRMED` for the header**, which self-checks on all 11 objects on -the disc. ❔ the four data sections are undecoded. **New to this corpus** — no -document mentioned `REGN`, `MCOL` or `hidden/MiscBin.pak` before 2026-08-24. +**Status: ✅ `CONFIRMED`.** A `REGN` object is a **tetrahedral navigation mesh +of the whole play volume** — vertices, tetrahedra, faces with full adjacency, +per-tetrahedron portal costs — plus a uniform grid for point location. The +decode is complete except for two small fields; see +[the 2026-08-26 section](#-2026-08-26--the-reader-found-regn-is-a-tetrahedral-navigation-mesh), +which supersedes the offsets in the older sections below (they were read +**16 bytes early** — the loader's fixup base is `chunk + 0x10`). The older +sections are kept because their refutations are still instructive. + +**New to this corpus** — no document mentioned `REGN`, `MCOL` or +`hidden/MiscBin.pak` before 2026-08-24. ## Where it is @@ -373,4 +381,285 @@ nothing rises above chance. object in the executable and watch which fields it dereferences. That is static PE work (`/work/*.pe`, offset = VA − 0x82000000) of the same kind that cracked the `.slb` packing phase, and it is the honest next step rather than a -twenty-first correlation. \ No newline at end of file +twenty-first correlation. +--- + +# ✅ 2026-08-26 — the reader found: `REGN` is a **tetrahedral navigation mesh** + +The previous section closed with "find what reads a `REGN` object in the +executable and watch which fields it dereferences". That worked, and it did not +need the consumer: the **deserialiser** answers the question, because the file +carries its own pointer map and the deserialiser tells you how to read it. + +## ✅ The reader — `sub_82465110` / `sub_82465138` / `sub_82465200` + +Three functions, all in the resource module around `0x82460000`: + +``` +sub_82465110 find_pof0(chunk) + 82465110 lwz r11, 4(r3) ; datasize + 82465118 add r11, r11, r3 + 82465120 addi r3, r11, 16 ; -> chunk + 16 + datasize + 82465124 lwz r11, 0(r3) + 82465114 lis r10, 0x504F / 8246511c ori r10, r10, 0x4630 ; 'POF0' + 82465128 cmplw cr6, r11, r10 + 8246512c beqlr cr6 ; else return 0 +``` + +``` +sub_82465138 relocate_chunk_chain(chunk) + 82465164 lbz r11, 8(r31) / clrlwi r11,r11,31 ; already-relocated bit + 82465178 bl 0x82465110 ; find the POF0 chunk + 82465194 addi r4, r11, 16 ; POF0 payload + 82465198 lwz r5, 4(r11) ; POF0 payload size + 8246519c addi r3, r31, 16 ; ← THE FIXUP BASE = chunk + 0x10 + 824651a0 bl 0x82465200 + 824651a8 ori r11, r11, 0x1 / stb r11, 8(r31) ; set the bit +``` + +``` +sub_82465200 apply_pof0(base=r3, table=r4, size=r5) + 82465224 clrrwi r8, r10, 6 ; top two bits of the lead byte select + 82465228 cmplwi cr6, r8, 0x40 ; 0x40 → 6-bit delta, 1 byte + 82465230 cmplwi cr6, r8, 0x80 ; 0x80 → 14-bit delta, 2 bytes + 82465238 cmplwi cr6, r8, 0xC0 ; 0xC0 → 22-bit delta, 3 bytes + 82465258 add r11, r10, r11 ; running WORD index, never reset + 8246525c slwi r8, r11, 2 + 82465260 lwzx r10, r8, r3 ; slot = base[word] + 82465264 cmplwi cr6, r10, 0x0 + 8246526c add r10, r10, r3 ; *slot += base (skipped when *slot == 0) + 82465270 stwx r10, r8, r3 +``` + +Two things follow, and both are load-bearing: + +1. **The fixup base is `chunk + 0x10`, not `chunk + 0`** (`82465198`: + `addi r3, r31, 16`). Every stored "offset" in a `REGN` file is relative to + file offset `0x10`. **Everything on this page above was read 16 bytes early.** + That single error is why twenty correlation tests returned chance. +2. **The `POF0` table is an exact list of which words are pointers.** It is not + a heuristic — it is the data the retail loader itself walks. Decoding it + gives the pointer graph directly, with no guessing. + +`sub_82465138` is reached from exactly two callers, `sub_82461018` (vtable slot +9 of the class at `0x820af8bc`, the pak/resource file class) and +`sub_82461DE8`; both store `chunk + 16` as the object's data pointer, which +confirms the same `+0x10` base from the other side. + +### 🔴 …and the magic is never compared + +Worth recording because it is what sent the search to the fixup table: +**`'REGN'` and `'MCOL'` are not constructed anywhere in the executable.** The +title builds its four-character tags as `lis`/`ori` pairs, and a sweep of every +such pair recovers 156 tags — `RATC`, `T8aD`, `XBG7`, `IPFB`, `IDXD`, `LSTA`, +`POF0`, `PRMD`, `TBMD`, `WMV3` … — but neither of these. Nor does either half +appear as an immediate anywhere: `0x474E` (`'GN'`) occurs **zero** times in +1 865 751 instructions, and the flat PE image contains the byte string `REGN` +**zero** times. So no magic-dispatch site exists to find; `.rgn` objects are +handed to the map code by the stage record, not identified by their tag. + +## ✅ The header is 0x70 bytes at `chunk + 0x10`, with **six** sections + +Corrected, and re-derived from the `POF0` table, which relocates exactly six +header words (`0x70`, `0x74`, `0x78`, `0x7c`, `0x80`, `0x84`) on 11 of 11: + +``` +chunk +0x00 char[4] 'REGN' + +0x04 u32 data size (POF0 chunk at +0x04-value + 0x10) + +0x08 u8 flags; bit0 = "already relocated" (set by sub_82465138) +data = chunk + 0x10: + +0x00 f32[4] bbox min (w = 1.0) + +0x10 f32[4] bbox max + +0x20 f32[4] extent + +0x30 f32[4] cell size + +0x40 u32[4] grid dims + +0x50 u16[6] record counts, one per section + +0x60 ptr[6] section pointers +``` + +The old page had four sections because it read the last two pointers as data. +There are six, their strides are `12, 96, 48, 8, 32, 4`, and each section's +span divided by its stride is its `counts[]` entry exactly — 11 of 11, with the +only slack being 16-byte alignment padding and, for the face list, **two +all-zero sentinel records** (which is the unexplained "constant 96-byte tail" +from the section above: 2 × 48). + +| # | contents | stride | count | +|---|---|---|---| +| 0 | vertices | 12 | `counts[0]` | +| 1 | **tetrahedra** | 96 | `counts[1]` | +| 2 | **faces** (plane + adjacency) | 48 | `counts[2]` | +| 3 | cell index, one per cell | 8 | `counts[3]` = cells | +| 4 | cell items, one per **occupied** cell | 32 | `counts[4]` | +| 5 | tetrahedron references | 4 | `counts[5]` | + +## ✅ How a cell reaches its geometry — the question, answered + +The `POF0` table places every pointer in the file, and there are only four +kinds. Verified on all 11 objects by `tools/re-capture/regn_decode.py verify`: + +* the six header words, and nothing else in the header; +* **one pointer per occupied cell**, at section-3 record offset **`+4`** + — so a cell is `{ u32 count; item* }` and the *count* word is not a pointer; +* **exactly one pointer per section-4 record, at offset `+0x14`** — the 32-byte + cell item is `{ …, u32 n @+0x10, tetref* @+0x14, … }`; +* **every word of section 5**, all `counts[5]` of them. + +So the chain is: + +``` +position ─▶ cell (x,y,z) index = (z·dimY + y)·dimX + x + ─▶ sec3[index] {count, item*} + ─▶ item {…, n @+0x10, refs* @+0x14, …} + ─▶ refs[0 .. n) each a pointer into section 1 + ─▶ tetrahedron +``` + +Four independent checks, all 11 of 11 objects: + +* **section-5 targets land on section-1 record boundaries** — every one of + **261 000** pointers is `sec1 + 96·k` with zero remainder; +* **the reference arrays are packed contiguously in cell order** — + `item[i].refs == sec5 + 4·Σ item[j