Nothing here changes what a tool computes; it changes where tools look. - tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has existed nowhere since /work became a clone, so they matched nothing and printed empty results. They now resolve the disc through a new disc.py from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised). Nine scripts that imported siblings from the retired Reborn checkout or an old session scratchpad now import from their own directory. unitgroup.py only needs the variable when --pak is not given. - sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead devkit key and a doc comment claiming a devkit fallback that does not exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either. - sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so always skipped. It reads $SYLPHEED_DISC now, and passes against the disc. - Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe as places to look now name sylpheed.db, Canary's ppc_context.h and the flat .pe; docs/re/README.md no longer says the native Canary build does not run. Historical records keep their original paths: findings that were measured against /work/xenia-rs/sylpheed.db still say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
720 lines
35 KiB
Markdown
720 lines
35 KiB
Markdown
# `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: 878–998 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 0–12 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 |
|
||
|---|---|---|
|
||
| 0–3 | slots 2 and 3 are zero in ~100 %; 0 and 1 mostly small | ❔ |
|
||
| **4–6** | 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** |
|
||
| 8–11 | as `f32` these are **denormals** (1.4 × 10⁻⁴⁵ upward) — i.e. they are **integers**, not floats | ✅ integer fields |
|
||
| 12–23 | six **pairs**: the even slot 200 … 67 000 and never zero, the odd 0 … 212 000 and zero in 7–13 % | ❔ |
|
||
|
||
The denormal signature in slots 8–11 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 8–11 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 8–11 index, and what the six pairs in 12–23 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 (the flat `.pe` in the project root, 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 18–28 %) | ❌ 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.13–0.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 8–11 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.
|