From 8c8e2dbeb4df4bb6e1ac19976442b23abb3ef132 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 23 Jul 2026 21:52:20 +0200 Subject: [PATCH] =?UTF-8?q?ship:=20use=20only=20the=20primary=20composite?= =?UTF-8?q?=20=E2=80=94=20fixes=20parts=20floating=20mid-ship?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sub-composites (`e_rou__eng`, `_wep_*`) are LOCAL detail rigs: their nodes are posed in their own frame, not ship-space, so folding them in placed the engines at ~mid-ship (Z≈99) instead of aft — the "floating in air" parts. The PRIMARY composite (`e_rou_`, the one with the most nodes) already carries the full ship-space layout: every hull body AND every `GN_*` hardpoint frame (GN_Bridge at the stern, GN_Engine aft, GN_ShieldG amidships…). Assemble from that one composite only; engines/bridge/shield now resolve to their real aft/mid frames. Robust to the odd single-composite case (e108's `e_rou_e108_Missile_open` is still picked as its own primary). Co-Authored-By: Claude Opus 4.8 --- crates/sylpheed-formats/src/ship.rs | 48 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/sylpheed-formats/src/ship.rs b/crates/sylpheed-formats/src/ship.rs index ce9de2a..e66cf92 100644 --- a/crates/sylpheed-formats/src/ship.rs +++ b/crates/sylpheed-formats/src/ship.rs @@ -230,27 +230,43 @@ fn composites_for<'a>(names: &'a [String], id: &str) -> Vec<&'a String> { pub fn assemble_ship(bytes: &[u8], id: &str) -> Vec { let names = xbg7_resource_names(bytes); let resources: HashSet = names.iter().cloned().collect(); - let comps = composites_for(&names, id); + + // The PRIMARY composite carries the real ship-space layout (hull bodies + + // every `GN_*` hardpoint frame). Sibling composites (`e_rou__eng`, + // `_wep_*`) are LOCAL detail rigs — their nodes are in their own frame, not + // ship-space, so folding them in floats parts mid-ship. Pick the composite + // with the most nodes; that is the assembled hull. + let scenes: Vec<(String, Vec)> = composites_for(&names, id) + .into_iter() + .map(|c| (c.clone(), scene_world_nodes(bytes, c))) + .collect(); + let Some((_, nodes)) = scenes.iter().max_by_key(|(_, n)| n.len()) else { + // No composite at all → raw fallback below. + let mut placed = Vec::new(); + const ID: [[f32; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + for part in names.iter().filter(|n| is_base_part(n) && ship_id_of(n) == Some(id)) { + placed.push(ScenePart { resource: part.clone(), m: ID, t: [0.0; 3] }); + } + return placed; + }; let mut placed: Vec = Vec::new(); let mut placed_res: HashSet = HashSet::new(); - // GN hardpoint frames, world-space, from every composite. + // GN hardpoint frames, world-space, from the primary composite. let mut frames: Vec = Vec::new(); - for c in comps { - for node in scene_world_nodes(bytes, c) { - if node.resource.starts_with("GN_") { - frames.push(node); - } else if let Some(res) = resolve_hull_resource(&node.resource, &resources) { - // Only the ship's own parts. A composite also mounts shared, - // cross-id turret models (`e303_wep_*` on an `e106` hull); those - // need the per-mount Vessel recipe and are placed separately. - if ship_id_of(&res) != Some(id) { - continue; - } - if placed_res.insert(res.clone()) { - placed.push(ScenePart { resource: res, m: node.m, t: node.t }); - } + for node in nodes { + if node.resource.starts_with("GN_") { + frames.push(node.clone()); + } else if let Some(res) = resolve_hull_resource(&node.resource, &resources) { + // Only the ship's own parts. A composite also mounts shared, cross-id + // turret models (`e303_wep_*` on an `e106` hull); those need the + // per-mount Vessel recipe and are placed separately. + if ship_id_of(&res) != Some(id) { + continue; + } + if placed_res.insert(res.clone()) { + placed.push(ScenePart { resource: res, m: node.m, t: node.t }); } } }