re(route-b): full field map verified 154/154, and 101 defaulted fields read out

Anchoring each key string to the first store AFTER the accessor call makes the
extraction exact: 159 fields, 125 of them floats, and verify_fieldmap now checks
every one against the live objects -- 154 agree with the disc, 0 disagree.

The last ten mismatches taught the rule: angle fields are degrees on disc and
RADIANS at runtime (AV_PitchPlus_Max 3.6 -> 0.0628319; MaximumBank_Normal 60 ->
1.0472), an exact conversion across every angle field and unit.

101 defaulted fields now have runtime values: angle limits default to 3.6 degrees,
afterburner/turn-attack fields to 0, FiringRange to 0 (matching the loader's miss
value), Size_Y to that unit's Size_X, and the ratio family is per-unit rather than
constant (HQRatio 0.2 or 1, DefencePoint 0.1 or 0.25, ...).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:44:57 +00:00
parent 0434afa6a6
commit d674cddad4
3 changed files with 155 additions and 11 deletions

View File

@@ -6,16 +6,11 @@ use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive; use sylpheed_formats::pak::PakArchive;
// Offsets read out of sub_82341A20's own key strings (stfs stores only). // Offsets read out of sub_82341A20's own key strings (stfs stores only).
const MAP: &[(usize, &str)] = &[ // Offsets and names recovered from `sub_82341A20` itself: each key is built as
(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (80, "Size_Radius"), // `addi r4, r30, -N` (r30 = 0x82088f94, so the name is a string in the image),
(64, "Color_R"), (68, "Color_G"), (72, "Color_B"), // and the value lands in the first store into the object after the accessor
(88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"), // call. 125 float-typed fields.
(116, "ResistanceToOptics"), (120, "ResistanceToShell"), const MAP: &[(usize, &str)] = &[(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (64, "Color_R"), (68, "Color_G"), (72, "Color_B"), (80, "Size_Radius"), (84, "HP"), (88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"), (116, "ResistanceToOptics"), (120, "ResistanceToShell"), (124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"), (132, "ResistanceParalyze"), (156, "MinimumVelocity"), (160, "MaximumVelocity"), (164, "CruisingVelocity"), (168, "Acceleration"), (172, "Deceleration"), (176, "AV_PitchPlus_Max"), (180, "AV_PitchPlus_Min"), (184, "AA_PitchPlus_Max"), (188, "AA_PitchPlus_Min"), (192, "AV_PitchMinus_Max"), (196, "AV_PitchMinus_Min"), (200, "AA_PitchMinus_Max"), (204, "AA_PitchMinus_Min"), (208, "AV_Yaw_Max"), (212, "AV_Yaw_Min"), (216, "AA_Yaw_Max"), (220, "AA_Yaw_Min"), (224, "AV_Roll_Max"), (228, "AV_Roll_Min"), (232, "AA_Roll_Max"), (236, "AA_Roll_Min"), (248, "SideThrustVelocity_Max"), (252, "SideThrustAcceleration"), (256, "MaximumBank_Normal"), (260, "YawDragFactor"), (264, "PitchDragFactor"), (268, "RollDragFactor"), (272, "DragFactorThreshold"), (276, "ArterBurner_Vc"), (280, "ReverseThrust_Vc"), (284, "ArterBurner_Acc"), (288, "ReverseThrust_Acc"), (292, "AccPitchFactor"), (296, "DecPitchFactor"), (300, "AV_AxisMode_Max"), (304, "AV_AxisMode_Min"), (308, "AA_AxisMode_Max"), (312, "AA_AxisMode_Min"), (316, "PowerCutConsumeShield"), (320, "PowerCutDeceleration"), (324, "AB_ConsumeShield_Begin"), (328, "AB_ConsumeShield"), (332, "AB_AV_PitchPlus"), (336, "AB_AA_PitchPlus"), (340, "AB_AV_PitchMinus"), (344, "AB_AA_PitchMinus"), (348, "AB_AV_Yaw"), (352, "AB_AA_Yaw"), (356, "AB_AV_Roll"), (360, "AB_AA_Roll"), (368, "SideRoll_Time"), (372, "SideRoll_Length"), (380, "BarrelRoll_CountMinimum"), (384, "BarrelRoll_CountMaximum"), (388, "BarrelRoll_Time"), (392, "BarrelRoll_Radius"), (400, "TurnAttack_CutoffRatio"), (404, "TurnAttack_DoubleRatio"), (408, "CutoffTimeMin"), (412, "CutoffTimeMax"), (416, "TurnAttack_DoubleTimeMin"), (420, "TurnAttack_DoubleTimeMax"), (428, "Turn_AngularVelocity"), (432, "TurnAway_Time_Minimum"), (436, "TurnAway_Time_Maximum"), (444, "BoostAway_Time_Minimum"), (448, "BoostAway_Time_Maximum"), (456, "HoldPosition_LengthMin"), (460, "HoldPosition_LengthMax"), (464, "HoldPosition_MinimumTime"), (468, "HoldPosition_MaximumTime"), (472, "HoldPosition_SideRatio"), (476, "HoldPosition_BackRatio"), (480, "HoldPosition_CutoffRatio"), (484, "HoldPosition_CancelTime"), (492, "Slalom_CutoffRatio"), (496, "Slalom_TurnCount_Min"), (500, "Slalom_TurnCount_Max"), (508, "Through_CutoffRatio"), (512, "Through_AngleMinimum"), (516, "Through_AngleMaximum"), (520, "Through_Time1Max"), (524, "Through_Time1Min"), (528, "Through_Time2Max"), (532, "Through_Time2Min"), (536, "Through_LengthMinimum"), (540, "Through_LengthMaximum"), (548, "SolidCutoff_Ratio"), (552, "SolidCutoff_LengthMin"), (556, "SolidCutoff_LengthMax"), (560, "HomingResistAdjustment"), (564, "UsingChaffRatio"), (568, "MaxValue"), (572, "ChargeDelay"), (576, "ChargeDelay_Break"), (580, "ChargeSpeed"), (584, "Delay"), (588, "DelayAdjustment"), (624, "DestroyMotionTime"), (628, "DryMass"), (632, "GrossMass"), (664, "LowerHPThresholdRatio"), (668, "SELength"), (672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"), (692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint")];
(124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"),
(132, "ResistanceParalyze"),
(672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"),
(692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint"),
];
fn main() { fn main() {
let mut a = std::env::args().skip(1); let mut a = std::env::args().skip(1);
@@ -50,7 +45,12 @@ fn main() {
let Some(got) = w.get(off) else { continue }; let Some(got) = w.get(off) else { continue };
match o.get_f32(key) { match o.get_f32(key) {
Some(want) => { Some(want) => {
if (want - got).abs() <= want.abs() * 1e-4 { agree += 1 } else { // Angle fields are stored in RADIANS at runtime and degrees on
// disc, so a match is either the raw value or its conversion.
let rad = want * std::f32::consts::PI / 180.0;
let ok = (want - got).abs() <= want.abs() * 1e-4
|| (rad - got).abs() <= rad.abs() * 1e-4;
if ok { agree += 1 } else {
disagree += 1; disagree += 1;
println!(" MISMATCH {id} {key}: disc {want}, live +{off} = {got}"); println!(" MISMATCH {id} {key}: disc {want}, live +{off} = {got}");
} }

View File

@@ -0,0 +1,105 @@
map check: 154 fields agree with the disc, 0 disagree
runtime value where the disc record DEFAULTS the field:
AA_AxisMode_Max 0 (5 units)
AA_AxisMode_Min 0 (5 units)
AA_PitchMinus_Max 0.0628319 (5 units)
AA_PitchMinus_Min 0.0628319 (5 units)
AA_PitchPlus_Max 0.0628319 (5 units)
AA_PitchPlus_Min 0.0628319 (5 units)
AA_Roll_Max 0.0628319 (5 units)
AA_Roll_Min 0.0628319 (5 units)
AA_Yaw_Max 0.0628319 (5 units)
AA_Yaw_Min 0.0628319 (5 units)
AB_AA_PitchMinus 0 (5 units)
AB_AA_PitchPlus 0 (5 units)
AB_AA_Roll 0 (5 units)
AB_AA_Yaw 0 (5 units)
AB_AV_PitchMinus 0 (5 units)
AB_AV_PitchPlus 0 (5 units)
AB_AV_Roll 0 (5 units)
AB_AV_Yaw 0 (5 units)
AB_ConsumeShield 0 (5 units)
AB_ConsumeShield_Begin 0 (5 units)
AV_AxisMode_Max 0 (5 units)
AV_AxisMode_Min 0 (5 units)
AV_PitchMinus_Max 0.0628319 (5 units)
AV_PitchMinus_Min 0.0628319 (5 units)
AV_PitchPlus_Min 0.0628319 (5 units)
AV_Roll_Max 0.0628319 (5 units)
AV_Roll_Min 0.0628319 (5 units)
AV_Yaw_Max 0.0628319 (5 units)
AV_Yaw_Min 0.0628319 (5 units)
ArterBurner_Acc 2 (5 units)
ArterBurner_Vc 2 (5 units)
AttackCraftPoint 0.1, 0.5 (5 units)
AttackVesselPoint 0.1 (2 units)
BarrelRoll_CountMaximum 0 (5 units)
BarrelRoll_CountMinimum 0 (5 units)
BarrelRoll_Radius 0 (5 units)
BarrelRoll_Time 0 (5 units)
BoostAway_Time_Maximum 0 (5 units)
BoostAway_Time_Minimum 0 (5 units)
ChargeDelay 1 (5 units)
Color_B 1 (1 units)
Color_G 1 (1 units)
CutoffTimeMax 0 (5 units)
CutoffTimeMin 0 (5 units)
DecPitchFactor 30 (5 units)
DefencePoint 0.1, 0.25 (4 units)
Delay 0 (3 units)
DelayAdjustment 0, 2 (5 units)
DragFactorThreshold 0.5 (5 units)
DryMass 10000 (2 units)
FiringRange 0 (5 units)
GrossMass 10000 (5 units)
HQRatio 0.2, 1 (4 units)
HoldPosition_BackRatio 0 (5 units)
HoldPosition_CancelTime 0 (5 units)
HoldPosition_CutoffRatio 0 (5 units)
HoldPosition_LengthMax 0 (5 units)
HoldPosition_LengthMin 0 (5 units)
HoldPosition_MaximumTime 0 (5 units)
HoldPosition_MinimumTime 0 (5 units)
HoldPosition_SideRatio 0 (5 units)
HomingResistAdjustment 0 (5 units)
LowerHPThresholdRatio 0.3 (1 units)
MinimumVelocity 0 (4 units)
PitchDragFactor 2 (5 units)
PowerCutConsumeShield 0 (5 units)
PowerCutDeceleration 0 (5 units)
ResistanceToExplosion 0.5 (3 units)
ResistanceToPlayer 1 (5 units)
ResistanceToShell 0.1 (3 units)
ReverseThrust_Acc 0.25 (3 units)
RollDragFactor 2 (5 units)
SELength 2000 (5 units)
ShieldRatio 1 (1 units)
SideRoll_Length 0 (5 units)
SideRoll_Time 0 (5 units)
Size_Y 200, 300, 400, 600, 700 (5 units)
Slalom_CutoffRatio 0 (5 units)
Slalom_TurnCount_Max 0 (5 units)
Slalom_TurnCount_Min 0 (5 units)
SolidCutoff_LengthMax 0 (5 units)
SolidCutoff_LengthMin 0 (5 units)
SolidCutoff_Ratio 0 (5 units)
Through_AngleMaximum 0 (5 units)
Through_AngleMinimum 0 (5 units)
Through_CutoffRatio 0 (5 units)
Through_LengthMaximum 0 (5 units)
Through_LengthMinimum 0 (5 units)
Through_Time1Max 0 (5 units)
Through_Time1Min 0 (5 units)
Through_Time2Max 0 (5 units)
Through_Time2Min 0 (5 units)
ThrusterRatio 0.2, 1 (4 units)
TurnAttack_CutoffRatio 0 (5 units)
TurnAttack_DoubleRatio 0 (5 units)
TurnAttack_DoubleTimeMax 0 (5 units)
TurnAttack_DoubleTimeMin 0 (5 units)
TurnAway_Time_Maximum 0 (5 units)
TurnAway_Time_Minimum 0 (5 units)
Turn_AngularVelocity 0 (5 units)
UsingChaffRatio 0 (5 units)

View File

@@ -387,3 +387,42 @@ matches the loader's own miss value.
off-by-one column in the dump reader — `f[3]` is the u32, `f[4]` the float — and off-by-one column in the dump reader — `f[3]` is the u32, `f[4]` the float — and
decoding one by hand (`1128792000` = `0x43480000` = 200.0) showed the map had been decoding one by hand (`1128792000` = `0x43480000` = 200.0) showed the map had been
right all along. right all along.
## ✅ The whole map verified — 154/154 — and 101 defaulted fields read out
Anchoring the key string to the **first store after the accessor call** (rather
than to the nearest store) makes the extraction exact: **159 fields**, of which
**125 are floats**. `verify_fieldmap.rs` now checks every one of them against the
live objects: **154 field values agree with the disc record, 0 disagree** across
five capital ships.
Getting there needed one rule, and the ten last "mismatches" are what taught it:
```
AV_PitchPlus_Max disc 3.6 live +176 = 0.0628319 ( = 3.6° in radians)
MaximumBank_Normal disc 60 live +256 = 1.0472 ( = 60° in radians)
```
**Angle fields are degrees on disc and radians at runtime** — the incidental
observation from the previous pass, now confirmed as an exact conversion across
every angle field and every unit.
With the map verified, the dump answers the brief's question for **101 fields**
that these records leave defaulted
([`captures/stage02-defaulted-field-runtime-values.txt`](captures/stage02-defaulted-field-runtime-values.txt)).
The shape of the answer:
- **Angle limits default to 3.6°** (`0.0628319` rad) — every `AA_*`/`AV_*`
`Pitch/Roll/Yaw` `Min`/`Max`, identical across all five units.
- **Afterburner and turn-attack fields default to 0** (`AB_*`, `TurnAttack_*`,
`AA_AxisMode_*`, `AV_AxisMode_*`).
- **`FiringRange` defaults to 0**, matching the loader's own miss value exactly.
- **The ratio family is per-unit, not constant**: `HQRatio` 0.2 or 1,
`ThrusterRatio` 0.2 or 1, `AttackCraftPoint` 0.1 or 0.5, `DefencePoint` 0.1 or
0.25, `DelayAdjustment` 0 or 2 — so these are *runtime values for units that do
not set the field*, not a single global default.
- **`Size_Y` takes that unit's own `Size_X`** (200/300/400/600/700).
That is the `…Ratio` / `…Count` family the mission brief named, answered from the
running game, with the field identities read out of the title's own code rather
than guessed.