Files
Sylpheed/docs/re/structures/regn-map-grid.md
sim e909c7c133 chore: retire the last dead paths and names from the consolidation
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>
2026-09-16 22:30:28 +02:00

35 KiB
Raw Permalink Blame History

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, 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) 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 u16s 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 u32s 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 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 0x700x84, the six section pointers. At +0 they would be 0x600x74: the loader would relocate the u16 counts and would leave the pointers at 0x780x84 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 orderitem[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, 0x700x84
"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.