diff --git a/crates/sylpheed-formats/examples/verify_fieldmap.rs b/crates/sylpheed-formats/examples/verify_fieldmap.rs new file mode 100644 index 0000000..fc48421 --- /dev/null +++ b/crates/sylpheed-formats/examples/verify_fieldmap.rs @@ -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 ... +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> = 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::()) { + 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> = 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 = 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::>().join(", "), hits.len()); + } +} diff --git a/docs/re/live-unit-definitions.md b/docs/re/live-unit-definitions.md index b2fc865..9cc5006 100644 --- a/docs/re/live-unit-definitions.md +++ b/docs/re/live-unit-definitions.md @@ -336,3 +336,54 @@ Two readings corrected on the way: 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 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.