re(route-b): the field map comes out of the code, and with it the Ratio family
The filler builds each key as addi r4, r30, -N with r30 = 0x82088f94, so every store's field NAME is a string in the image. Pairing keys with the following stfs gives the layout outright: Size_X/Y/Z at +48/52/56, HQRatio +88, ShieldRatio +92, ThrusterRatio +96, resistances +116..132, Radar/FCS/FiringRange +672/676/680, Attack/Defence points +692/696/700. verify_fieldmap.rs checks it against the live dump: 62 fields agree with the disc, 0 disagree. That yields the runtime value of each field for units whose record omits it -- HQRatio 0.2/1, ThrusterRatio 0.2/1, ShieldRatio 1, ResistanceToPlayer 1, ResistanceToShell 0.1, ResistanceToExplosion 0.5, AttackVesselPoint 0.1, AttackCraftPoint 0.1/0.5, DefencePoint 0.1/0.25, FiringRange 0, Size_Y = Size_X. Recorded with the caveat that several show two values across units, so these are per-unit runtime values rather than one global default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
crates/sylpheed-formats/examples/verify_fieldmap.rs
Normal file
70
crates/sylpheed-formats/examples/verify_fieldmap.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
//! Check the code-derived offset→field map against the live dump, then read out
|
||||||
|
//! the runtime value of every field the disc record leaves defaulted.
|
||||||
|
//! Run: verify_fieldmap <disc-root> <live-dump> <ID=0xVA>...
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use sylpheed_formats::idxd::IdxdObject;
|
||||||
|
use sylpheed_formats::pak::PakArchive;
|
||||||
|
|
||||||
|
// Offsets read out of sub_82341A20's own key strings (stfs stores only).
|
||||||
|
const MAP: &[(usize, &str)] = &[
|
||||||
|
(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (80, "Size_Radius"),
|
||||||
|
(64, "Color_R"), (68, "Color_G"), (72, "Color_B"),
|
||||||
|
(88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"),
|
||||||
|
(116, "ResistanceToOptics"), (120, "ResistanceToShell"),
|
||||||
|
(124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"),
|
||||||
|
(132, "ResistanceParalyze"),
|
||||||
|
(672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"),
|
||||||
|
(692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint"),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut a = std::env::args().skip(1);
|
||||||
|
let disc = a.next().unwrap();
|
||||||
|
let dump = a.next().unwrap();
|
||||||
|
let pairs: Vec<(String, String)> =
|
||||||
|
a.filter_map(|s| s.split_once('=').map(|(i, v)| (i.into(), v.into()))).collect();
|
||||||
|
|
||||||
|
let mut live: BTreeMap<String, BTreeMap<usize, f32>> = BTreeMap::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
for line in std::fs::read_to_string(&dump).unwrap().lines() {
|
||||||
|
if let Some(r) = line.strip_prefix("=== ") { cur = r.trim().into(); continue; }
|
||||||
|
let f: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
// dump columns: addr +off hex u32 f32 -- the float is f[4], not f[3]
|
||||||
|
if f.len() >= 5 && f[1].starts_with('+') {
|
||||||
|
if let (Ok(o), Ok(v)) = (usize::from_str_radix(&f[1][1..], 16), f[4].parse::<f32>()) {
|
||||||
|
live.entry(cur.clone()).or_default().insert(o, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||||
|
let (mut agree, mut disagree) = (0, 0);
|
||||||
|
let mut defaults: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
|
||||||
|
for e in pak.entries() {
|
||||||
|
let Ok(b) = pak.read(e) else { continue };
|
||||||
|
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||||
|
let Some(id) = o.get_raw("ID") else { continue };
|
||||||
|
let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue };
|
||||||
|
let Some(w) = live.get(va) else { continue };
|
||||||
|
for (off, key) in MAP {
|
||||||
|
let Some(got) = w.get(off) else { continue };
|
||||||
|
match o.get_f32(key) {
|
||||||
|
Some(want) => {
|
||||||
|
if (want - got).abs() <= want.abs() * 1e-4 { agree += 1 } else {
|
||||||
|
disagree += 1;
|
||||||
|
println!(" MISMATCH {id} {key}: disc {want}, live +{off} = {got}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => defaults.entry((*key).into()).or_default().push((id.into(), *got)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\nmap check: {agree} fields agree with the disc, {disagree} disagree\n");
|
||||||
|
println!("runtime value where the disc record DEFAULTS the field:");
|
||||||
|
for (key, hits) in &defaults {
|
||||||
|
let vals: Vec<String> = hits.iter().map(|(_, v)| format!("{v}")).collect();
|
||||||
|
let uniq: std::collections::BTreeSet<&String> = vals.iter().collect();
|
||||||
|
println!(" {:<22} {:<28} ({} units)", key,
|
||||||
|
uniq.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "), hits.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -336,3 +336,54 @@ Two readings corrected on the way:
|
|||||||
So the runtime `Size_Y = Size_X` must be written **after** the definition is
|
So the runtime `Size_Y = Size_X` must be written **after** the definition is
|
||||||
loaded — by whatever instantiates a unit from it, or a post-load pass over the
|
loaded — by whatever instantiates a unit from it, or a post-load pass over the
|
||||||
table. `+108` itself is too generic to chase (1 129 loads image-wide).
|
table. `+108` itself is too generic to chase (1 129 loads image-wide).
|
||||||
|
|
||||||
|
## ✅ The field map, read out of the code — and the Ratio family with it
|
||||||
|
|
||||||
|
The filler builds each key as `addi r4, r30, -N` with `r30 = 0x82088f94`, so the
|
||||||
|
**field name for every store is a string in the image**. Pairing each key with the
|
||||||
|
`stfs` that follows gives the object layout directly, with no guessing:
|
||||||
|
|
||||||
|
| offset | field | offset | field |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `+48` | `Size_X` | `+116` | `ResistanceToOptics` |
|
||||||
|
| `+52` | `Size_Y` | `+120` | `ResistanceToShell` |
|
||||||
|
| `+56` | `Size_Z` | `+124` | `ResistanceToExplosion` |
|
||||||
|
| `+64/68/72` | `Color_R/G/B` | `+128` | `ResistanceToPlayer` |
|
||||||
|
| `+80` | `Size_Radius` | `+132` | `ResistanceParalyze` |
|
||||||
|
| **`+88`** | **`HQRatio`** | `+672` | `RadarRange` |
|
||||||
|
| **`+92`** | **`ShieldRatio`** | `+676` | `FCSRange` |
|
||||||
|
| **`+96`** | **`ThrusterRatio`** | `+680` | `FiringRange` |
|
||||||
|
| `+692` | `AttackVesselPoint` | `+696` | `AttackCraftPoint` |
|
||||||
|
| `+700` | `DefencePoint` | | |
|
||||||
|
|
||||||
|
`examples/verify_fieldmap.rs` checks it against the live dump: **62 fields agree
|
||||||
|
with the disc record, 0 disagree** across five capital ships.
|
||||||
|
|
||||||
|
That turns the same dump into the answer for the family the brief asked about —
|
||||||
|
the runtime value of each field **for the units whose record omits it**:
|
||||||
|
|
||||||
|
| field | runtime value(s) seen | units |
|
||||||
|
|---|---|---|
|
||||||
|
| `Size_Y` | 200, 300, 400, 600, 700 (each `= Size_X`) | 5 |
|
||||||
|
| `HQRatio` | **0.2**, **1** | 4 |
|
||||||
|
| `ThrusterRatio` | **0.2**, **1** | 4 |
|
||||||
|
| `ShieldRatio` | **1** | 1 |
|
||||||
|
| `ResistanceToPlayer` | **1** | 5 |
|
||||||
|
| `ResistanceToShell` | **0.1** | 3 |
|
||||||
|
| `ResistanceToExplosion` | **0.5** | 3 |
|
||||||
|
| `AttackVesselPoint` | **0.1** | 2 |
|
||||||
|
| `AttackCraftPoint` | **0.1**, **0.5** | 5 |
|
||||||
|
| `DefencePoint` | **0.1**, **0.25** | 4 |
|
||||||
|
| `FiringRange` | **0** | 5 |
|
||||||
|
|
||||||
|
⚠️ Read that table as *"what the field holds at runtime for a unit that does not
|
||||||
|
set it"*, **not** as "the default constant". Several fields show **two distinct
|
||||||
|
values** across units (`HQRatio` 0.2 or 1, `AttackCraftPoint` 0.1 or 0.5), so the
|
||||||
|
value is derived per unit rather than being one global default — consistent with
|
||||||
|
`Size_Y`, which takes each unit's own `Size_X`. Only `FiringRange` (0 everywhere)
|
||||||
|
matches the loader's own miss value.
|
||||||
|
|
||||||
|
*Method note:* the first run of this check reported 32 "mismatches". They were an
|
||||||
|
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
|
||||||
|
right all along.
|
||||||
|
|||||||
Reference in New Issue
Block a user