ship: index-less brg/eng/sld parts never matched their GN frame — 34 ships assembled without a bridge
Tier 3 matched a part to its hardpoint by trailing index, so `e105_brg` compared "01" == "" against GN_Bridge_01 and fell through silently. The runtime capture is what exposed it: the game draws the bridge and places it at [0, 70, -1850] rel e105_bdy_01, and assemble_ship emitted nothing there. With no index to match on, take the lowest-numbered frame of the category. Diffing assemble_ship part counts across every container: 34 (stage, ship) entries gain parts — e102 +2 (bridge and engine), e104 +1, e105 +1, Stages 02-29. ship_audit is unchanged, so nothing regressed, and the capture now agrees to dT 0.03 / dR 0.000. Also fixes the diff itself: correlate_frames compared static against a rotation sampled from the first block, which can belong to another INSTANCE of the class. Scoped to the position-agreeing cluster, e105_eng_01 goes 1.711 -> 0.000 and both e106 nacelles to 0.000. The one remaining rotation delta (e106_wep_02_01, 0.134) is a turret whose rotation varies by 0.182 between blocks that agree on its position — the runtime disagrees with itself more than with the assembler. The new rotVar column makes that distinction visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,13 +143,14 @@ fn main() {
|
||||
// is meaningless; the largest cluster of blocks that agree with each other
|
||||
// is the placement, and the rest are honestly reported as other instances.
|
||||
const TOL: f32 = 25.0; // float noise in the WV products, measured ≤0.4
|
||||
let cluster = |ts: &Vec<[f32; 3]>| -> (Vec<[f32; 3]>, usize) {
|
||||
let mut best: Vec<[f32; 3]> = Vec::new();
|
||||
let cluster = |ts: &Vec<[f32; 3]>| -> (Vec<usize>, usize) {
|
||||
let mut best: Vec<usize> = Vec::new();
|
||||
for seed in ts {
|
||||
let near: Vec<[f32; 3]> = ts
|
||||
let near: Vec<usize> = ts
|
||||
.iter()
|
||||
.filter(|t| (0..3).all(|i| (t[i] - seed[i]).abs() < TOL))
|
||||
.copied()
|
||||
.enumerate()
|
||||
.filter(|(_, t)| (0..3).all(|i| (t[i] - seed[i]).abs() < TOL))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
if near.len() > best.len() {
|
||||
best = near;
|
||||
@@ -160,15 +161,15 @@ fn main() {
|
||||
};
|
||||
println!("\nacross {used_frames} blocks — consensus T (largest agreeing cluster):");
|
||||
let mut agree = 0usize;
|
||||
let mut consensus: BTreeMap<String, [f32; 3]> = BTreeMap::new();
|
||||
let mut consensus: BTreeMap<String, ([f32; 3], [[f32; 3]; 3])> = BTreeMap::new();
|
||||
for (part, ts) in &samples {
|
||||
let (cl, outliers) = cluster(ts);
|
||||
let (cl_idx, outliers) = cluster(ts);
|
||||
let cl: Vec<[f32; 3]> = cl_idx.iter().map(|&i| ts[i]).collect();
|
||||
let med = [
|
||||
median(cl.iter().map(|t| t[0]).collect()),
|
||||
median(cl.iter().map(|t| t[1]).collect()),
|
||||
median(cl.iter().map(|t| t[2]).collect()),
|
||||
];
|
||||
consensus.insert(part.clone(), med);
|
||||
let spread: Vec<f32> = (0..3)
|
||||
.map(|a| {
|
||||
let v: Vec<f32> = cl.iter().map(|t| t[a]).collect();
|
||||
@@ -181,8 +182,32 @@ fn main() {
|
||||
agree += 1;
|
||||
"AGREES"
|
||||
};
|
||||
// How much the ROTATION varies between blocks that agree on position.
|
||||
// A part bolted to the hull reads 0 here; a part that is articulating
|
||||
// (turret aiming, engine gimballing) does not — which is what separates
|
||||
// "the assembler has the rotation wrong" from "the part moved".
|
||||
let all_ms = rots.get(part).cloned().unwrap_or_default();
|
||||
let ms: Vec<[[f32; 3]; 3]> =
|
||||
cl_idx.iter().filter_map(|&i| all_ms.get(i).copied()).collect();
|
||||
// Keep a rotation from INSIDE the cluster: the first sample overall can
|
||||
// belong to another instance, and diffing static against that reads as a
|
||||
// rotation error that is really an instance mix-up.
|
||||
consensus.insert(
|
||||
part.clone(),
|
||||
(med, ms.first().copied().unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])),
|
||||
);
|
||||
let rot_var = ms
|
||||
.iter()
|
||||
.flat_map(|a| ms.iter().map(move |b| (a, b)))
|
||||
.map(|(a, b)| {
|
||||
(0..3)
|
||||
.flat_map(|i| (0..3).map(move |j| (i, j)))
|
||||
.map(|(i, j)| (a[i][j] - b[i][j]).abs())
|
||||
.fold(0.0f32, f32::max)
|
||||
})
|
||||
.fold(0.0f32, f32::max);
|
||||
println!(
|
||||
" {part:18} {:2}/{:2} blocks T=[{:9.1}{:9.1}{:9.1}] spread=[{:6.2}{:6.2}{:6.2}] {verdict}{}",
|
||||
" {part:18} {:2}/{:2} blocks T=[{:9.1}{:9.1}{:9.1}] spread=[{:6.2}{:6.2}{:6.2}] rotVar={rot_var:5.3} {verdict}{}",
|
||||
cl.len(), ts.len(), med[0], med[1], med[2], spread[0], spread[1], spread[2],
|
||||
if outliers > 0 { format!(" (+{outliers} other-instance)") } else { String::new() }
|
||||
);
|
||||
@@ -212,8 +237,8 @@ fn main() {
|
||||
println!("\nstatic vs runtime (both relative to {}):", sref.resource);
|
||||
let mut worst_t = 0.0f32;
|
||||
let mut worst_r = 0.0f32;
|
||||
for (part, med) in &consensus {
|
||||
let med = *med;
|
||||
for (part, (med, rm)) in &consensus {
|
||||
let (med, rm) = (*med, *rm);
|
||||
// A part may be instanced (mirrored twins share a resource name); take
|
||||
// the static copy that lands nearest the captured one.
|
||||
let cands: Vec<&sylpheed_formats::mesh::ScenePart> =
|
||||
@@ -238,7 +263,6 @@ fn main() {
|
||||
let r = rel(best);
|
||||
let dt: Vec<f32> = (0..3).map(|i| r[i] - med[i]).collect();
|
||||
let dtm = dt.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
|
||||
let rm = rots.get(part).map(|v| v[0]).unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
|
||||
let drm = (0..3)
|
||||
.flat_map(|i| (0..3).map(move |j| (i, j)))
|
||||
.map(|(i, j)| (best.m[i][j] - rm[i][j]).abs())
|
||||
|
||||
@@ -349,9 +349,21 @@ pub fn assemble_ship(bytes: &[u8], id: &str, include_external: bool) -> Vec<Scen
|
||||
let Some((_, gncat)) = CATS.iter().find(|(c, _)| *c == cat) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(frame) =
|
||||
frames.iter().find(|f| f.resource.contains(gncat) && trailing_index(&f.resource) == idx)
|
||||
{
|
||||
// An index-less part (`e105_brg`, against a `GN_Bridge_01` frame) used to
|
||||
// compare `"01" == ""` and fall through, so the bridge was silently
|
||||
// dropped from the assembly while the game draws it — caught by a runtime
|
||||
// capture, which places `e105_brg` at the `GN_Bridge_01` frame exactly.
|
||||
// With no index to match on, take the lowest-numbered frame of the
|
||||
// category; an indexed part still matches its own index only.
|
||||
let mut cands: Vec<&ScenePart> = frames
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
f.resource.contains(gncat)
|
||||
&& (idx.is_empty() || trailing_index(&f.resource) == idx)
|
||||
})
|
||||
.collect();
|
||||
cands.sort_by_key(|f| trailing_index(&f.resource).parse::<u32>().unwrap_or(u32::MAX));
|
||||
if let Some(frame) = cands.first().copied() {
|
||||
placed.push(ScenePart { resource: part.clone(), m: frame.m, t: frame.t, s: frame.s });
|
||||
placed_res.insert(part.clone());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user