Correction to the previous commit's own result. The raw byte sweep reported rot_n001 "26 times in 7 archives" - a SUBSTRING count. The exact field value occurs zero times, in every IDXD record and every non-IDXD payload. What the disc carries is rot_n001_break x14, LOD_Frame_rot_n001_break x7, GameModel_rot_n001_break x5. rot_n001 exact is not on this disc. A prefix match is not an exact match, and a byte sweep is the tool that breaks it. rou_e202 does exist, in the other vocabulary: a Model field of a Generic record, six times, one per GP_MAIN_GAME_<lang> pack, with no GameResourceID - which is why the GameResourceID census called it absent. Set arithmetic over every IDXD record: roster 40, Generic.Model 46, GameResourceID 480; roster n Model 34, roster n GID 38, roster n both 33. 38 + 34 - 33 = 39, so 39 of 40 are one or the other and rot_n001 is the sole exception. The six that are GID but not Model: mob_n040..043 are the four AsteroidGroup_00N.AsteroidModelName values (asteroid models, not units) and rou_e004 is a Level_0.Model. What selects the 40 is still open, but two answers are dead: not the Generic.Model set (34 of 46), not the GameResourceID set (38 of 480). The 12 Generic.Model values outside the roster include rou_f002 and rou_f004 - the player's own craft for S7-S16/S24-27,29 and for S28 - while rou_f001 is kept, so "the playable models" is dead too. All fifteen artefacts byte-identical.
547 lines
30 KiB
Markdown
547 lines
30 KiB
Markdown
# ✅ The player-tuning tables — analog response curves, booster/flight limits, special attacks
|
||
|
||
- **Regenerate:** `python3 tools/re-capture/main_game_unnamed.py` → [`../data/main-game-unnamed.txt`](../data/main-game-unnamed.txt)
|
||
- **Where:** `GP_MAIN_GAME_{D,E,F,I,J,S}.pak`, as **unnamed** IDXD objects — identical hash sets in all six language copies.
|
||
- **Related:** [[flight-speed-law]], [[flight-controls-runtime]], [[unit-datasheet-static]], [[archive-naming]].
|
||
|
||
Found while characterising the 337 unnamed IDXD objects in each main-game pak
|
||
(see `archive-naming.md`). **333 of the 337 belong to families the corpus already
|
||
documents.** These are the four that do not.
|
||
|
||
## ✅ The analog stick response curves — object `955ca077`
|
||
|
||
Eight records, one per input axis, each **11 positional values plus a named
|
||
`Count = 11`**. ⚠️ `Count` naming an indexed group is not safe in general — here
|
||
it was tested and **holds 8/8**.
|
||
|
||
| record | shape | curve |
|
||
|---|---|---|
|
||
| `AnalogRevice_yaw` | **identity** | `0.0 0.1 … 1.0` |
|
||
| `AnalogRevice_roll` | **identity** | `0.0 0.1 … 1.0` |
|
||
| `AnalogRevice_throttle` | **identity** | `0.0 0.1 … 1.0` |
|
||
| `AnalogRevice_pitch` | strongly eased | `0.000 0.003 0.010 0.020 0.040 0.065 0.100 0.155 0.250 0.500 1.000` |
|
||
| `AnalogRevice_eye_pitch` | eased | `0.00 0.01 0.03 0.06 0.10 0.18 0.28 0.42 0.60 0.80 1.00` |
|
||
| `AnalogRevice_eye_yaw` | eased, flat dead lead-in | `0.00 0.00 0.00 0.00 0.03 0.10 0.18 0.29 0.45 0.68 1.00` |
|
||
| `AnalogRevice_adv_roll` | S-curve | `0.000 0.005 0.032 0.080 0.138 0.231 0.387 0.584 0.792 0.941 1.000` |
|
||
| `AnalogRevice_adv_yaw` | **not monotone** | `0.000 0.016 0.052 0.143 0.482 0.940 1.000 0.916 0.611 0.382 0.208` |
|
||
|
||
✅ **Three of the eight are the identity ramp** — yaw, roll and throttle are
|
||
unshaped, and the shaping the game does apply is all on **pitch and the camera
|
||
("eye") axes**. 🟡 `adv_yaw` **rises to 1.0 at sample 6 then falls back to
|
||
0.208**, so whatever it indexes, it is *not* a stick-deflection curve like the
|
||
other seven; it has the shape of an envelope over something else. Untested.
|
||
|
||
The same object carries a `Tweak` record with what read as **deadzones and
|
||
input-timing constants** — `mov_stick_play = 5000`, `eye_stick_play = 6000`
|
||
(raw stick units), `order_cancel_time = 120`, `YawMagForNormal = 2.0`, and six
|
||
`receipt_*` / `minimum_side_s` values. 🟡 The units are inferred from magnitude,
|
||
not read from the consumer.
|
||
|
||
## 🔴 ~~The player craft's flight envelope~~ — object `890e1be4`, CORRECTED
|
||
|
||
> **My label from the first pass was wrong twice over.** Kept, with the
|
||
> measurements that refuted it, because both errors are instructive.
|
||
|
||
**Error 1 — `Booster` is not a new schema.** Its 50 field names are a **strict
|
||
subset of the unit `Maneuver` record: 50/50 shared, zero `Booster`-only**. (The
|
||
first check compared against `Generic` and scored 0/50, which looked like a
|
||
brand-new structure — *the wrong record of a multi-record object is not a
|
||
control*.) `CruisingVelocity` appears in exactly **115** records disc-wide: 114
|
||
`Maneuver` + this one `Booster`.
|
||
|
||
**Error 2 — it is not the profile the player actually flies.**
|
||
[[flight-speed-law]] measured the Delta Saber at **~125 / ~420 / ~1 530**
|
||
(min / cruise / max). Against the two candidate tables:
|
||
|
||
| | min | cruise | max | ratios measured ÷ table |
|
||
|---|---:|---:|---:|---|
|
||
| unit `Maneuver` (5 units carry it) | 100 | 350 | 1 200 | **1.25 / 1.20 / 1.28** — consistent |
|
||
| `Booster` | 100 | 612 | 2 100 | 1.25 / **0.69** / **0.73** — not |
|
||
|
||
✅ **The flight matches the unit `Maneuver` record**, at one consistent scale.
|
||
`Booster` is a **second, faster profile** — and the way it differs is the
|
||
finding: **39 of its 50 values are identical to the player unit's `Maneuver`**,
|
||
and of the 11 that move, the three velocities are scaled by **exactly ×1.75**
|
||
(350 → 612, i.e. 612.5 rounded; 1 200 → 2 100; `SideThrustVelocity_Max`
|
||
500 → 875) and two accelerations by **×1.5** (`Acceleration` 600 → 900,
|
||
`SideThrustAcceleration` 1 000 → 1 500). The rest: `Deceleration` 500 → 600,
|
||
`PowerCutDeceleration` 10 → 15, `PowerCutConsumeShield` 20 → 75,
|
||
`AB_ConsumeShield_Begin` 50 → **20**, `ArterBurner_Vc` 2.0 → **1.5**.
|
||
|
||
🟡 What that profile is *for* — an upgrade, a difficulty tier, a boost mode — is
|
||
not settled. Nothing selects it on the disc that I could find.
|
||
|
||
The object's other records stand as described: `TacticalManeuver`
|
||
(`Turn180RequiredTime 0.5`, `RollL/RRequiredTime 1.0`, `RollL/RSlideLength 350`,
|
||
`RollToHorizonTime 2.0`), `SpecialAttack` + `SpecialAttackGauge_1/2/3`
|
||
(`ChargeMaximum` 33 / 66 / 100), `SpecialWeapon`, `Misc`.
|
||
|
||
## ✅✅ The object is `PlayerParams`, and `sub_822F9498` is ITS loader — not the unit loader
|
||
|
||
Resolving **every** string `sub_822F9498` references, in code order, gives **90**
|
||
of them, and they are **exactly this object's schema, in this object's record
|
||
order**:
|
||
|
||
```
|
||
CollisionEffectName … EffectPlacementFrameName (Misc, 5)
|
||
SpecialAttack + 17 fields
|
||
TacticalManeuver + 7
|
||
SpecialWeapon + 8
|
||
Booster + 50
|
||
```
|
||
|
||
It never names `Generic`, `Maneuver`, `Explosion`, `Shield` or `StructureCount`;
|
||
the string `Maneuver` exists once in the image and has **0 xrefs**. The function
|
||
is called **exactly once**, from `sub_821A6CF0` — which is itself called once and
|
||
whose four strings include the literal **`PlayerParams`**.
|
||
|
||
⇒ 🔴 **Correction to [[unit-datasheet-static]], which calls `sub_822F9498` "the
|
||
unit-definition loader".** It reads one object, the player parameter table. Its
|
||
`AA_`/`AV_` interleave finding (`AV_` at `X`, `AA_` at `X+8`, five axes) still
|
||
stands as a **struct layout** — but the struct is **`PlayerParams`'s `Booster`
|
||
record**, not each unit's `Maneuver`. Whatever loads the 114 unit `Maneuver`
|
||
records does not use these name strings at all, and has not been found.
|
||
|
||
🟡 `name_hash`/`tag_hash` of `PlayerParams` under 7 prefixes × 6 spellings
|
||
(84 combinations) hits the object's key `0x890E1BE4` **0 times** — the string
|
||
names the table to the code, not the pak entry.
|
||
|
||
⚠️ The loader names **8** `SpecialWeapon` fields; the disc values **7**.
|
||
`HPRegenerator_Quantity` is named and never valued — the usual pattern.
|
||
|
||
## ✅ All five player craft fly identically; `Booster` differs on ten fields
|
||
|
||
Ranking all 114 units by how many of the 50 `Booster` values they reproduce:
|
||
the top five are **exactly the five `_Player` units** — `f001_T`, two
|
||
`f001_T_Tt`, `f002_W`, `f004_A` — all at **39/50**, and the sixth-best unit drops
|
||
to **13/50**. **No unit matches 50/50.**
|
||
|
||
✅ And the five players **agree with each other on all 50 fields**: the three
|
||
player ships share one flight model. `Booster` stands alone on **10** of them
|
||
(the eleventh, `AA_Yaw_Max`, is `65.0` vs `65` — a formatting difference, not a
|
||
value; my earlier "11 differ" over-counted).
|
||
|
||
## 🟡 So what selects `Booster`? Nothing does, and that is the problem
|
||
|
||
`PlayerParams` is loaded **once, unconditionally**, at startup: one call site,
|
||
one caller, no branch. So `Booster` is not a profile the game *chooses* — it is
|
||
the only flight table this path loads.
|
||
|
||
🟡 **That is in tension with the measurements and I have not resolved it.**
|
||
[[flight-speed-law]] clocked the player at ~125 / ~420 / ~1 530, which tracks the
|
||
unit `Maneuver` 100 / 350 / 1 200 at a flat ~1.25 and misses `Booster`'s
|
||
100 / 612 / 2 100 badly — and the `RT`-held run reached ~1 530, not anything near
|
||
2 100. Both tables cannot govern the same craft. **Cheapest next test is a
|
||
runtime one:** watch which of the two constant sets reaches the live flight
|
||
struct, and re-measure with the afterburner held.
|
||
|
||
## ✅✅ SOLVED — `sub_821A6CF0` reads the analog block, found by solving the base
|
||
|
||
Two iterations called this block unreachable. It is not: it is read through a
|
||
**base register**, and the base can be *solved from the displacements alone*.
|
||
|
||
🔑 **The method** (`tools/re-capture/name_block_bases.py` →
|
||
[`../data/name-block-bases.txt`](../data/name-block-bases.txt)): for each group
|
||
of `addi rX, rBASE, -N` sharing a source register, every (string address,
|
||
displacement) pair implies one candidate base; the true base collects a vote from
|
||
**every name it explains**, so it wins outright. ⚠️ Taking candidates from one
|
||
displacement instead misses it — that first cut scored the unit loader at 52/226
|
||
against the right answer's 217/226.
|
||
|
||
✅ **Control, with no prior knowledge: the tool recovers `r30 = 0x82088F94` for
|
||
the unit loader `sub_82341A20`, resolving 217 of 226 displacements.** It also
|
||
independently recovers `sub_8230D1F8` (129/132), `sub_822F9498` (90/91) and
|
||
`sub_822AE628` (81/108) — and finds **261** such functions image-wide
|
||
(**277** before the `r0` correction at the end of this file).
|
||
|
||
✅ **The answer: `sub_821A6CF0`, `r29 = 0x820A1630`, 22 of 24 displacements.** In
|
||
code order it names
|
||
|
||
```
|
||
ControlTweakName YawMagForNormal
|
||
mov_stick_play mov_trigger_play eye_stick_play receipt_after_b
|
||
receipt_reverse_s receipt_tgt_near receipt_tgt_atk receipt_side_s
|
||
minimum_side_s receipt_match_spd order_cancel_time
|
||
AnalogRevice_adv_roll … AnalogRevice_throttle (8 curve names)
|
||
GP_MAIN_GAME
|
||
```
|
||
|
||
— the whole `Tweak` + `AnalogRevice_*` schema, in the object's own order, plus
|
||
the **pak** it comes from. `r29` is built at `0x821A6D34` as `addi r29, r11, 5680`
|
||
= `0x820A0000 + 5680` = `0x820A1630`, confirming the solved base exactly.
|
||
|
||
🔑 **And it is the same function that reads `PlayerParams`** — `sub_821A6CF0`
|
||
references that literal and calls `sub_822F9498`. So one function loads the
|
||
player's parameter object *and* the control-tweak/analog table. 🟡 It reads the
|
||
curves as `AnalogRevice_*` record names; how the 11 samples are then *applied*
|
||
is still unread.
|
||
|
||
> Withdrawn: the earlier 🔴 "referenced by nothing" and the softened 🟡
|
||
> "not found by these routes". Both measurements were right — 0/28 xrefs, 0 hits
|
||
> from naive base tracking — and both conclusions were wrong. **The base was
|
||
> solvable from the data the whole time.**
|
||
|
||
⚠️ **The twin-string-block trap, in the flesh.** `mov_stick_play` and
|
||
`eye_stick_play` each exist **twice**; only the *second* copy (`0x820AA630`,
|
||
`0x820AA654`) is referenced, by **`sub_822AE628`** — which also references
|
||
`ControlTweak`, `Camera`, `Rendering`, `BackGround`, `WeaponChangeFrame` and the
|
||
DOF/bloom field names. And **`ControlTweak` records exist only in
|
||
`GP_HANGAR_ARSENAL.pak` (×6)**, while `Tweak` exists only in the six main-game
|
||
paks (×1 each). `sub_822AE628` reads the **hangar's** control table. Taking it
|
||
for ours would have been a clean, wrong answer.
|
||
|
||
## ✅ The Stage 16 boss — object `0202269d`
|
||
|
||
Records `Guardian` (19 fields) and `Core` (10). `Guardian` names its three
|
||
weapons by shell id — `Shell_S16Boss_AAGun`, `Shell_S16Boss_Laser`,
|
||
`Shell_S16Boss_HBeam` — which is what identifies the object: **Stage 16**, the
|
||
stage `stage-settings-table.md` singles out via its unique `IsBoss16Enable`
|
||
field. `Guardian` HP 65 000 with `DamageLevel1/2` at 35 000 / 20 000; `Core`
|
||
HP 42 000 with levels at 25 000 / 10 000 and a lock-on release cycle
|
||
(`LockOnReleaseInterval 15.0` ± `LockOnReleaseRandomInterval 5.0`).
|
||
|
||
## 🟡 The fourth: object `a6c6c79b`
|
||
|
||
One record, `Generic`, one field — the **name** `eff_n0071` with an empty value.
|
||
An effect id and nothing else. Not identified.
|
||
|
||
## ✅ Classifying all rows objectively — 50 are data-table schemas (2026-08-27)
|
||
|
||
The earlier pass classified by eye and by function. This one is **by row** and
|
||
uses an objective test: **are the row's names IDXD record/field names on the
|
||
disc?** (13 450 of those disc-wide.) `name_block_bases.py` now prints it.
|
||
|
||
| | rows |
|
||
|---|---:|
|
||
| **≥50 % disc names, ≥8 names — a data-table schema** | **50** |
|
||
| everything else (engine/XDK vocabulary, key lists, noise) | 211 |
|
||
|
||
⚠️ Written as 53 / 224 of 277; the `r0` correction below withdrew 16 rows, three
|
||
of them from the 53. The two axes below are quoted at their original values.
|
||
|
||
Split against the base-confidence axis: `solved` bases (non-zero low half) split
|
||
34 / 136, `round` bases 16 / 91 — so **a round base is not the same question as a
|
||
non-table row**, and both axes are needed.
|
||
|
||
✅ The 53 include every loader the corpus already knows, which is the control.
|
||
❔ **What they also include, still unopened** (each grepped: the noun appears in
|
||
no `docs/re/` file):
|
||
|
||
| row | names | what it looks like |
|
||
|---|---:|---|
|
||
| `sub_823BDAA8` r11 | 33 | `rou_e901` + `GN_MainGun_01_MuzC`, `GN_MainGun_02_Muz01…` — the **S16 boss's muzzle/attach frames**, next to the collision table above |
|
||
| `sub_823BDAA8` r10 | 25 | `Motion_stand`, `Motion_stand_b1`, `Motion_attackA_start`… — **motion names**, the `EnumMotions` family `DefTables` declares |
|
||
| `sub_82315AE8` r11 | 20 | `InitRotation`, `MaxRotationSpeed`, `RotationAccel`, `MaxVerticalSpeed` — the **`Guardian` record's own fields**, i.e. the S16 boss loader |
|
||
| `sub_8219E560` r11 | 18 | `Detail_Rank`, `MISSIONS`, `DETAIL_TITLE`, `Detail_Board_Permanent` — the **leaderboard screen** keys |
|
||
| ~~`sub_825F2CF0` / `sub_825F2F88` r0~~ | ~~30 each~~ | 🔴 **WITHDRAWN — not a name block at all.** See the `r0` correction below |
|
||
|
||
⚠️ Four rows that *look* new are not: `sub_82297550`, `sub_822A2F00`,
|
||
`sub_822A9C18`, `sub_82288028` mix arsenal fields the corpus owns
|
||
(`ConditionToDevelop`, `WeaponDesc` → [[arsenal-develop-economy]];
|
||
`SilhouetteModel` → [[arsenal-item-weapon-chain]]) with **literal screen
|
||
coordinates as strings** — `757,228`, `903,343`, `1092,457` — which is why their
|
||
disc-overlap sits at 53–70 % rather than ~100 %.
|
||
|
||
## ✅ Mining the index: what the base-solver's index actually contains (2026-08-27)
|
||
|
||
277 rows over **190 distinct functions** — a function can read more than one
|
||
block — or **261 over 176** after the `r0` correction below. Classified:
|
||
|
||
**🔴 The tool's false-positive mode, now named.** 107 rows solve to a base on a
|
||
**64K boundary** — a bare `addis rX, r0, 0xHHHH` with no `addi` of its own, so any
|
||
scatter of displacements votes for it. **82 of them are `0x820B0000`**, and they
|
||
are ~60 near-identical functions in `0x8281xxxx–0x8284xxxx` all "naming" the same
|
||
`rou_e007 rou_e010 rou_e015 …` list. Read a round base with its resolution ratio,
|
||
never on its own. The 170 rows with a non-zero low half are the trustworthy set.
|
||
|
||
**Already owned** (the index re-derives them, which is the point): the unit
|
||
loader `sub_82341A20` (217), stage settings `sub_8230D1F8` (129), `PlayerParams`
|
||
`sub_822F9498` (90), the hangar `sub_822AE628` (81), squadron orders
|
||
(`sub_82320B48` → `ORDER_WINGMAN_*`), missile guidance (`sub_8236B608`,
|
||
`sub_8237BB78` → `st1_up_aperture` etc., [[weapon-datasheet-static]]), shell
|
||
movement (`sub_82261F70` → `Spiral_BeginTime`, [[weapon-struct-runtime]]),
|
||
substructures (`sub_823479B8` → `ParentStructureID`), and ~~six~~ **three** camera/fog readers
|
||
(`sub_8247DFC0` r31, `sub_823B2620` r31, `sub_822C7480` r31 — all three bases in
|
||
the same `0x8209E6xx` region; the other three were `r0` artefacts, below).
|
||
|
||
**🔑 The find: `sub_8233C368` is the `AIParams` loader.** `r28`, base
|
||
`0x8208583C`, 20 names — `Enumerate_AIs`, `FiringLength`, `GuardLength`,
|
||
`AutoGuardLength`, `CounterLength`, `MusterLength` and the 14 manoeuvre weights.
|
||
⚠️ **Corrected:** I first wrote that this unblocked a NEEDS-HUMAN item.
|
||
[[stage-mission-tables]] already had `AIParams` as ✅ static and directly
|
||
portable, with every field name; **only the loader was unnamed.** The disc-wide
|
||
census that followed is in that document. The same base `0x8208583C` also serves
|
||
`sub_82338EE0` (97 names, `Weapon TargetType SpecialWeaponType ReticleType
|
||
IsCharging …`) — the **weapon** datasheet loader, likewise not previously named.
|
||
|
||
⚠️ **CORRECTION (2026-08-27) — a row is a (function, REGISTER) pair, not a
|
||
function.** My one-line labels for those blocks each quoted **one of two** rows,
|
||
and the other row of the same function is a different schema entirely:
|
||
|
||
| function | one row | the other row |
|
||
|---|---|---|
|
||
| `sub_822E3EC8` | `r11` 29 — shader constants + techniques | `r10` 15 — the material **map slots** |
|
||
| `sub_822AFA50` | `r11` 16 — XDK shader-compiler tokens (`__vspltw`, `__vsel`) | `r10` 13 — the **menu camera tags** |
|
||
| `sub_823AE908` | `r11` 43 — the **S16 boss collision** table | `r31` 33 — shader tokens (the dense-block false positive) |
|
||
|
||
**Read `docs/re/data/name-block-bases.txt` by row, and quote the register.**
|
||
The three findings themselves are written up where they belong:
|
||
[[xbg7-mesh]] (material slots + shader constants) and [[collisionset]] (the S16
|
||
boss's 23 collision parts over 20 meshes). `sub_822AFA50`'s camera-tag row is
|
||
13 `roh_n001_menu{1,2}_*_cam_{pos,tag}` names — the menu scene's camera
|
||
positions and look-at tags, with `menu2` having sub-shots `2_0`–`2_4b`.
|
||
|
||
🟡 None of those five were opened; the index says what each names, not what each
|
||
means.
|
||
|
||
## 🔴 CORRECTION (2026-08-28) — `r0` is not a base register; 16 rows withdrawn
|
||
|
||
The last shortlisted row was `sub_825F2CF0` + `sub_825F2F88`, **`r0`**, 30 names
|
||
each on the same base `0x8209EB30`, 97 % of them disc names. The question I set
|
||
was "a pair, or a clone?" **Both answers are wrong: neither function reads a name
|
||
block.**
|
||
|
||
Diffing the two, instruction for instruction, settles it in one look — 145
|
||
instructions each, **72 differ and every difference is the same substitution**:
|
||
|
||
```
|
||
825F2CF0 addi r11, r0, -288 825F2F88 addi r11, r0, -288
|
||
825F2CF4 stvx v14, r11, r12 825F2F8C lvx v14, r11, r12
|
||
825F2CF8 addi r11, r0, -272 825F2F90 addi r11, r0, -272
|
||
825F2CFC stvx v15, r11, r12 825F2F94 lvx v15, r11, r12
|
||
… stvx / stvx128, v14 … v127 … lvx / lvx128, v14 … v127
|
||
```
|
||
|
||
They are the compiler's **VMX register save / restore helper pair** — a spill of
|
||
v14–v127 and its matching reload. So they *are* a pair rather than a clone, but
|
||
the pairing is `stvx`/`lvx`, not two readers of one table.
|
||
|
||
🔑 **The root cause, and it is a class of false positive, not one row.** In
|
||
PowerPC `addi rD, rA, N` the `rA` slot **reads as literal zero when it names r0** —
|
||
the form is `li rD, N`. The 72 `addi r11, r0, -N` are the vector spill offsets,
|
||
plain immediates. The solver had excluded `r1` (the stack pointer) and nothing
|
||
else, so it took those offsets as displacements, voted a base, and landed
|
||
`0x8209EB30` — **inside the stage-settings name block**, where a scatter of
|
||
16-byte-spaced negative offsets is dense enough to "resolve" 30 real field names.
|
||
|
||
**A 97 %-disc-names row can still be an artefact.** The disc-name test measures
|
||
what the *base region* contains, not that the function reads it.
|
||
|
||
✅ **Nothing is lost.** All 30 of the withdrawn names are a **strict subset of the
|
||
real reader's block**: `sub_8230D1F8 r29`, base `0x8209FD28`, 129 names —
|
||
the stage-settings loader the corpus already owns ([[stage-settings-table]]).
|
||
The string-xref join confirms it independently: `FinalPassBG`, `FogMinDistance`,
|
||
`ScreenColorR`, `ExposureKey_BeginValue` each exist **once** in the image and
|
||
each has **exactly one** xref, from `sub_8230D1F8`. `NoseCameraFOV` and
|
||
`GlareType` exist **twice** — the second copy is read by `sub_822AE628`, the
|
||
hangar's game-wide defaults, which is the trap the corpus already records.
|
||
|
||
**Fix and its effect.** `name_block_bases.py` now skips `reg in ('r0', 'r1')`.
|
||
Every number moves by exactly the 16 `r0` rows and nothing else:
|
||
|
||
| | before | after |
|
||
|---|---:|---:|
|
||
| rows | 277 | **261** |
|
||
| distinct functions | 190 | **176** |
|
||
| bases with a non-zero low half | 170 | **154** |
|
||
| bases on a 64K boundary (low confidence) | 107 | 107 |
|
||
| data-table schemas (≥50 % disc, ≥8 names) | 53 | **50** |
|
||
|
||
Both ways: `154 + 107 = 261`, and `277 − 16 = 261`. The diff against the previous
|
||
artefact removes 100 lines and adds 3; **every removed row line carries `r0`**,
|
||
the three added lines are those counts, and the control is untouched
|
||
(`sub_82341A20 r30 = 0x82088F94`, 217/226). Two of the 16 functions keep a row
|
||
through another register, which is why 190 − 176 = 14, not 16.
|
||
|
||
The three withdrawn data-table rows are `sub_825F2CF0`, `sub_825F2F88` and
|
||
`sub_82222E70` (15 names, 100 % disc — the same `0x8209E6D8` stage-settings
|
||
region, and by the same mechanism).
|
||
|
||
🟡 What this does **not** settle: the 🟡 stalled `SpaceSize` / `GlareType` /
|
||
`Nebura_*` numerics are no closer — their reader was already known to be
|
||
`sub_8230D1F8`, and the r0 row never was a second one.
|
||
|
||
## 🔴 CORRECTION (2026-08-28) — the confidence axis was inverted
|
||
|
||
Having killed the `r0` rows, the other named false-positive mode was "**107 rows
|
||
solve to a base on a 64K boundary — a bare `addis rX, r0, 0xHHHH` with no `addi`
|
||
of its own, so any scatter of displacements votes for it**". That reasoning is
|
||
wrong, and the measurement says so.
|
||
|
||
**The test.** The solver now simulates `lis`/`addis rD, r0, HI`, `addi rD, rA, N`
|
||
and `or rD, rA, rA` forward through each row's own function and asks whether the
|
||
**solved base ever lands in the solved register**. That is positive confirmation
|
||
from the instruction stream, independent of the displacement vote.
|
||
|
||
| | confirmed |
|
||
|---|---|
|
||
| bases on a **64K boundary** ("low confidence") | **107 / 107** |
|
||
| bases with a non-zero low half ("trustworthy") | **8 / 154** |
|
||
|
||
A 64K-boundary base is not evidence of a weak vote — it is the case where the
|
||
compiler needed **no second instruction**, so `addis r11, r0, 0x820B` stands in
|
||
the code in full and the base can be read off directly. A non-zero low half
|
||
usually means the base was built in the **caller** or loaded from memory, which
|
||
the simulation cannot see; a miss there is silence, not a refutation. (The
|
||
control, `sub_82341A20 r30 = 0x82088F94`, is one of those silences — it is
|
||
validated by its 217/226 resolution and by the disc, not by this test.)
|
||
|
||
🔑 **The control that settles it: every row the corpus independently validated
|
||
against the disc has a 64K-boundary base.**
|
||
|
||
| row | base | what it is, verified elsewhere |
|
||
|---|---|---|
|
||
| `sub_822814D8` r11 | `0x820B0000` | debriefing keys, 21 `STAGE_RESULT` fields |
|
||
| `sub_8227A3A0` r11 | `0x820B0000` | career keys, `OVERVIEW` 9 / `EX_OVERVIEW` 7 |
|
||
| `sub_82286BC8` r11 | `0x820B0000` | save-screen keys ([[savegame-format]]) |
|
||
| `sub_8219E560` r11 | `0x820A0000` | leaderboard, 13 records + 5 fields |
|
||
| `sub_822215D0` r11 / r10 | `0x820A0000` | the 205-name `PG*` HUD roster ([[hud-config]]) |
|
||
| `sub_822E3EC8` r11 / r10 | `0x820B0000` | shader constants + material slots ([[xbg7-mesh]]) |
|
||
| `sub_823AE908` r11, `sub_823BDAA8` r11 / r10, `sub_823C0260` r11 | `0x820B0000` | the S16 boss collision, frames and motions ([[collisionset]], [[stage16-boss]]) |
|
||
| `sub_82315AE8` r11 | `0x820B0000` | the S16 boss loader, 19/19 field names |
|
||
|
||
Thirteen rows over ten functions. Meanwhile the dense-short-string false
|
||
positives the corpus *did* name — the `r31 = 0x8202xxxx` rows on
|
||
`sub_82341A20`, `sub_8230D1F8`, `sub_822F9498`, `sub_823AE908` — all sit in the
|
||
class that was called trustworthy. **The axis pointed the wrong way in both
|
||
directions.**
|
||
|
||
### ✅ What the 0x820B0000 cluster actually is: duplication, not error
|
||
|
||
82 rows carry base `0x820B0000`, and **60 of them are one function emitted 60
|
||
times**. Each is **exactly 491 instructions**; diffing any two, **two
|
||
instructions differ**, both of them a global data pointer:
|
||
|
||
```
|
||
9 828110FC addi r30, r11, 14272 | 8281250C addi r30, r11, 15552
|
||
487 82811874 addi r3, r11, -13024 | 82812C84 addi r3, r11, -12984
|
||
```
|
||
|
||
Their sequences of string addresses are **identical, all 60** — the same 40
|
||
resource names in the same order, written into a per-copy global through
|
||
`sub_8217FA08(dest, name, -1)` in 24-byte strides. That is a static array
|
||
duplicated once per translation unit, not sixty tables. The index is inflated by
|
||
**duplication**, which the resolution ratio cannot show and the confirmation test
|
||
does not care about.
|
||
|
||
The roster itself: 30 `rou_*`, 6 `mob_*`, 3 `rob_*`, 1 `rot_*`. **38 of the 40
|
||
are `GameResourceID` values on the disc** (480 distinct, each used once) —
|
||
[[collisionset]] owns that prefix vocabulary and the `GameResourceID` link; what
|
||
it does not say is that the image compiles a fixed 40-name subset of them.
|
||
🟡 The two that are not: **`rot_n001`** — and no disc `GameResourceID` uses the
|
||
`rot_` prefix at all (`eff` 197, `rou` 172, `mob` 71, `wep` 23, `rob` 4, `stg*` 9)
|
||
— and **`rou_e202`**, whose siblings `rou_e201` and `rou_e901` are both present.
|
||
Two names the image carries that this disc field never uses.
|
||
|
||
🟡 Not settled: what the 60 globals are read by, and what selects 40 of the 172
|
||
`rou_*` ids.
|
||
|
||
## ✅ What the 60 duplicated functions are (2026-08-28)
|
||
|
||
Following the duplication finding above: each of the 60 is a **static-object
|
||
constructor**, and the object it builds is a **40-element array of 24-byte string
|
||
objects**.
|
||
|
||
```
|
||
8280FE20 addis r11, r0, 0x820B ; the string literal
|
||
8280FE28 addi r4, r11, -9584
|
||
8280FE34 addi r30, r11, 12944 ; r30 = 0x828E3290, the array
|
||
8280FE3C bl 0x8217FA08 ; assign(dest, literal, -1)
|
||
8280FE40 li r11, 0 … stw r11,4..20(r30) ; zero the rest of the object
|
||
8280FE64 addi r3, r30, 24 ; next element, stride 24
|
||
```
|
||
|
||
**Why they have no callers.** Each clone appears exactly **twice** in the whole
|
||
image: once in the `.pdata` unwind table — where every function appears, as
|
||
`(address, prolog/length word)`, and reading that as a registry is a trap — and
|
||
once as a slot of a **null-terminated 182-entry function-pointer array at
|
||
`0x82870018`–`0x828702EC`**. **170 of those 182 targets have zero direct xrefs**,
|
||
so the array is dispatched indirectly: it is the static-initialiser list, run
|
||
before `main`. Zero callers here means "static ctor", not "dead code".
|
||
|
||
**The teardown side confirms the shape.** Between two consecutive clones sit
|
||
**39–43 other functions (median 40)**, and the dominant size in the whole
|
||
`0x8281xxxx`–`0x8284xxxx` region is **11 instructions, 2 280 of them** (60 × ~40).
|
||
Reading three consecutive ones:
|
||
|
||
```
|
||
sub_828105BC: r3 = 0x828E3290 -> bl sub_823F3D68
|
||
sub_828105E4: r3 = 0x828E3290 + 24 -> bl sub_823F3D68
|
||
sub_82810610: r3 = 0x828E3290 + 48 -> bl sub_823F3D68
|
||
```
|
||
|
||
One thunk per element, same 24-byte stride, same global, all with zero xrefs —
|
||
the per-object destructors registered at exit. So the pattern is **one ctor
|
||
writing 40 names + 40 one-line dtor thunks, emitted 60 times**: a template or
|
||
macro-generated static array, not sixty tables.
|
||
|
||
🟡 What this still does not say: **which code reads the arrays**. An exact-address
|
||
search finds 2 347 functions outside the 60 that materialise one of the 60 global
|
||
addresses — far too broad to be a consumer list, and the region is a dense global
|
||
data area, so that search has no power here. The consumer needs a different route.
|
||
|
||
### ✅ `rot_n001` and `rou_e202` are on the disc after all
|
||
|
||
Both are absent from the 480 `GameResourceID` values, but a raw byte search across
|
||
every decompressed pak entry finds **`rot_n001` 26 times in 7 archives**
|
||
(including `DefTables.pak`) and **`rou_e202` 6 times in 6** — and those six are
|
||
exactly one per `GP_MAIN_GAME_<lang>` pack, with none in `DefTables`. Controls on
|
||
the same sweep: `rou_e201` 151 / 8 archives, `rou_e901` 917 / 8, `mob_n500` 46 / 8.
|
||
So neither is an image-only name; **`rou_e202` is simply the rarest member of the
|
||
roster**, carried once per language pack. 🟡 Which field holds them is not settled.
|
||
|
||
## 🔴✅ The roster's two vocabularies — and a prefix match I mistook for a hit (2026-08-28)
|
||
|
||
🔴 **Correction first.** Last iteration's raw byte sweep reported `rot_n001` "26
|
||
times in 7 archives". That was a **substring** count. Matching the *exact* field
|
||
value gives **zero**, in every IDXD record and every non-IDXD payload. Expanding
|
||
the surrounding characters shows what the disc actually carries:
|
||
|
||
```
|
||
rot_n001_break x14
|
||
LOD_Frame_rot_n001_break x7
|
||
GameModel_rot_n001_break x5
|
||
```
|
||
|
||
**`rot_n001` exact does not exist on this disc; `rot_n001_break` does**, and it is
|
||
itself one of the `Generic.Model` values. The image names the stem, the disc names
|
||
the `_break` variant. **A prefix match is not an exact match** — the corpus's own
|
||
rule, and the byte sweep is exactly the tool that breaks it.
|
||
|
||
✅ **`rou_e202` does exist**, in the *other* vocabulary: it is a **`Model` field of
|
||
a `Generic` record**, six times — one per `GP_MAIN_GAME_<lang>` pack — and it has
|
||
no `GameResourceID`. That is why the `GameResourceID` census called it absent.
|
||
|
||
✅ **The roster is covered by two disjointly-populated fields.** Exact-value sets
|
||
across every IDXD record on the disc:
|
||
|
||
| set | size |
|
||
|---|---:|
|
||
| the compiled roster | 40 |
|
||
| `Generic.Model` values | **46** |
|
||
| `GameResourceID` values | 480 |
|
||
| roster ∩ `Generic.Model` | 34 |
|
||
| roster ∩ `GameResourceID` | 38 |
|
||
| roster ∩ **both** | 33 |
|
||
|
||
`38 + 34 − 33 = 39` — **39 of the 40 are one or the other**, and the single
|
||
uncovered name is `rot_n001`, for the reason above. The six roster names that are
|
||
`GameResourceID` but not `Generic.Model` explain themselves:
|
||
`mob_n040`…`mob_n043` are the four **`AsteroidGroup_00N.AsteroidModelName`**
|
||
values (18 each, one per pack — asteroid models, not units), and `rou_e004` is a
|
||
`Level_0.Model`, not a `Generic` one.
|
||
|
||
🟡 **What selects the 40 is still open, and two candidate answers are now dead.**
|
||
It is not the `Generic.Model` set (34 of 46) and not the `GameResourceID` set
|
||
(38 of 480). The 12 `Generic.Model` values *outside* the roster are `mob_n050`,
|
||
`rob_e005`, `rot_n001_break`, `rou_e001`, `rou_e002`, `rou_e016`, `rou_e017`,
|
||
`rou_e018`, **`rou_f002`, `rou_f004`**, an empty string, and a Shift-JIS literal
|
||
`文字列` ("character string" — a template placeholder, the `_Test` pattern again).
|
||
**`rou_f002` and `rou_f004` are the player's own craft** for S7–S16 + S24–27,29 and
|
||
for S28 ([[stage-numbering-and-player-craft]]), and the roster omits both while
|
||
keeping `rou_f001` — so "the playable or most-used models" is dead too.
|
||
|
||
[[unit-substructure-records]] mentions `Generic.Model` as "the mesh the corpus
|
||
already decodes"; what it does not say is that there are only **46 distinct**
|
||
values of it disc-wide. `AsteroidGroup_00N` / `AsteroidModelName` are new nouns.
|