Read as 8 big-endian words, word 0 as four bytes is (x,y,z,1) -- a 3-D cell index, matching the object's 5x5x5 grid. Words 4-6 are a position and word 7 a positive scalar (a bounding sphere); word 1 is a count, word 2 the relocated pointer. Every record pointer lands in the same region, 8976/8976, each a fixed distance on with the same stride -- so there are two parallel arrays, A and B. The 50% is the tell. Three independent criteria -- byte 3 == 1, valid cell index, sphere reaches that cell -- all land on 50.0%, which says half the records are not this type rather than that the reading half-works. The POF0 slot list interleaves both arrays and I was testing B's records against A's layout. So array A is a per-cell record, the same role REGN's section 3 plays. Array B is unread, and the criteria have not yet been re-run on A alone -- if the reading is right they should go to 100%.
133 lines
6.1 KiB
Markdown
133 lines
6.1 KiB
Markdown
# `MCOL` — the same container as `REGN`, and the same map parameters
|
||
|
||
**🟡 Opened 2026-08-26.** `MCOL` sits beside `REGN` in `hidden/MiscBin.pak`, 11
|
||
of each, and has never been decoded. This page establishes what it shares with
|
||
`REGN` — which is a lot, and gives the next attempt a large head start.
|
||
|
||
## ✅ Same container
|
||
|
||
Over all **11** objects:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `POF0` fixup table at `data_size@+4` + 16 | **11 / 11** |
|
||
| bbox pad words are `1.0` / `1.0` / `0.0` at `+0x1C`, `+0x2C`, `+0x3C` | **11 / 11** |
|
||
| `extent == max − min` for the `0x30` block | **11 / 11** |
|
||
|
||
So the header prefix is byte-for-byte the same shape as `REGN`'s: magic, data
|
||
size, then bbox min / bbox max / extent as `f32[4]`, then a triple at `0x40`.
|
||
The `POF0` mechanism applies, which means **the `chunk + 0x10` base and the
|
||
loader's own pointer list are available here too** — the two things that cracked
|
||
`REGN`.
|
||
|
||
## ✅ And the same map parameters, exactly
|
||
|
||
The bounding boxes and the `0x40` triple are not merely similar — the
|
||
distributions are **identical**:
|
||
|
||
| | `MCOL` | `REGN` |
|
||
|---|---|---|
|
||
| bbox ±250 000 | 2 | 2 |
|
||
| bbox ±50 000 | 6 | 6 |
|
||
| bbox ±25 000 | 3 | 3 |
|
||
| `0x40` = 50 000 | 2 | 2 |
|
||
| `0x40` = 10 000 | 9 | 9 |
|
||
|
||
Eleven maps, and for each one an `MCOL` and a `REGN` describing the same volume
|
||
at the same cell size. `0x40` is the **cell size** in `REGN`; the same values in
|
||
the same multiplicities here is strong evidence it is the cell size in `MCOL`
|
||
too — though note this is a match of *distributions*, not a demonstrated
|
||
object-to-object pairing, which would need the two linked by name or by a stage's
|
||
tables.
|
||
|
||
## ❔ What is not yet known
|
||
|
||
* **Everything past `0x40`.** `MCOL`'s words at `0x50`–`0x84` do **not** look
|
||
like `REGN`'s (`REGN` has grid dims at `0x50`, six `u16` counts at `0x60` and
|
||
six section pointers at `0x70`; `MCOL` has a large value, two mid-range values
|
||
and `112` at `0x50`, mostly zeros at `0x60`, and `0x05050501` at `0x70`). The
|
||
headers agree on the spatial prefix and diverge after it.
|
||
* **Everything past `0x40`** — but see below; the pointer layout is now known.
|
||
|
||
|
||
## ✅ The pointer layout, from `POF0`
|
||
|
||
**2026-08-26.** Running the known-good decoder (`regn_decode.py` on
|
||
`auto/regn-reader`) rather than my own broken one. Sanity check first: on `REGN`
|
||
it returns header slots `0x70`–`0x84` exactly — the six section pointers — so the
|
||
tool and my use of it are right.
|
||
|
||
On `MCOL`, over all **11** objects:
|
||
|
||
| | |
|
||
|---|---|
|
||
| header-region relocated slots are **exactly `0x54`, `0x58`, `0x5C`, `0x74`** | **11 / 11** |
|
||
| slot `0x5C` resolves to **`0x80`** — the first byte after the header | **11 / 11** |
|
||
| slot `0x74` resolves to **(first array pointer − 8)** | **10 / 11** |
|
||
|
||
So `MCOL` has **four** top-level pointers where `REGN` has six, and one of them
|
||
(`0x5C`) always addresses the data immediately following the header.
|
||
|
||
**The bulk of the relocations form record arrays.** 92.7 % of the gaps between
|
||
consecutive relocated words are **32 bytes**, arranged in 7–127 contiguous runs
|
||
per object. Combined with `0x74` landing 8 bytes before the first of them, the
|
||
reading is an array of **32-byte records each carrying one pointer at `+8`**.
|
||
|
||
What the four targets look like:
|
||
|
||
0x5C -> 0x80 c685620b 4596789d 4694b3b5 44a9a634 floats
|
||
0x54 -> … c6826964 456b1aa4 469ab065 c685cfe8 floats
|
||
0x58 -> … 00000001 00020003 00040005 00050004 small ints / u16 pairs
|
||
0x74 -> … 00000001 00000001 00007710 00000000 counts, then the array
|
||
|
||
Two float blocks, an index block and a record array is the shape of a mesh —
|
||
which is what a name like `MCOL` beside a navigation mesh would suggest. 🟡 That
|
||
is a reading of the shape; none of the four blocks has been decoded.
|
||
|
||
❔ Still open: the record layout, what the index block indexes, the **one object
|
||
in eleven** where `0x74` does not land 8 before the array, and the 7.3 % of gaps
|
||
that are not 32 (they are the boundaries between runs, but that has not been
|
||
checked).
|
||
|
||
## 🟡 The 32-byte record — a cell entry, and there are two interleaved arrays
|
||
|
||
**2026-08-26.** Reading each record as 8 big-endian words (record start =
|
||
pointer slot − 8), the first entries of the smallest object are:
|
||
|
||
@0x6780 00000001 00000001 00007710 00000000 00000000 00000000 00000000 47295092
|
||
@0x67A0 01000001 00000001 00007730 00000000 …
|
||
@0x67C0 02000001 00000001 00007750 00000000 46023555 C6023555 C6023555 471FA1A7
|
||
@0x6820 00010001 00000001 000077B0 00000000 …
|
||
|
||
Word 0 read as **four bytes** is `(x, y, z, 1)` — a **3-D cell index**. That
|
||
object's grid is 5×5×5 (bbox ±25 000, cell 10 000), and the values run 0–4 in
|
||
the first byte and step the second byte at the right point. Words 4–6 are a
|
||
position and word 7 a positive scalar — a bounding sphere. Word 1 is a count and
|
||
word 2 the relocated pointer.
|
||
|
||
**Every record pointer lands in the same region — 8 976 / 8 976 (100 %)** — and
|
||
each points 0xFA0 further on with the same stride, so there are **two parallel
|
||
arrays**, not one: array A at `0x6780` and array B at `0x7720`.
|
||
|
||
### The 50 % is the tell, not a failure
|
||
|
||
Testing the cell-index reading over *all* relocated records gives almost exactly
|
||
half:
|
||
|
||
byte 3 == 1 4 491 / 8 976 (50.03 %)
|
||
bytes 0..2 a valid cell index 4 488 (50.00 %)
|
||
the record's sphere reaches that cell 4 485 (49.97 %)
|
||
|
||
Three independent criteria all landing on 50.0 % is not a partial fit — it says
|
||
**half the records are not this type**. The `POF0` slot list interleaves both
|
||
arrays, and I was testing array B's records against array A's layout. Reported as
|
||
a rate it would read like a half-working hypothesis; split by array it is two
|
||
clean populations.
|
||
|
||
🟡 So array A is a **per-cell record** — cell index, count, pointer into array B,
|
||
bounding sphere — the same role `REGN`'s section 3 plays. ❔ Array B's layout is
|
||
unread, and the split has not yet been re-run per array to confirm 100 % on A.
|
||
|
||
**Next step**: separate the two arrays by their start addresses (`0x74` → A,
|
||
A's pointers → B), then re-run the three criteria on A alone. If the reading is
|
||
right they go to 100 %. |