# The asteroid-field tables (`AsteroidGroup_00N`) **Status: ✅ decoded statically, 2026-08-28.** Every number below is an exact disc census over all 41 `.pak` archives using the corpus reader. ## Where it sits in the chain [[stage-definition-table]] and [[stage-mission-tables]] already record that a stage's `Phase_N` block carries an **`AsteroidDefinition`** field naming a `.tbl` (`test_s8p1_asteroid.tbl` in the `_Test` template, `""` in most phases), and [[unit-datasheet-static]] already partitions the `Generic` records and finds **10 per pack whose only field is `{EnumAsteroidGroup}` — "an asteroid-group file"**. What neither says is what is *inside* those ten files. ``` Stage_SNN . Phase_N . AsteroidDefinition = "S04_asteroid.tbl" └── that object: Generic . EnumAsteroidGroup = "7" AsteroidGroup_000 … AsteroidGroup_006 AsteroidModelName = mob_n050 AsteroidFrameName = Frame_S04_Asteroid_01 EnumAsteroid = 39 ``` ## ✅ The table **10 objects per pack × 6 `GP_MAIN_GAME_` packs = 60**, holding **384 `AsteroidGroup_*` records** in total. Each object has **4 or 7** groups (12 objects with 4, 48 with 7). Every group record carries exactly **three** fields, present on all 384 with no partials: | field | distinct values | what it is | |---|---:|---| | `AsteroidModelName` | 28 | the mesh — `mob_n050`…`mob_n056` (24 uses each) and `mob_n040`…`mob_n043` (18 each) | | `AsteroidFrameName` | 64 | `Frame_S[_pN]_Asteroid_NN` — the placement frame | | `EnumAsteroid` | 37 | a **count**, 1 … 588; 6 633 asteroids per pack summed | `Frame_S_Asteroid` is the same name [[regn-map-grid]] reports the mission `Route` tables showing independently — the frame is the join to the map data. ## ✅ `Enum` is a count; `Enumerate` names a table `EnumAsteroidGroup` equals the number of `AsteroidGroup_*` records in its own object in **54 of 60** objects, and `EnumAsteroid` is a plain integer. That is the opposite of the `Enumerate*` convention, where the value is a child table's file name. **The `-erate` is the difference**, and it is worth checking the spelling before assuming which kind a field is. ⚠️ `Count`-style fields still are not safe: the **6 failures are one object, repeated once per language pack** — it declares `4` and contains `7`. ## 🔑 The tenth object is unreferenced, and it is the one that miscounts Joining the ten objects to the nine `AsteroidDefinition` values by frame prefix: | object's frame prefix | groups | declared | named by | |---|---:|---:|---| | `Frame_S01_Asteroid` | 4 | 4 | `S01_asteroid.tbl` | | `Frame_S04/S05/S13/S14_p1/S14_p2/S08_p1/S08_p2_Asteroid` | 7 | 7 | the matching `SNN[_pN]_asteroid.tbl` | | `Frame_Test_Asteroid` | 4 | 4 | `test_s8p1_asteroid.tbl` | | **`Frame_Alpha_S01_Asteroid` + `Frame_Alpha_S01_Haze` + `Frame_TAlpha_S01_Asteroid`** | **7** | **4** | **nothing** | Nine `AsteroidDefinition` names, nine matching objects, and a **tenth that no phase names** — an `Alpha` Stage-01 variant with its own `Haze` frames and a `TAlpha` set. It is also the only object whose declared count is wrong, which is what an abandoned table looks like. ## 🔑 S28 has an asteroid *volume* but no asteroid *definition* [[mcol-collision]] lists **nine** `_AsteroidVolume_wp` collision meshes: S01, S04, S05, S08_p1, S08_p2, S13, S14_p1, S14_p2 and **S28_p1**. The nine `AsteroidDefinition` values are the same eight stages plus **`test_s8p1`**. So the two sets share eight and differ by one each: **`S28_p1` has the collision volume for an asteroid field but no table saying which rocks to scatter in it**, while `test_s8p1` has a table with no volume of its own. ## ✅ `EnumAsteroid` is the frame record's `FrameCount` — 57/57 (2026-08-28) Each `AsteroidFrameName` is itself an **IDXD record name**. Those records carry one named field, **`FrameCount`**, and a run of anonymous ones: * **`EnumAsteroid` equals that record's `FrameCount` in 57 of 57** frames that have a record — **zero mismatches**, and the map is single-valued for all 64 frame names. * The anonymous fields number **exactly 8 × `FrameCount`, on all 384 occurrences** (ratio histogram: `{8.0: 384}`). So the frame record is a table of **8 values per asteroid** — the placement data — and `EnumAsteroid` counts the rocks. [[mission-wave-arrivals]] already met these records: they were the outliers that broke its route-speed measurement, and it filtered them out by name, noting "the corpus already records that the 8-value frame is not universal". [[regn-map-grid]] already guessed the link — "a map's asteroids and debris, which the mission `Route` tables independently show as `Frame_S_Asteroid` records… stated as a reading, not a measurement; nothing here counts objects." **What neither says is that the count is exact.** Now it is measured. 🔑 **The abandoned table lost its placement data too.** Exactly **7 of the 64** frame names have no record, and they are exactly the seven groups of the unreferenced object: `Frame_Alpha_S01_Asteroid_{01,02,04}`, `Frame_Alpha_S01_Haze_{01,02,03}`, `Frame_TAlpha_S01_Asteroid_03`. `57 + 7 = 64`, zero partials on either side. ## ✅ The name→object join DOES reproduce by hash — with the path prefix Withdrawing the 🔴 from the first write-up. `AsteroidDefinition` values resolve to their objects' pak entry keys as **`name_hash("stage\" + name)`**, **9 of 9**, each landing on the object the frame-prefix join independently predicted. My first attempt hashed the bare file name; [[archive-naming]]'s resolver hashes `prefix + name` over a list of path prefixes, and `stage\` is the one that works here. (`Stage\` scores identically — the hash lower-cases.) **Two independent joins, 9/9 agreement**, and both leave the same tenth object unreferenced. ⚠️ Sharper still, found a turn later: **[[stage-mission-tables]] already says it in words** — "The prefix is `stage\` — `name_hash` is case-insensitive, so its case is free" — and its `stagetbl.py` examples pass `'stage\AIParams_S02.tbl'`. The answer to my "🔴 does not reproduce by hash" was written in the doc that owns the stage tables. ## ✅ The 8 values are `index, quaternion(x,y,z,w), position(x,y,z)` (2026-08-28) Measured over every `Frame_*Asteroid*` record on the disc — **384 records, 37 518 items** (6 253 per language pack × 6; the other 380 per pack belong to the seven record-less `Alpha` frames). 🔴 **The layout itself is not new** — [[stage-mission-tables]] already documents `(time, quat.x, quat.y, quat.z, quat.w, pos.x, pos.y, pos.z)` for `Route_*`, and [[unit-group-table]] already documents `1 + 8·FrameCount` for formations. I wrote this up as if the shape were a discovery and called slot 0 an "index" without checking; the correction and the disc-wide census are in [[stage-mission-tables]]. What the asteroid measurement does add is the **norm test** and the fact that this family sets `time` to the ordinal. One raw item, for shape: ``` 0 0.268744 0.505919 0.579670 0.579487 15667.14 -7199.61 -58794.88 1 -0.053496 -0.540765 -0.260996 0.797868 -43571.37 -60583.68 -3417.80 2 -0.696396 0.682371 … ``` | slot | test | result | |---|---|---| | 0 | equals the running index `0…N−1` | **37 518 / 37 518**, max **587** — and the largest `EnumAsteroid`/`FrameCount` is **588**, so the last index is `N−1`. ⚠️ This slot is the **`time`** of the shared keyframe layout ([[stage-mission-tables]]); the asteroid tables, having nothing that moves, set it to the ordinal — see the disc-wide census there | | 1–4 | is it a **unit quaternion**? `\|q\|` | **37 518 / 37 518 within 1e-4 of 1.0** (min 0.999999, max 1.000001) | | 4 | sign of the real part | min **+0.001**, never negative — the canonical **w-last** form, so the order is `(x, y, z, w)` | | 5–7 | position, metres | ranges **−163 858…+79 572**, **−114 308…+76 422**, **−105 224…+110 263** — inside the ±250 km half-extent of `mapmesh_box_500km` | And `len(anonymous fields) == 8 × FrameCount` on **384 of 384** records, zero mismatches. The unit-norm test is the control: an arbitrary 4-tuple has no reason to sit on the unit sphere, and **every one of the 37 518 does**. World unit = 1 metre, so these are metres from the map origin. **There is no scale slot** — an asteroid's size comes from its group's `AsteroidModelName`, which is why one group is one model plus one placement list. ## 🔴✅ CORRECTION — S28 does have a definition: it borrows Stage 14's (2026-08-28) Withdrawing "`S28_p1` has an asteroid volume with no definition". That came from a set-difference over **file names**, which cannot see reuse. Joining the fields **per phase** instead: | table | the phases that declare it | |---|---| | `S01_asteroid.tbl` | S01.Phase_1, S01.Phase_2 | | `S04_asteroid.tbl` | S04.Phase_1, S04.Phase_2 | | `S05_asteroid.tbl` | S05.Phase_1, S05.Phase_2 | | `S08_p1` / `S08_p2` | S08.Phase_1 / S08.Phase_2 | | `S13_asteroid.tbl` | S13.Phase_1 | | `S14_p1_asteroid.tbl` | S14.Phase_1 | | **`S14_p2_asteroid.tbl`** | **S14.Phase_2 *and* S28.Phase_1** | | `test_s8p1_asteroid.tbl` | **S02.Phase_1** | **S28.Phase_1 declares `S14_p2_asteroid.tbl` while using its own `S28_p1_AsteroidVolume_wp` mesh** — it scatters Stage 14's rock list inside its own volume. Nine tables, nine volumes, one table shared by two phases. ⚠️ The earlier census's own arithmetic contained this: the four values counted **×12** (2 per language pack) were `S01`, `S04`, `S05` — two phases of one stage — and `S14_p2`, which is **two phases of two different stages**. Reading ×12 as "two phases of the same stage" was the error. ⚠️ **`test_s8p1_asteroid.tbl` is not test-only either** — it is what **S02.Phase_1** ships with. A `test_` name here is a leftover, not a dead table. **The true residual is two phases, and it is the opposite pairing:** `S01.Phase_3` has the volume and no definition (S01 keeps its volume across all three phases but scatters rocks only in 1–2), and `S02.Phase_1` has a definition and no volume (its `MapMesh` is the plain `mapmesh_box_500km`). ## 🟡 Not settled * Whether the quaternion is object→world or world→object, and its handedness — the norm test cannot tell them apart.