Files
Sylpheed/docs/re/structures/regn-map-grid.md
Sylpheed RE agent 0cbef2023f re: withdraw "everything was read 16 bytes early" — the planes were right
The coordinator refuted the broad form of my claim and was correct to. The
older reading put section 2 at chunk+offset with the normal at intra-record
+16, which addresses exactly the same bytes as chunk+0x10+offset with the
normal at +0: on 3506e972 both put face record 0's normal at 0x1c700. Its
unit-normal and n·p+d results were measuring real planes and stand.

What is actually wrong is the record BOUNDARY, and it decides which record the
four integer words belong to — the face adjacency, i.e. the field the whole
correlation search was looking for. Settled: requiring own==f and
tet[A].faces[i]==f gives 100.000% with the block from record f and 0.000% from
record f+1, on all 11 objects.

Also audits my own numbers for the same defect the coordinator found in the
point-in-bbox test: most of them are content tests that address identical bytes
under either convention and have no power over the origin. The four that do
have power are listed, including that POF0 at base+0 would relocate the u16
counts and leave the section pointers unrelocated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 07:58:45 +00:00

720 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it)
**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
`hidden/MiscBin.pak` — 40 entries, **none** name-resolved, in three groups:
| magic | count | sizes |
|---|---|---|
| `REGN` | **11** | 49 KB … 3.1 MB |
| `MCOL` | **11** | 37 KB … 81 KB |
| (other) | 18 | — |
Eleven of each, which pairs them: one `REGN` and one `MCOL` per map. `MCOL` is
untouched here; the name and the size range read like mesh collision.
## The header, and why it is believable
```
0x00 char[4] 'REGN'
0x04 u32 size of the data area (the POF0 table starts at 0x04-value + 16)
0x10 f32[4] bbox min (x, y, z, 1.0)
0x20 f32[4] bbox max (x, y, z, 1.0)
0x30 f32[4] extent (max - min)
0x40 f32[4] cell size
0x50 u32[4] grid dimensions
0x60 u16[6] six counts
0x70 u32[4] four section offsets (the first is always 0x80)
```
**The check that makes this a decode rather than a guess** — over all 11 objects:
* `extent == cell × dims` holds **11 of 11**, exactly;
* `counts[3]` equals the **cell count**: `1000` on every 10×10×10 map and `125`
on every 5×5×5 one.
Two independent fields reproducing the grid is what rules out coincidence.
## The three map sizes on the disc
| half-extent | cell | dims | objects |
|---|---|---|---|
| 250 000 | 50 000 | 10×10×10 | 2 |
| 50 000 | 10 000 | 10×10×10 | 6 |
| 25 000 | 10 000 | 5×5×5 | 3 |
So every map is a cube partitioned into 125 or 1 000 uniform cells — 10 km cells
in a 100 km cube for the common case, and one pair of maps five times larger.
## ✅ Section 3 is the cell index — and it self-checks 11/11
The fourth section is a **one entry per cell** table of 8-byte records
`(count, offset)`, immediately followed by the records those offsets point at:
```
index = offsets[3] .. offsets[3] + cells*8
payload = align16(index end) ..
record = 32 bytes (offsets step by 0x20)
```
Checked over all 11 objects: the lowest offset any cell refers to equals
**`align16(offsets[3] + cells × 8)` — 11 of 11**. On the six 10×10×10 maps
`cells × 8` is already 16-aligned and the payload butts straight up against the
index; on the three 5×5×5 maps `125 × 8 = 1000` is not, and the payload starts
8 bytes later, which is what makes the alignment rule visible rather than assumed.
Two more invariants from the same sweep:
* every occupied cell has count **exactly 1** — total items equals occupied cells
on all 11 objects, so this is "one record per cell", not a bucket list;
* **`counts[4] = occupied cells + 2`**, exactly, on all 11 (e.g. 997/995,
880/878, 125/123). The `+2` is unexplained — two sentinels, or two cells
counted differently.
Most cells are occupied: 878998 of 1 000, 123 of 125.
## ✅ It is a serialised object graph with a `POF0` fixup table
Every one of the 11 objects contains the tag **`POF0`**, always near the tail,
and its position is exactly **`header[0x04] + 16`** — on **11 of 11**:
```
e993b93e header +0x04 = 0x0b100 POF0 at 45328 = 45312 + 16
e4155d94 header +0x04 = 0x235d70 POF0 at 2317696 = 2317680 + 16
… 11 of 11 identical relation
```
`POF0` is a **pointer-offset (fixup) table**: the file is a serialised C++ object
graph, and the loader patches the recorded slots into real pointers. That
explains a detail that would otherwise be odd — the "offsets" inside the cell
index are absolute **file** offsets, because that is what a fixup table rewrites.
So `header[0x04]` is the size of the data area, and everything past
`header[0x04] + 16` is relocation bookkeeping rather than content.
## 🔴 Two readings of the cell payload, both refuted by generalising
Both came from the smallest object and both died the moment they were checked
against the other ten — recorded because the temptation to keep them was real:
* **"the `f32` at record `+0x1c` is the grid's bounding-sphere radius."** On
`e993b93e` it is 86 689 against `√3 × 50 000 = 86 603`, a ratio of **1.001**.
On the other ten the ratio runs **0.13 0.27**. Fitted to one sample.
* **"a record's `(count, offset)` pairs point at leaf arrays of `count × 4`
bytes."** True for the first record of `e993b93e`; across the objects the
offset deltas fail that rule on **every** object checked (0 of 11 clean).
What survives is only descriptive: the payload area is dominated by **float
data** — the "strings" a printable-run scan finds are all byte patterns like
`0x46`/`0x47` high bytes, i.e. medium-magnitude floats, not text.
## ❔ The other three sections
Their offsets scale with the object, and `counts[0..2]` scale with them —
`(318, 1172, 2584)` for the 350 KB map against `(2936, 14967, 31155)` for the
3.1 MB one, a roughly `1 : 4.5 : 10` ratio that holds across all eleven. The
smallest object (49 KB) is nearly empty by comparison — `(8, 6, 18)` — which
makes it the cheapest one to decode first.
## Why this matters, stated without overclaiming
A mission's enemy count **rises and falls** as waves arrive and are destroyed, so
somewhere there is a scheduler with parameters — what spawns, where, and on what
trigger. A per-map uniform grid indexed by cell is exactly the structure such a
thing is indexed *by*.
🔴 **But nothing here shows spawn parameters yet.** The header is a spatial
partition and no more; the sections are unread. Treat this as the location of the
world's spatial data, not as the wave table.
## ✅ Update: `REGN` is a stage's **MapPath**
The per-stage definition record (see
[stage-definition-table.md](stage-definition-table.md)) has a field
`MapPath = test.rgn`, and `name_hash("test.rgn") = 0x3506e972`, which is one of
the `REGN` objects in `MiscBin.pak`. The sibling field `MapMesh = test.col`
hashes to `0x2cf7eb47`, an `MCOL` object in the same pak.
So `.rgn`/`REGN` is stage navigation/path data referenced by the stage record,
and `.col`/`MCOL` is the stage collision mesh. This does not by itself validate
either of the two refuted cell-payload readings recorded above, but it does
explain why the payload looks like a grid of route data.
## ✅ Sections 0, 1 and 2 have record strides — and section 2 is a PLANE list
**2026-08-26.** The three sections the page called undecoded are fixed-stride
record arrays, and `counts[0..2]` are their record counts. Dividing each
section's byte span by its count over all 11 objects:
| section | stride | evidence |
|---|---|---|
| 0 | **12 bytes** | `count × 12` fits with a remainder of 012 in **11 / 11** |
| 1 | **96 bytes** | `count × 96` fits with a remainder of **exactly 0 in 11 / 11** |
| 2 | **48 bytes** | `count × 48` fits with a remainder of **exactly 96 in 11 / 11** |
Section 1 landing on a zero remainder in every object, and section 2 on the same
96-byte tail in every object, is what makes these strides rather than a
coincidence of division.
### ✅ Section 0 is a point list
12 bytes is three `f32`. Over **13 467** records across all 11 objects, **every
one lies inside that object's own header bounding box — 13 467 / 13 467 (100 %)**.
Values land on the box corners (`±250 000`) and inside.
### ✅ Section 2 is a plane list — 48 bytes, and the plane equation closes
Read as twelve `f32`:
[0..3] zero
[4..6] unit normal |n| = 1 ± 0.02 in 133 573 / 133 573 (100 %)
[7] signed distance d
[8..10] a point on the plane, inside the bbox in 133 573 / 133 573 (100 %)
[11] 1.0 exactly in 133 573 / 133 573 (100 %)
**The check that makes this a decode**: for a genuine plane, `n·p + d` must be
zero. Over all **133 573** records:
|n·p + d| / scale : median 2.29e-08 p90 6.78e-08 max 2.15e-07
That is float round-off, not a fit — the relation holds to the last bits of a
32-bit float in **every record on the disc**. Three independent 100 % properties
(unit normal, point in bbox, trailing 1.0) and an exact algebraic identity are
well past what a wrong reading survives.
So a `REGN` object carries, alongside its uniform grid, a **point list** and a
**plane list** — which is the shape of collision or region-boundary geometry,
and consistent with `MCOL` sitting beside it.
### ❔ Still open
* **Section 1** (96 bytes/record, **63 410** records disc-wide) — a per-slot
census is below, but what the record *means* is still ❔.
* **What the planes are for.** "Collision or region boundary" is a reading of the
shape; nothing here shows what queries them.
* The **zeros at `[0..3]`** of every plane record, and the constant `96`-byte
tail after section 2, are unexplained.
## 🟡 Section 1, slot by slot — measured, but not read
**2026-08-26.** 96 bytes is 24 slots. Censusing every one of the **63 410**
records across all 11 objects gives a clear regional structure, even though the
record's purpose is not settled:
| slots | what the values are | reading |
|---|---|---|
| 03 | slots 2 and 3 are zero in ~100 %; 0 and 1 mostly small | ❔ |
| **46** | 96 % have \|v\| > 1 000, range **±250 000** — the header bbox range | 🟡 a **position** |
| **7** | 99.9 % > 1 000, always positive, 519 … 107 600 | 🟡 a **radius or extent** |
| 811 | as `f32` these are **denormals** (1.4 × 10⁻⁴⁵ upward) — i.e. they are **integers**, not floats | ✅ integer fields |
| 1223 | six **pairs**: the even slot 200 … 67 000 and never zero, the odd 0 … 212 000 and zero in 713 % | ❔ |
The denormal signature in slots 811 is worth stating plainly: a float field
never holds `1.4e-45`, so those four words are integers that a float reader
would silently turn into near-zero garbage.
A position plus a positive scalar plus integer links is the shape of a
**bounding-volume hierarchy node**, which would fit a file that also carries a
point list and a plane list. That is a reading of the shape and nothing more.
### ❌ The index test has no discriminating power
I tried to confirm the integer slots are indices by splitting each into two
`u16`s and checking them against each section's record count. The result is
useless, and the reason is worth recording:
| slot | halves valid for section 0 | section 1 | section 2 |
|---|---|---|---|
| 8 | 100.00 % | 100.00 % | 100.00 % |
| 9 | 100.00 % | 99.99 % | 100.00 % |
| 10 | 8.78 % | 44.19 % | 100.00 % |
| 11 | 8.76 % | 44.16 % | 100.00 % |
**A test that accepts every hypothesis rejects none.** Section 2 has tens of
thousands of records, so "is this `u16` below the plane count" is satisfied by
almost any small number — it measures the size of the section, not the meaning
of the field. Only slots 10 and 11 discriminate at all, and they merely rule
*out* section 0.
`slot 11`'s halves are consecutive (`n`, `n+1`) in 54 % of records and slot 10's
in 12 % — suggestive of paired links, but 54 % is not a rule and I am not
promoting it.
❔ So section 1 stays open. What would settle it is a test with power: pick a
record, follow a candidate index, and check that the thing it lands on is
spatially consistent with that record's own position and radius. That needs the
tree walked, not the fields counted.
## ❌ The BVH reading is refuted — and slot 7 is a *local* scale
**2026-08-26.** Last iteration I wrote that "position + positive scalar +
integer links is the shape of a bounding-volume hierarchy node", explicitly as a
reading of the shape. Tested with a design that has power, it fails.
**The containment test.** If slots 811 hold child indices, a child's sphere
should sit inside its parent's. Following every `u16` half of every integer slot
and checking `|C_child C_parent| + R_child ≤ R_parent` (2 % slack):
RANDOM control 0 of 126 787 = 0.00 %
slot 8 half 0 63 389 tried 0.00 %
slot 8 half 1 63 395 tried 0.00 %
…every candidate… 0.00 %
**The random control is the informative row.** It is also 0.00 %, which means no
node's sphere contains any other node's sphere *anywhere in the file* — so there
is no nesting for an index to point at, whatever the indices mean. The
hypothesis fails before the indices are even in question.
**Why**: slot 7 is far too small to bound neighbours.
| | median | p10 | p90 |
|---|---|---|---|
| slot 7 | **3 139** | 1 965 | 9 275 |
| pairwise centre distance | **45 457** | 19 071 | 126 295 |
Slot 7 is **14× smaller** than the typical distance between nodes, and a random
other centre falls within it only **0.40 %** of the time. It is also smaller
than the smallest grid cell on any map (10 000).
So ✅ **slot 7 is a local scale, not a hierarchy radius** — sub-cell sized, with a
narrow spread. Together with 63 410 scattered centres across a 500 km cube, that
is the shape of **many small independent volumes**, not a tree.
🟡 That fits per-object collision volumes — a map's asteroids and debris, which
the mission `Route` tables independently show as `Frame_S<NN>_Asteroid` records.
Stated as a reading, not a measurement; nothing here counts objects.
❔ Still open: what slots 811 index, and what the six pairs in 1223 are. What
this iteration removed is a wrong frame — the file is not a tree, so tree-shaped
tests will keep returning nothing.
## ❌ The cell index does not reference section 1 — two powered tests, both negative
**2026-08-26.** The obvious coupling in a file with a uniform grid and a list of
small volumes is that the grid indexes the volumes. It does not, by the only
test that could show it: **spatial agreement**.
**Test 1 — the cell payload as indices.** For every occupied cell, read its
32-byte payload record and try each `u16` in it as a section-1 index, then check
whether that node's centre lies **inside the cell that referenced it**:
RANDOM control 0.138 %
+0 0.08 % +2 0.18 % +4 0.11 % +8 0.08 %
+10 0.10 % +12 0.08 % +14 0.08 % …
Every field sits at the chance rate. **Nothing points at section 1.**
**Test 2 — the cell payload as coordinates.** 32 bytes is eight floats, so a
position could be in there. Trying every float triple and asking whether it lies
inside its own cell:
+0 0.81 % +4 0.81 % +8 0.15 % +12 0.16 % +16 0.26 % +20 0.78 %
Also chance.
### ⚠️ And the tempting number in that table is worthless
The same run reported those triples lie **inside the object's bounding box in
100.00 %** of cases, at five different offsets. That looks like a decode and is
not: the bbox spans the entire 500 km map, so *any* mid-range float triple
passes, and overlapping windows starting at +0 and +4 both scoring 100 % is the
tell — a real field would not survive being read at a four-byte shift. It
measures the size of the box, not the meaning of the bytes. This is the third
time in this file's investigation that a containment test against something
large has produced a meaningless 100 %, so it is recorded rather than quietly
dropped.
❔ So the grid and section 1 have no demonstrated link, and how a cell reaches
its geometry is unknown. What is now excluded: the cell payload holding
section-1 indices, and holding cell-local coordinates.
✅ Incidental, from the same sweep: `u32` slots at **+0, +8 and +12** of the
payload record are below `0x10000` in **100 %** of records, while +4, +16, +20
and +24 are in only 11 % and +28 never — so the record has three index-shaped
fields and four wide ones, whatever they refer to.
## ❌ …nor the point list, nor the plane list — the static coupling search is exhausted
**2026-08-26.** The previous test only tried section-1 targets, which left the
obvious gap: the payload's three index-shaped `u32`s might address the **points**
or the **planes** instead. Tested the same way — follow the index, ask whether
the target lies inside the cell that referenced it:
RANDOM control 0.203 %
+0 -> points 0.17 % planes 0.16 %
+8 -> points 0.81 % planes 0.09 %
+12 -> points 0.81 % planes 0.09 %
All at the control rate. The two 0.81 % cells are 4× the baseline, and I am
**not** treating that as a signal: across this and the previous iteration I have
now run on the order of twenty of these tests (three index fields × three
sections × two readings × several offsets), and at that count a single 4×
enrichment on ~8 000 trials is what noise looks like. Reporting it as a lead
would be exactly the multiple-comparisons error that a long hypothesis sweep
invites.
### Where this leaves `REGN`
✅ Decoded: the header and grid, section 0 (points), section 2 (planes), section
3 (the cell index), and the strides and field regions of section 1.
❌ Not decoded, and **not reachable by the tests available statically**: any link
between the grid and the geometry. Every index-shaped field has been followed
into every section and checked for spatial agreement, against controls, and
nothing rises above chance.
**What would actually settle it** is the code — find what reads a `REGN`
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.
---
# ✅ 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`, and every **record origin** on this page above is
therefore 16 bytes early.
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.
### 🔴 A correction to how I first stated point 1
The first version of this section said "everything on this page above was read
16 bytes early", which is **too broad and I withdraw it**. For the plane fields
it is *false*: the older reading put the section at `chunk + offset` and the
normal at intra-record `+16`, which addresses **exactly the same bytes** as
`chunk + 0x10 + offset` with the normal at `+0`. Its unit-normal and
`n·p + d = 0` results were measuring real planes and stand unchanged. On
`3506e972`, both conventions put face record 0's normal at file offset
**`0x1c700` (116 480)** = `0x10 + 0x1c6f0`, where the header word at `0x78` is
`0x1c6f0`.
What is genuinely wrong is the **record boundary**, and it matters for one
reason: it decides which 48-byte record the four integer words belong to — and
those integer words are the face adjacency, i.e. precisely the thing the
correlation search was hunting for. It also mis-resolves every stored pointer
by 16 bytes, twice over along the cell → item → ref → tetrahedron chain, which
is the mechanical reason that chain never landed on anything.
### ✅ Which tests actually decide the origin — and which have no power
Most of the numbers on this page are **content** tests and address the same
bytes under either convention, so they cannot settle the origin. Stated
plainly, because the same trap caught the point-in-bbox test:
| test | discriminates the base? |
|---|---|
| section-5 targets on 96-byte boundaries; ref-array packing | ❌ no — `target start` cancels the base |
| face passes through 3 of 4 vertices; portal-cost identity | ❌ no — same absolute bytes either way |
| sphere reaches its cell (98.7 % vs 1828 %) | ❌ no for the base; it discriminates the **axis order** |
| points inside the bbox (the older section) | ❌ no — a denormal reads as ≈ 0, which is inside a ±250 km box |
| **the loader's own `addi r3, r31, 16`** | ✅ direct |
| **where `POF0` says the header pointers are** | ✅ at `+0x10` the six relocated slots are exactly `0x70``0x84`, the six section pointers. At `+0` they would be `0x60``0x74`: the loader would relocate the `u16` **counts** and would leave the pointers at `0x78``0x84` unrelocated. Not merely wrong — non-functional. |
| **what section-0 record 0 contains** | ✅ at `+0x10` a real vertex (`-250000, 250000, -250000` on `d4c89536`); at `+0` the two remaining header pointer words read as floats — `2.2e-40, 2.7e-40, 0` |
| **the section-2 record boundary** | ✅ see below, 100.000 % against 0.000 % |
### ✅ The record boundary, settled
A tetrahedron names face `f`; the plane at record `f` is independently verified
(three of the tet's four vertices lie on it, 100 %). The question is whether the
adjacency block that says *"I am face `f`, my tetrahedra are A and B, at their
face slots i and j"* sits in the **same** 48-byte record as that plane.
Requiring `own == f` **and** `tet[A].faces[i] == f`:
adjacency block taken from record f (base + 0x10) : 100.000 %
adjacency block taken from record f + 1 (base + 0) : 0.000 %
11 of 11 objects, every face, both sides. The plane and the four integer words
that describe it are one record, and that record starts at `chunk + 0x10 +
offset + 48·f`.
`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<i].n`, exact, and the total equals
`counts[5]`;
* `counts[4]` equals the number of relocated section-3 pointers equals the
number of occupied cells;
* **the cell ordering is x-fastest, then y, then z.** A tetrahedron's bounding
sphere reaches the box of a cell that lists it in **98.7 100 %** of all
261 000 references; transposing the axes — the natural control — drops that
to **18 28 %**.
The list is a strict subset of "every cell the bounding sphere touches"
(Jaccard ≈ 0.4 1.0), i.e. the build used a tighter test than sphere-vs-box.
That is expected and is not claimed as decoded.
## ✅ Section 1 is a **tetrahedron** — 96 bytes
```
+0x00 f32[3] bounding-sphere centre
+0x0c f32 bounding-sphere radius
+0x10 u16[4] vertex indices → section 0
+0x18 u16[4] face indices → section 2
+0x20 6 × { f32 cost; f32 ❔ } one pair per face pair, in the order
(0,1) (0,2) (0,3) (1,2) (1,3) (2,3)
+0x50 u32 the record's own index
+0x54 u8[4] flags, values 0/1
```
**The check that makes this a decode, not a reading**: each of a tetrahedron's
four faces must pass through exactly three of its four vertices. Over
**253 722** (tetrahedron, face) pairs on the disc, the count of vertices lying
on the plane is **3 in 100.00 % of cases**. The control — the same test with a
randomly chosen face of the same object — gives 3 in **0.07 2.2 %**, and
touches no vertex at all 96 % of the time.
Two more, both 11 of 11: the bounding sphere encloses all four vertices in
100 % of the 63 410 records (with ~0.1 % padding), and the word at `+0x50` is
the record's own index in 100 %.
### ✅ The six floats at `+0x20` are the portal-graph edge costs
For face `i` of a tetrahedron, let `omit(i)` be the one vertex not on it. Then
cost[k] == | V[omit(i)] V[omit(j)] | / 3 for the k-th pair (i<j)
which is exactly the **distance between the centroids of faces `i` and `j`**
the A\* step cost of crossing the tetrahedron from one portal to another. It
holds to `1e-4` relative in **380 460 of 380 460** values, all 11 objects.
❔ The second float of each pair is unread. It is a genuine varied float, zero
in 8 26 % of slots. **🔴 Refuted**: "zero marks a hull edge" — `P(zero)` is
the same for edges on the hull as for interior ones (e.g. 2.9 % / 16.4 %
against 16.7 % / 64.0 % — no lift at all). A clearance width is the obvious
reading and is *not* established.
## ✅ Section 2 is a **face** — a plane plus the adjacency graph
```
+0x00 f32[3] unit normal
+0x0c f32 d
+0x10 f32[3] a point on the plane
+0x1c f32 1.0
+0x20 u16[3] the face's three vertex indices → section 0
+0x26 u16 the face's own index
+0x28 u16[2] the tetrahedra on either side; 0xFFFF = hull (no neighbour)
+0x2c u16[2] which of that tetrahedron's four face slots this is
```
The old page's plane reading was the same 16 bytes early, which is why it
reported "four zeros" at the front: those were the *previous* record's four
integer words, read as floats.
Verified over every face of every object (11 of 11, 100.00 % each):
* the word at `+0x26` is the record's own index;
* for each non-`0xFFFF` side, `tet[side].faces[slot[side]] == this face`;
* the face's three vertices are a subset of that tetrahedron's four.
And the Euler relation closes: `4·T == 2·(F B) + B` exactly on every object
(e.g. `4 × 1172 = 4688`, `F = 2584`, `B = 480` hull halves).
## ✅ What a `REGN` object is
A stage's `MapPath` is a **tetrahedral navigation mesh of the whole play
volume**, plus a uniform grid for point location:
* vertices, tetrahedra, and faces form a conforming tet mesh with full
face adjacency and hull markers;
* each tetrahedron carries the six face-to-face traversal costs, so A\* over
the portal graph needs no geometry at query time;
* the uniform grid answers "which tetrahedra could contain this point" in O(1),
which is what a flight game needs to localise a ship into the mesh each frame.
The 100 km-cube smoke-test map (`e993b93e`, `counts = [8, 6, 18, …]`) is the
textbook **six-tetrahedron decomposition of a cube** — 8 corners, 6 tets, 18
distinct faces (24 faces less the 6 shared internally) — which is as strong a
confirmation of the reading as the statistics are.
## 🔴 Corrections to the sections above, kept rather than deleted
Every one of these was a real observation; each fails for the same single
reason, and that is worth having on the page.
| earlier claim | status |
|---|---|
| "four section offsets at `0x70`" | 🔴 there are **six**, `0x70``0x84` |
| "the first section offset is always `0x80`" | 🔴 the *value* is `0x80`; the section is at `0x90` — the base is `+0x10` |
| "`counts[4]` = occupied cells **+ 2**, unexplained" | 🔴 artefact of the base error; `counts[4]` = occupied cells exactly |
| "the constant 96-byte tail after the plane list" | ✅ explained: two all-zero sentinel face records |
| "record `+0x1c` is a bounding-sphere radius (ratio 1.001 on one object, 0.130.27 on the rest)" | 🟡 **half right**`+0x0c` *is* a bounding-sphere radius, of the *tetrahedron*; the 1.001 was a coincidence of that map, where every tet shares the cube's circumsphere |
| "`(count, offset)` pairs point at leaf arrays of `count × 4` bytes" | 🟡 **right mechanism, wrong record** — it is the cell *item* at `+0x10`/`+0x14` pointing into section 5, not the cell index |
| "slots 811 are integers (denormal as float)" | ✅ correct, and they are the four vertex + four face `u16` indices |
| "section 2 records are 48 bytes, unit normal, `n·p + d = 0` in 133 573/133 573" | ✅ **stands** — the same bytes under either origin convention; only the record boundary moves, see the correction above |
| "section 0 points are inside the bbox, 13 467/13 467" | ⚠️ true but **uninformative** — it passes at either origin, because a denormal reads as ≈ 0 and 0 is inside the box |
| "slot 7 is a local scale, ~14× smaller than the inter-node distance" | ✅ correct as measured — it is a *tetrahedron's* bounding radius, which is exactly that small |
| "the BVH reading is refuted, no node sphere contains another" | ✅ stands, and is now explained: these are sibling tetrahedra, not a tree |
| "the cell payload holds section-1 indices" | ✅ stands — it holds a **pointer to an array of pointers**, and no index anywhere |
| "the static-correlation avenue is exhausted" | ✅ stands for correlations; the file's own fixup table was never a correlation |
## ❔ Still open
* the second float of each portal pair at tetrahedron `+0x24`, `+0x2c`, … ;
* the four flag bytes at tetrahedron `+0x54` (values 0/1);
* the exact predicate the tools used to decide cell membership (tighter than
sphere-vs-box);
* **the runtime consumer.** The deserialiser is found and proven; the code that
*queries* the grid is not. Searches that came up empty, so they are not
repeated: functions loading the header field groups off one base register
(only stack frames match); `vctsxs`/`vctuxs` float→int conversion (16
functions, only one outside the XDK ranges); and the magic-comparison route,
which cannot exist (above). The likely reason the field-offset search fails
is that a `float4`-aligned header is read with VMX loads, which carry no
useful displacement signature.
## Tooling
`tools/re-capture/regn_decode.py` — standalone, stdlib only.
```
regn_decode.py list hidden/MiscBin.pak
regn_decode.py dump hidden/MiscBin.pak [name_hash]
regn_decode.py verify hidden/MiscBin.pak # every check quoted above
```
`verify` prints, per object: the `POF0` pointer-slot shape, the reference-array
packing, the face/vertex incidence with its random control, the sphere/cell
agreement with its transposed-axis control, and the portal-cost identity.