The runtime capture served its true purpose: as the answer key that exposed the static encoding. The XBG7 node joint table (node+0x44) holds NINE keyframe channels [TX TY TZ RY RX RZ SX SY SZ] behind EIGHT pointer slots shifted one track forward: channel k+1 = f64@(ptr[k]+8), and channel 0 -- TX, which no pointer names -- sits at ptr[0]-0x48 (tracks are 0x50-byte records, value at +8). The old 8-slot read took TY as X, RY as Y (usually 0 -- why both hulls stacked on the centreline) and never saw TX at all (the +-264 hull offsets). Euler order is Ry(ch3)*Rx(ch4)*Rz(ch5) with angles applied directly, pinned by the captured engine nacelle Rx(-15)*Rz(30) decomposition. mesh.rs gains read_trs9/node_rotation; scene_world_nodes and node_transforms both fixed (saber fin overrides unaffected). assemble_ship rewritten fully static and exact: - every composite-node instance placed (alias duplicates collapsed, real instances kept) including cross-id turret mounts (rou_e303_wep_01_root x2) - the e_rou_<id>_eng cluster rig mounts at GN_Engine_01 (two mirrored nacelles +-131 with -+30-degree cant + centre engine) -- world = frame o rig_local - brg/sld at their exact GN frames (frames were always exact) - shared-geometry twin pairs at +-TX: X-reflect the instance whose offset opposes the geometry's dominant side (viewer reverses winding on det<0) - squeezed node names resolve (wep02 -> wep_02_01) Ground-truth gate: ship::tests::static_assembly_matches_runtime_capture asserts static == the baked e106 capture for all 8 parts (T<1.0, R<0.02) plus both nacelles, both turrets, and the starboard-hull mirror. Viewer now uses pure static assembly; ship_capture + the baked table remain as the verification oracle. Renders show a coherent, bilaterally-symmetric destroyer. 81 formats-lib + all disc tests green; viewer builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.6 KiB
Rust
90 lines
3.6 KiB
Rust
//! RE diagnostic: raw-dump a composite scene graph's node records — name, table
|
|
//! pointers, and the FULL joint-table slot values (not just the 8 the decoder
|
|
//! currently reads) — to derive the true TRS slot layout against captured
|
|
//! ground truth.
|
|
//! cargo run --release --example node_dump -- <Stage.xpr path> <composite name> [slots]
|
|
use sylpheed_formats::mesh::xbg7_descriptor_range;
|
|
|
|
fn be32(d: &[u8], o: usize) -> u32 {
|
|
if o + 4 > d.len() {
|
|
return 0;
|
|
}
|
|
u32::from_be_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
|
|
}
|
|
fn be_f64(d: &[u8], o: usize) -> f64 {
|
|
if o + 8 > d.len() {
|
|
return f64::NAN;
|
|
}
|
|
f64::from_be_bytes(d[o..o + 8].try_into().unwrap())
|
|
}
|
|
|
|
fn main() {
|
|
let a: Vec<String> = std::env::args().collect();
|
|
let bytes = std::fs::read(&a[1]).unwrap();
|
|
let comp = &a[2];
|
|
let nslots: usize = a.get(3).and_then(|s| s.parse().ok()).unwrap_or(12);
|
|
let (desc, desc_end) = xbg7_descriptor_range(&bytes, comp).expect("composite found");
|
|
let d = &bytes[desc..desc_end];
|
|
println!("descriptor [{desc:#x}..{desc_end:#x}) len {:#x}", d.len());
|
|
|
|
let mut i = 0usize;
|
|
while i + 3 <= d.len() {
|
|
let starts = (i + 4 <= d.len() && &d[i..i + 4] == b"rou_")
|
|
|| (i + 3 <= d.len() && &d[i..i + 3] == b"GN_");
|
|
if !starts {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
let mut j = i;
|
|
while j < d.len() && d[j] != 0 && d[j].is_ascii_graphic() {
|
|
j += 1;
|
|
}
|
|
let name = std::str::from_utf8(&d[i..j]).unwrap_or("?").to_string();
|
|
if name.ends_with("_col") || name.ends_with("_spc") || name.ends_with("_gls") || name.ends_with("_lum") {
|
|
i = j.max(i + 1);
|
|
continue;
|
|
}
|
|
// find the 0xFFFFFFFF end marker
|
|
let mut ff = i;
|
|
let scan_end = (i + 0x90).min(d.len().saturating_sub(4));
|
|
while ff < scan_end && be32(d, ff) != 0xFFFF_FFFF {
|
|
ff += 4;
|
|
}
|
|
if ff >= scan_end || ff < 0x10 {
|
|
i = j.max(i + 1);
|
|
continue;
|
|
}
|
|
let t44 = be32(d, ff - 0x10) as usize;
|
|
let t48 = be32(d, ff - 0x0C) as usize;
|
|
let child = be32(d, ff - 0x08) as usize;
|
|
let sib = be32(d, ff - 0x04) as usize;
|
|
// Dump the record's u32s between name end and marker for structure context.
|
|
let rec_u32: Vec<String> = ((j + 1)..ff)
|
|
.step_by(4)
|
|
.map(|o| format!("{:08X}", be32(d, o)))
|
|
.collect();
|
|
println!("\nNODE {name} @{i:#x} ff@{ff:#x} t44={t44:#x} t48={t48:#x} child={child:#x} sib={sib:#x}");
|
|
println!(" rec: {}", rec_u32.join(" "));
|
|
if t44 != 0 && t44 < d.len() {
|
|
// Joint table: nslots pointer slots. Each slot record appears to be a
|
|
// keyframe track: a value ALSO sits 0x48 BEFORE the pointed address
|
|
// (the rest-pose key A), while the decoder currently reads ptr+8 (B).
|
|
for k in 0..nslots {
|
|
let p = be32(d, t44 + k * 4) as usize;
|
|
if p == 0 || p + 24 > d.len() {
|
|
continue;
|
|
}
|
|
let a = if p >= 0x48 { be_f64(d, p - 0x48) } else { f64::NAN };
|
|
let b = be_f64(d, p + 8);
|
|
let head = if p >= 0x50 { be32(d, p - 0x50) } else { 0 };
|
|
let t_a = if p >= 0x50 { be_f64(d, p - 0x50) } else { f64::NAN };
|
|
let t_b = be_f64(d, p);
|
|
println!(
|
|
" slot{k:2} @{p:#x} head={head:08X}: A={a:12.5} (t={t_a:8.3}) B={b:12.5} (t={t_b:8.3})"
|
|
);
|
|
}
|
|
}
|
|
i = j.max(i + 1);
|
|
}
|
|
}
|