name_block_bases.py extended with a per-row data-table test; artefact +57/-0, byte-identical across two runs (now ~2 min 12 s -- it adds a disc-wide pak scan). The test is objective, not by eye: a row is a data-table schema if its names are IDXD record/field names on the disc (13450 such names disc-wide). 53 of 277 rows are >=50 % disc names with >=8 names; the other 224 are engine/XDK vocabulary, compiled key lists, or noise. The two axes are independent: against base confidence, solved bases split 34 table / 136 not, round bases 16 / 91. "Round base" and "not a table" are different questions. The 53 contain every loader already known -- that is the control. Five rows in the 53 are unowned, each noun grepped and appearing in no docs/re/ file: sub_823BDAA8 r11 (33) = the S16 boss's muzzle/attach frames (GN_MainGun_*_Muz*); sub_823BDAA8 r10 (25) = motion names (Motion_stand, Motion_attackA_start), the EnumMotions family DefTables declares; sub_82315AE8 r11 (20) = the Guardian record's own fields, i.e. the S16 boss loader; sub_8219E560 r11 (18) = the leaderboard screen keys; sub_825F2CF0 + sub_825F2F88 r0 (30 each, same base) = post-processing (FinalPassBG, FogMin/MaxDistance). Four rows that look new are not, and their disc-overlap says so -- 53-70 % rather than ~100 %, because they mix arsenal fields the corpus owns (ConditionToDevelop, WeaponDesc, SilhouetteModel) with literal screen coordinates as strings. Not settled: none of the five was opened -- this iteration produced the shortlist, not the findings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
292 lines
16 KiB
Markdown
292 lines
16 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 **277** such functions image-wide.
|
||
|
||
✅ **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 277 rows objectively — 53 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** | **53** |
|
||
| everything else (engine/XDK vocabulary, key lists, noise) | 224 |
|
||
|
||
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 | `FinalPassBG`, `FinalPassToneRatio`, `FogMin/MaxDistance` — **post-processing**, two functions with the same base |
|
||
|
||
⚠️ 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 277: 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). 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 camera/fog readers
|
||
(`sub_825F2CF0`, `sub_825F2F88`, `sub_8247DFC0`, `sub_823B2620`, `sub_82222E70`,
|
||
`sub_822C7480`).
|
||
|
||
**🔑 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.
|