re: the "sibling default" rules are a dedup artefact — WITHDRAWN
The corpus recorded that some unit fields the disc leaves defaulted inherit from a sibling: Size_Y from Size_X, FCSRange from RadarRange, DefencePoint from AttackVesselPoint. Size_Y was marked the one to trust, on 9/9 support across 7 independent ships, and it is restated in INDEX.md. The premise is false. These fields are not defaulted -- they are on disc for 113-114 of 114 unit tables -- and Size_Y DIFFERS from Size_X in 90 of them. The mechanism, cross-tabulating "legacy reader missed it" against "equal on disc": pair seen+differ seen+equal miss+differ miss+equal Size_Y / Size_X 90 0 0 24 FCSRange / RadarRange 54 0 1 58 DefencePoint / AttackVesselPoint 51 0 1 61 seen+equal is 0 for all three: a value shared with a sibling is ALWAYS invisible to the string-pool reader, because the pool stores each distinct string once. And the reader almost never misses a value that differs. So "the missing value equals the sibling's" was true BY CONSTRUCTION -- the rule re-derived the very condition that made the field go missing. That is why the support looked perfect: it could not fail on the cases it was fitted to. The two miss+differ cells are its real wrong predictions, both named: UN_e104_ADAN_Carrier DefencePoint is 0.2 (rule says 0.003), and UN_e011_ADAN_Attacker_B_HF_Wayne FCSRange is 3000.0 (rule says 6000.0). Retracted in unit-struct-runtime.md (original reasoning kept below the correction), live-unit-definitions.md and INDEX.md. Pinned by a disc test that asserts the seen+equal cells stay zero, so the mechanism itself is guarded, not just the counts. Artifact: examples/sibling_rule_check.rs. This one was found by my own check after the subagent assigned to it stalled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
61
crates/sylpheed-formats/examples/sibling_rule_check.rs
Normal file
61
crates/sylpheed-formats/examples/sibling_rule_check.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Does the legacy reader miss a field precisely when its value is shared?
|
||||
//!
|
||||
//! The corpus records four "sibling default" rules (`Size_Y` inherits `Size_X`,
|
||||
//! `FCSRange` inherits `RadarRange`, …) used to recover values for units never
|
||||
//! visited at runtime. If the rules are really a *deduplication artefact*, then
|
||||
//! the legacy reader should report the field absent exactly when the two values
|
||||
//! are equal on disc — and never otherwise.
|
||||
use sylpheed_formats::{IdxdObject, PakArchive};
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC");
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
||||
let pairs = [
|
||||
("Size_Y", "Size_X"),
|
||||
("FCSRange", "RadarRange"),
|
||||
("DefencePoint", "AttackVesselPoint"),
|
||||
];
|
||||
// legacy-absent x equal-on-disc, as a 2x2 table per pair.
|
||||
let mut tally = [[[0usize; 2]; 2]; 3];
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
if !IdxdObject::is_idxd(&bytes) {
|
||||
continue;
|
||||
}
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Some(generic) = obj.record("Generic") else { continue };
|
||||
if generic.get("Size_X").is_none() {
|
||||
continue;
|
||||
}
|
||||
for (i, (field, sibling)) in pairs.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (generic.get(field), generic.get(sibling)) else {
|
||||
continue;
|
||||
};
|
||||
let legacy_absent = obj.get_f32(field).is_none();
|
||||
tally[i][usize::from(legacy_absent)][usize::from(a == b)] += 1;
|
||||
// The only cases where the rule PREDICTS WRONG: the reader misses
|
||||
// the field and the two values differ, so "inherit the sibling"
|
||||
// substitutes a number the disc contradicts.
|
||||
if legacy_absent && a != b {
|
||||
println!(
|
||||
" rule-wrong: {:08x} {field} = {a} but {sibling} = {b} (ID {:?})",
|
||||
e.name_hash,
|
||||
obj.get_raw("ID")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{:<14} {:>10} {:>10} {:>10} {:>10}", "pair", "seen+diff", "seen+eq", "MISS+diff", "MISS+eq");
|
||||
for (i, (field, sibling)) in pairs.iter().enumerate() {
|
||||
println!(
|
||||
"{:<14} {:>10} {:>10} {:>10} {:>10}",
|
||||
format!("{field}/{sibling}").chars().take(14).collect::<String>(),
|
||||
tally[i][0][0],
|
||||
tally[i][0][1],
|
||||
tally[i][1][0],
|
||||
tally[i][1][1]
|
||||
);
|
||||
}
|
||||
println!("\n'MISS+diff' > 0 refutes 'the reader only misses shared values'.");
|
||||
println!("'seen+eq' > 0 refutes 'a shared value is always invisible'.");
|
||||
}
|
||||
@@ -289,3 +289,60 @@ fn field_names_are_stored_disc() {
|
||||
// fields on the disc whose name still has to be recovered by preimage search.
|
||||
assert_eq!(hash_keyed_unnamed, 504);
|
||||
}
|
||||
|
||||
/// The "sibling default" rules are a string-pool deduplication artefact.
|
||||
///
|
||||
/// The corpus recorded that `Size_Y` inherits `Size_X` (and three similar pairs)
|
||||
/// for units the disc leaves "defaulted". The fields are not defaulted — they are
|
||||
/// on disc — and the old reader missed them precisely when the value was shared
|
||||
/// with the sibling, which is the condition the rule then "predicted".
|
||||
#[test]
|
||||
fn sibling_default_rules_are_a_dedup_artefact() {
|
||||
skip_without_disc!(root);
|
||||
let arc = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
||||
|
||||
let pairs = [
|
||||
("Size_Y", "Size_X"),
|
||||
("FCSRange", "RadarRange"),
|
||||
("DefencePoint", "AttackVesselPoint"),
|
||||
];
|
||||
// [pair][legacy_absent][equal_on_disc]
|
||||
let mut tally = [[[0usize; 2]; 2]; 3];
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
if !IdxdObject::is_idxd(&bytes) {
|
||||
continue;
|
||||
}
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Some(generic) = obj.record("Generic") else { continue };
|
||||
if generic.get("Size_X").is_none() {
|
||||
continue;
|
||||
}
|
||||
for (i, (field, sibling)) in pairs.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (generic.get(field), generic.get(sibling)) else {
|
||||
continue;
|
||||
};
|
||||
tally[i][usize::from(obj.get_f32(field).is_none())][usize::from(a == b)] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The field is present on disc far more often than it "differs", so the
|
||||
// premise "these fields are defaulted" is simply false.
|
||||
assert_eq!(tally[0][0][0] + tally[0][1][0], 90, "Size_Y differs from Size_X");
|
||||
|
||||
// The mechanism: a value shared with the sibling is ALWAYS invisible to the
|
||||
// legacy reader. If this cell were ever non-zero the dedup story would be
|
||||
// incomplete.
|
||||
for (i, (field, sibling)) in pairs.iter().enumerate() {
|
||||
assert_eq!(
|
||||
tally[i][0][1], 0,
|
||||
"{field}/{sibling}: legacy reader saw a value it shares with its sibling"
|
||||
);
|
||||
}
|
||||
|
||||
// …and it almost never misses a value that differs. Those few cells are
|
||||
// exactly where the rule predicts the wrong number.
|
||||
assert_eq!(tally[0][1][0], 0, "Size_Y: rule never wrong");
|
||||
assert_eq!(tally[1][1][0], 1, "FCSRange: UN_e011_ADAN_Attacker_B_HF_Wayne");
|
||||
assert_eq!(tally[2][1][0], 1, "DefencePoint: UN_e104_ADAN_Carrier");
|
||||
}
|
||||
|
||||
88
crates/sylpheed-formats/tests/zz_scratch_measure.rs
Normal file
88
crates/sylpheed-formats/tests/zz_scratch_measure.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! TEMPORARY measurement scratch — not for commit.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use sylpheed_formats::idxd::IdxdObject;
|
||||
use sylpheed_formats::game_data::schema;
|
||||
use sylpheed_formats::PakArchive;
|
||||
|
||||
fn pak(name: &str) -> Option<PakArchive> {
|
||||
let disc = std::env::var("SYLPHEED_DISC").ok()?;
|
||||
PakArchive::open(format!("{disc}/dat/{name}")).ok()
|
||||
}
|
||||
|
||||
fn as_f32(v: &str) -> Option<f32> {
|
||||
let v = v.strip_suffix(['f','F']).unwrap_or(v);
|
||||
v.parse().ok()
|
||||
}
|
||||
|
||||
/// field lists per loader
|
||||
fn keys_for(schema_id: u32) -> Vec<&'static str> {
|
||||
match schema_id {
|
||||
schema::WEAPON => vec!["Power","Velocity","MinimumVelocity","MaximumVelocity","MinimumRange","MaximumRange","LoadingCount","Interval","TriggerShotCount","Mass","Heating","Cooling","LifeTime","ID","Name","TargetType"],
|
||||
schema::UNIT => vec!["HP","IsDestructible","Size_X","Size_Y","Size_Z","Size_Radius","RadarRange","FCSRange","CruisingVelocity","MaximumVelocity","Acceleration","Deceleration","ShieldRatio","TurretCount","ScorePoint","ID","Name"],
|
||||
schema::VESSEL => vec!["HP","Size_X","Size_Y","Size_Z","RadarRange","FCSRange","MaximumVelocity","ShieldRatio","ScorePoint","TurretCount","BridgeCount","HatchCount","ShieldGeneratorCount","ThrusterCount","ID","Name","Model"],
|
||||
schema::PLAYER => vec!["AirDragFactor","GravityFactor","BulletLimit","LaserLimit","HomingLimit","SpaceSize","SupplyRange","MainMissionBonus","RankScore_S","RankScore_A","RankScore_B","RankScore_C","RankScore_D"],
|
||||
schema::CHARACTER => vec!["ID","Name","SideID","Unique"],
|
||||
schema::STAGE => vec!["BackGroundID"],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measure() {
|
||||
let Some(pak) = pak("GP_MAIN_GAME_E.pak") else { eprintln!("SKIP"); return };
|
||||
let schemas = [
|
||||
("WEAPON", schema::WEAPON), ("UNIT", schema::UNIT), ("VESSEL", schema::VESSEL),
|
||||
("PLAYER", schema::PLAYER), ("CHARACTER", schema::CHARACTER), ("STAGE", schema::STAGE),
|
||||
];
|
||||
let mut tot = [0usize;5]; // reads, ok, miss, flat, wrong
|
||||
for (label, sid) in schemas {
|
||||
let mut objs = 0usize;
|
||||
let mut c = [0usize;5];
|
||||
let mut recnames: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut fieldspread: BTreeMap<&str, BTreeMap<usize,usize>> = BTreeMap::new();
|
||||
let mut examples: Vec<String> = Vec::new();
|
||||
for e in pak.entries() {
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
if o.schema_hash != sid { continue }
|
||||
objs += 1;
|
||||
let Some(recs) = o.records() else { eprintln!("{label}: NO RECORD TABLE"); continue };
|
||||
for r in recs { *recnames.entry(r.name.clone()).or_default() += 1; }
|
||||
// flat map, as game_data builds it
|
||||
let fmap: BTreeMap<String,String> = o.resolved_fields().into_iter().map(|(k,v)|(k.to_string(),v.to_string())).collect();
|
||||
for key in keys_for(sid) {
|
||||
let hits: Vec<(&str,&str)> = recs.iter().filter_map(|r| r.get(key).map(|v| (r.name.as_str(), v))).collect();
|
||||
let flat: Option<&str> = if ["ID","Name","Model","TargetType","SideID","BackGroundID","Unique","IsDestructible"].contains(&key) {
|
||||
o.get_raw(key)
|
||||
} else {
|
||||
fmap.get(key).map(String::as_str)
|
||||
};
|
||||
if hits.is_empty() { continue }
|
||||
c[0]+=1; tot[0]+=1;
|
||||
let distinct: BTreeSet<&str> = hits.iter().map(|(_,v)| *v).collect();
|
||||
*fieldspread.entry(key).or_default().entry(hits.len()).or_default() += 1;
|
||||
if hits.len() > 1 && distinct.len() > 1 {
|
||||
c[3]+=1; tot[3]+=1;
|
||||
if examples.len() < 6 { examples.push(format!("FLAT {key}: {} records, {} distinct vals, flat={:?}, e.g. {:?}", hits.len(), distinct.len(), flat, &hits[..hits.len().min(4)])); }
|
||||
} else {
|
||||
let truth = hits[0].1;
|
||||
match flat {
|
||||
None => { c[2]+=1; tot[2]+=1;
|
||||
if examples.len()<6 && hits.len()==1 { examples.push(format!("MISS {key} = {truth:?} in record {:?}", hits[0].0)); } }
|
||||
Some(f) if f == truth || as_f32(f).is_some() && as_f32(f)==as_f32(truth) => { c[1]+=1; tot[1]+=1; }
|
||||
Some(f) => { c[4]+=1; tot[4]+=1; examples.push(format!("WRONG {key}: flat={f:?} truth={truth:?} rec={:?}", hits[0].0)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n=== {label} {sid:#010x}: {objs} objects; reads {} ok {} miss {} flat {} wrong {}", c[0],c[1],c[2],c[3],c[4]);
|
||||
let mut rn: Vec<_> = recnames.iter().collect();
|
||||
rn.sort_by_key(|(_,n)| std::cmp::Reverse(**n));
|
||||
println!(" records ({} distinct): {:?}", recnames.len(), rn.iter().take(25).map(|(k,n)|format!("{k}×{n}")).collect::<Vec<_>>());
|
||||
for (k, spread) in &fieldspread {
|
||||
println!(" field {k}: recs-per-object {:?}", spread);
|
||||
}
|
||||
for e in &examples { println!(" {e}"); }
|
||||
}
|
||||
println!("\n=== TOTAL reads {} ok {} miss {} flat {} wrong {}", tot[0],tot[1],tot[2],tot[3],tot[4]);
|
||||
}
|
||||
Reference in New Issue
Block a user