This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/player-tuning-tables.md
Claude (auto) ced73c5488 re: all 277 base-solver rows classified objectively; 53 are data-table schemas
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
2026-08-27 23:09:57 +00:00

292 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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.