diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index e5b33059..4428cb38 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -122,7 +122,7 @@ files, which is how the same ground got covered twice. | [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — | | [`structures/asteroid-fields.md`](structures/asteroid-fields.md) | `AsteroidGroup_00N` — the asteroid-field tables a `Phase_N.AsteroidDefinition` names | ✅ 10 objects/pack, 384 group records, 3 fields each (`AsteroidModelName`, `AsteroidFrameName`, `EnumAsteroid` = **the frame record's `FrameCount`, 57/57**, and the frame holds **`index, quaternion(x,y,z,w), position(x,y,z)`** per asteroid — 37 518 items, unit norm 37 518/37 518); `Enum` counts while `Enumerate` names a table (54/60); the join resolves as `name_hash("stage\" + name)` 9/9; 🔑 a tenth `Frame_Alpha_S01_*` object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 **S28.Phase_1 borrows `S14_p2_asteroid.tbl`** — the per-phase join shows 9 tables ↔ 9 volumes with one table shared | | [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == max−min` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ | -| [`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 b7d2f499..d426acb3 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 @@ -131,23 +139,6 @@ thing is indexed *by*. partition and no more; the sections are unread. Treat this as the location of the world's spatial data, not as the wave table. -> ❌ **SETTLED 2026-08-26 — `REGN` is not the wave scheduler, and cannot be.** -> It is a **tetrahedral navigation mesh**: vertices, faces with plane equations -> and adjacency, tetrahedra with portal costs between their face pairs, and a -> grid that indexes which tetrahedra fall in each cell. Every section is -> accounted for by that structure. There is nowhere in it for "what spawns, when, -> on what trigger" to live — no time field, no unit reference, no trigger. -> -> The hedge above was right to keep the reading provisional, but the reasoning -> it hedged ("a grid is what a scheduler would be indexed by") was a **guess from -> shape**, and the shape belonged to pathfinding. Worth noting because that guess -> is what pointed this whole investigation at `REGN` in the first place. -> -> The wave-scheduler search should now treat `REGN` as **excluded**, not as -> unread. The arrival timetable already found in `Route_S.tbl` — keyframed -> `(time, quat, pos)` per squadron per phase, with `t` in seconds — remains the -> only located part of that mechanism. - ## ✅ Update: `REGN` is a stage's **MapPath** The per-stage definition record (see @@ -391,119 +382,338 @@ 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. +--- -## ⚠️ Open conflict with `auto/regn-reader` over the `+0x10` base +# ✅ 2026-08-26 — the reader found: `REGN` is a **tetrahedral navigation mesh** -**2026-08-26.** Branch `auto/regn-reader` decodes `REGN` as a **tetrahedral -navigation mesh** and reports the cell→geometry link solved, with strong checks -(faces passing through 3 of 4 tet vertices, 253 722/253 722; portal cost equal to -the distance between face centroids, 380 460/380 460). Its load-bearing structural -claim is that the `POF0` fixup base is **`chunk + 0x10`**, and therefore that -"every offset previously recorded on that page was read 16 bytes early" — offered -as the reason the ~20 correlation tests on this page returned chance. +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. -**I could not reproduce that as stated, on the one thing here that is -independently checkable.** Re-reading the plane list at both bases: +## ✅ The reader — `sub_82465110` / `sub_82465138` / `sub_82465200` -| | records | unit normals | -|---|---|---| -| section at `chunk + offset` (what this page used) | 133 573 | **133 573 (100.00 %)** | -| section at `chunk + 16 + offset` | 133 573 | **0 (0.00 %)** | +Three functions, all in the resource module around `0x82460000`: -And the plane identity `n·p + d = 0` holds to float round-off at the **unshifted** -base. A 16-byte shift destroys it completely. So for *this* record the unshifted -reading is right, and the blanket statement is not. +``` +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 +``` -🟡 **The likely reconciliation is bookkeeping, not disagreement.** The other -branch describes a 48-byte **face** record whose plane fields sit at a different -intra-record offset; `chunk+16` with the plane at `+0` addresses the same bytes as -`chunk+0` with the plane at `+16`, which is exactly what this page uses. If so -both readings are correct and only the origin convention differs — but that is a -guess, and I am not adopting either page's wording until it is checked. +``` +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 +``` -**Also confirmed here, incidentally**: my section-0 point test passes at **100 % -at both bases**, so it never had the power to distinguish them. That test could -not have caught a 16-byte error and should not be cited as if it validated the -offsets. +``` +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 +``` -❔ Until the conflict is resolved, treat the plane/face layout on this page as -verified by its own arithmetic and the tetrahedral decode on `auto/regn-reader` -as verified by its own — and do not merge the two offset conventions without -re-running one check from each. +Two things follow, and both are load-bearing: -## ✅ RESOLVED — the base IS `chunk + 0x10`, and my section-0 offsets were wrong +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. -**2026-08-26, same day.** The conflict above is settled, and it settles against -me on the boundary while leaving the plane arithmetic intact. +### 🔴 A correction to how I first stated point 1 -**The plane fields were never in dispute.** For object `3506e972`, the other -branch's face record 0 begins at `0x1c700`, and its plane normal begins at the -same address; my `chunk + offset_at_0x78 + 16` gives `0x1c700` too. **Same bytes, -different bookkeeping** — exactly the reconciliation guessed above. The -`n·p + d = 0` result stands unchanged. +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`. -**The base is `chunk + 0x10`**, on evidence that has power where my tests did -not: +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. -* the loader does it: `8246519c addi r3, r31, 16`; -* at `+0x10` the six `POF0`-relocated slots land exactly on `0x70`–`0x84`, the six - section pointers. At `+0` they would land on `0x60`–`0x74` — relocating the - `u16` **counts** and leaving two section pointers unrelocated. Not merely - wrong, non-functional; -* section-0 record 0 reads as a bbox corner at `+0x10` and as garbage at `+0`. +### ✅ Which tests actually decide the origin — and which have no power -### ❌ And my point-list evidence was vacuous +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: -I reported "13 467/13 467 section-0 points lie inside the bbox" as the check that -made section 0 a decode. Read at the correct base, record 0 of four objects is: - - base +0 (1.4e-41, 5.3e-41, 0) (2.2e-40, 2.7e-40, 0) - base +16 (-50000,-50000,-50000) (-250000, 250000, -250000) - -So my offsets were 16 bytes early throughout. **Only 11 of 13 467 points read as -denormal at the wrong base** — the rest were still plausible coordinates inside -the box, because a 16-byte shift within a packed array of `f32` triples yields -*other floats from the same array*. That is the general form of the failure and -it is worth stating plainly: - -> **A containment test cannot detect a shift inside a homogeneous array.** The -> shifted values are drawn from the same distribution as the correct ones, so the -> test passes at 100 % either way. It is not a weak check — for this class of -> error it is no check at all. - -The conclusion "section 0 is a point list" happens to be right. The evidence I -gave for it was not evidence. - -✅ **Adopt `chunk + 0x10`.** The plane *fields* need no change; the record -*boundary* moves, and the four integer words I described as "four zeros" move -with it into the record they describe — which is where the face adjacency, and -the answer to the cell→geometry question, was hiding. See branch -`auto/regn-reader` for the tetrahedral decode. - -## ✅ Independent confirmation of the face decode - -**2026-08-26.** I re-derived the other branch's central check with my own code, -my own reading of the record, and my own control — rather than accepting the -number. - -Reading at base `chunk + 0x10`, taking the plane at intra-record `+0` and the -**three `u16` vertex indices at `+32`**, and asking whether each named vertex -satisfies its own face's plane equation: - -| | | +| test | discriminates the base? | |---|---| -| faces where **all three** named vertices lie on the plane | **133 573 / 133 573 (100.00 %)** | -| a **random** vertex lying on that plane (control) | 2 782 / 400 719 = **0.69 %** | +| 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 % | -A 100 % against a 0.69 % baseline is not a fit — the face record genuinely names -the vertices of its own plane, and the `+0x10` base together with this layout is -right. +### ✅ The record boundary, settled -That also settles the record-boundary question from my side: the `u16`s that -describe a plane sit **after** it in the same 48-byte record. My earlier reading -of "four zeros at the start of each record" was those integers seen 16 bytes out -of position, one record late. +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. -This is worth having in the corpus independently of the other branch: two -different implementations, two different guesses at the intra-record layout, the -same 100 %. \ No newline at end of file +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