//! The definition-object layout must keep agreeing with the disc records. //! //! `unit_layout` was read out of the game's loader and checked against objects //! dumped from a running Stage 02. This pins that check: every mapped float //! field of every identified object must equal the value its disc record sets. //! It needs no emulator — the dump is checked in under `docs/re/captures/`. use std::collections::BTreeMap; use sylpheed_formats::idxd::IdxdObject; use sylpheed_formats::pak::PakArchive; use sylpheed_formats::unit_layout::{fields, Kind}; /// Live objects identified in the dump, with the disc record each one is. /// `bf001` is here because full-record agreement is what identified it: the /// four-value signature also fitted `UN_be005_ADAN_SpaceFortress`, which /// disagrees on `Color_R`/`Color_G`. const IDENTIFIED: &[(&str, &str)] = &[ ("UN_f106_TCAF_Destroyer", "0xbd3ee300"), ("UN_f105_TCAF_Cruiser", "0xbd40e200"), ("UN_e105_ADAN_Cruiser", "0xbd3fd800"), ("UN_e106_ADAN_Destroyer", "0xbd3e6f80"), ("UN_f101_TCAF_Acropolis", "0xbd3e1f00"), ("UN_bf001_TCAF_SchlosBase", "0xbd3b6a00"), ("UN_e007_ADAN_Turret", "0xbd3dea80"), ("UN_e010_ADAN_Attacker_S", "0xbd3faa80"), ("UN_f003_TCAF_ArrowHead", "0xbd3dbd00"), ("UN_f001_TCAF_DeltaSaber_T", "0xbd3dfc00"), ("UN_f001_TCAF_DeltaSaber_T_Player", "0xbd3da100"), ]; const DUMP: &str = include_str!("../../../docs/re/captures/stage02-live-unit-definitions-deep.txt"); fn disc_root() -> Option { if let Ok(p) = std::env::var("SYLPHEED_DISC") { if std::path::Path::new(&p).join("dat").is_dir() { return Some(p); } } let d = "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)"; std::path::Path::new(d).join("dat").is_dir().then(|| d.to_string()) } /// `va -> offset -> value`, from the dump's `addr +off hex u32 f32` columns. fn live() -> BTreeMap> { let mut out: BTreeMap> = BTreeMap::new(); let mut cur = String::new(); for line in DUMP.lines() { if let Some(rest) = line.strip_prefix("=== ") { cur = rest.trim().to_string(); continue; } let f: Vec<&str> = line.split_whitespace().collect(); if f.len() >= 5 && f[1].starts_with('+') { if let (Ok(off), Ok(val)) = (usize::from_str_radix(&f[1][1..], 16), f[4].parse::()) { out.entry(cur.clone()).or_default().insert(off, val); } } } out } #[test] fn mapped_fields_match_the_disc_records() { let Some(disc) = disc_root() else { eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); return; }; let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("main pak"); let live = live(); let floats: Vec<_> = fields().into_iter().filter(|f| f.kind == Kind::F32).collect(); let (mut agree, mut bad) = (0usize, Vec::new()); for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; let Some(id) = obj.get_raw("ID") else { continue }; let Some((_, va)) = IDENTIFIED.iter().find(|(i, _)| *i == id) else { continue }; let Some(words) = live.get(*va) else { continue }; for f in &floats { let (Some(want), Some(got)) = (obj.get_f32(f.name), words.get(&f.offset)) else { continue; }; // Angle fields are degrees on disc and radians in the object. let rad = want * std::f32::consts::PI / 180.0; if (want - got).abs() <= want.abs() * 1e-4 || (rad - got).abs() <= rad.abs() * 1e-4 { agree += 1; } else { bad.push(format!("{id}.{} disc {want} vs live +{} = {got}", f.name, f.offset)); } } } assert!(bad.is_empty(), "{} fields disagree: {bad:?}", bad.len()); assert!(agree >= 400, "expected ≥400 agreeing fields, got {agree}"); }