Files
Sylpheed/docs/re/structures/asteroid-fields.md
Sylpheed RE agent 027e6bb7a7 re: Placement_*/RouteTest_* are Null_Test.tbl, and Enumeration is a self-index
The template object is stage\Null_Test.tbl: its key hashes as
name_hash("stage\" + "Null_Test.tbl"), the file stage-definition-table names as
EnumerateNullFrame. Ten records - 4 Frame_Test_Asteroid_01..04, 3
Placement_00..02, 2 RouteTest_00..01, Enumeration - and the nine carrying a
FrameCount are exactly the "9/9 for Null_Test.tbl" stage-mission-tables already
counted. The corpus knew the file and the count, not the record names. A second
object (the same five plus Enumeration, without the asteroid frames) matched
none of ten candidate names across sixteen prefixes.

Enumeration is a record that indexes its own object. Disc-wide: 7750 IDXD
objects, 360 carry one, and in 360/360 its field names equal exactly that
object's other record names - zero subsets, zero strays. Field/record counts
run from 2 past 25 ((3,3)x36, (9,9)x24, (13,13)x24, (2,2)x24, (25,25)x18,
(16,16)x12), every pair on the diagonal.

Enumeration vs Enumerate is one letter and opposite scope: Enumerate is
archive-naming's route-2 seed, a single-record object whose fields name OTHER
objects; Enumeration names the records of its own object. Written up in
idxd-container, which had left "an object's kind is known only from the caller
that loads it" open.

Sharper version of the previous correction: I credited the stage\ prefix to
archive_naming.py, but stage-mission-tables states it in prose ("The prefix is
stage\ - name_hash is case-insensitive") and its stagetbl.py examples pass
'stage\AIParams_S02.tbl'. Noted in asteroid-fields.md.

All fifteen artefacts byte-identical.
2026-08-28 02:36:20 +00:00

8.7 KiB
Raw Blame History

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_<lang> 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_n050mob_n056 (24 uses each) and mob_n040mob_n043 (18 each)
AsteroidFrameName 64 Frame_S<NN>[_pN]_Asteroid_NN — the placement frame
EnumAsteroid 37 a count, 1 … 588; 6 633 asteroids per pack summed

Frame_S<NN>_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<Thing> is a count; Enumerate<Thing> 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<NN>_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 newstage-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…N1 37 518 / 37 518, max 587 — and the largest EnumAsteroid/FrameCount is 588, so the last index is N1. ⚠️ 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
14 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)
57 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.

🟡 Not settled

  • Why S28_p1 has an asteroid volume and no definition (S28 is the challenge mission that alone flies rou_f004, see stage-numbering-and-player-craft).
  • Whether the quaternion is object→world or world→object, and its handedness — the norm test cannot tell them apart.