re: audit the legacy IDXD reader against the real field table — and fix a test that encoded its error

With the record table decoded there is finally a ground truth to check the
old string-pool reader against. It infers `key -> value` from pool adjacency,
which is a consequence of how records are written, not a rule of the format.

Verified by hand against the disc, with an independent parser:

* `FCSRange` = 500000.0 — the module docs' own canonical example of a field
  "left at its default" that "omits the value string".
* `ShieldRatio` = 1.0, where `tests/pak_idxd_disc.rs` asserted None with the
  comment "a defaulted/omitted field must be None". That test encoded the
  false belief; it now keeps the None as a deliberate characterisation of the
  legacy reader, with the true value asserted beside it.
* `get_raw("Model")` on GP_HANGAR_ARSENAL returns the first record's model for
  every record — silent corruption, not an absent value. New test pins four
  records that disagree with it.

The cause is the flat API having no way to name a record: only 548 of 6325
objects have one. `HP` on the DeltaSaber answers 1000.0, the hull, while 63
Turret_* records each carry their own 100.0 (measured — a first draft said 34,
taken from a report rather than from the disc).

Disc-wide rates are recorded as single-source and labelled as such: get_raw
52% wrong, typed getters 38% miss, but 100% correct on single-record objects.

Also records a negative result: the 504 unnamed field keys were NOT recovered.
A 572464-string dictionary and 73191 variants gave 0/42. The key deltas do
prove the preimage ends with the two decimal digits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-25 21:42:51 +00:00
parent af32540190
commit 9141ea2b24
4 changed files with 206 additions and 1 deletions

View File

@@ -80,8 +80,54 @@ fn deltasaber_craft_stats() {
assert_eq!(obj.get_i64("Turn_AngularVelocity"), Some(100));
assert_eq!(obj.get_f32("ChargeSpeed"), Some(800.0));
assert_eq!(obj.get_f32("RadarRange"), Some(500000.0));
// a defaulted/omitted field must be None, not a neighbouring key
// ❌ NOT a defaulted field — this assertion used to claim it was. The record
// table says ShieldRatio = 1.0; the legacy string-pool reader simply cannot
// see it. Kept as a *characterisation* of that reader, with the truth beside
// it, so the blind spot stays visible instead of being read as "no value".
assert_eq!(obj.get_f32("ShieldRatio"), None);
let generic = obj.record("Generic").expect("Generic record");
assert_eq!(generic.get("ShieldRatio"), Some("1.0"));
// Same for the module docs' canonical "field with no value".
assert_eq!(generic.get("FCSRange"), Some("500000.0"));
// The values above are real, but each lives in a *different* record, which
// the flat API cannot express: these three are Generic / Maneuver / Shield.
assert_eq!(generic.get("HP"), Some("1000.0"));
assert_eq!(obj.record("Maneuver").unwrap().get("Acceleration"), Some("600.0"));
assert_eq!(obj.record("Shield").unwrap().get("ChargeSpeed"), Some("800.0"));
// And `HP` is not one number: 63 turret records carry their own (all 100.0).
let turrets = obj
.records()
.unwrap()
.iter()
.filter(|r| r.name.starts_with("Turret_"))
.count();
assert_eq!(turrets, 63);
assert_eq!(obj.record("Turret_000").unwrap().get("HP"), Some("100.0"));
}
/// The legacy reader returns one object-wide answer for a per-record field, and
/// it is silently *wrong* rather than absent. The hangar's weapon → model map is
/// the clearest case: every record reads back as the first one.
#[test]
fn legacy_reader_flattens_per_record_fields() {
skip_without_disc!(root);
let arc = PakArchive::open(root.join("dat/GP_HANGAR_ARSENAL.pak")).unwrap();
let bytes = arc.read_by_hash(0x8f72_ddde).expect("entry present").unwrap();
let obj = IdxdObject::parse(&bytes).unwrap();
// One answer for the whole object …
assert_eq!(obj.get_raw("Model"), Some("rou_f001_wep_33_hangar"));
// … while the records disagree with it and with each other.
for (record, model) in [
("Designator_LH", "rou_f001_wep_59_hangar"),
("Laser_Mine_B9L", "rou_f001_wep_29_hangar"),
("Saber_LG1", "rou_f001_wep_14_hangar"),
("Chaff_Flare_Dispencer", "rou_f001_wep_71_hangar"),
] {
assert_eq!(obj.record(record).unwrap().get("Model"), Some(model));
}
}
#[test]