ship: use only the primary composite — fixes parts floating mid-ship

The sub-composites (`e_rou_<id>_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_<id>`, 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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-23 21:52:20 +02:00
parent a8b5691d1c
commit 8c8e2dbeb4

View File

@@ -230,21 +230,38 @@ fn composites_for<'a>(names: &'a [String], id: &str) -> Vec<&'a String> {
pub fn assemble_ship(bytes: &[u8], id: &str) -> Vec<ScenePart> { pub fn assemble_ship(bytes: &[u8], id: &str) -> Vec<ScenePart> {
let names = xbg7_resource_names(bytes); let names = xbg7_resource_names(bytes);
let resources: HashSet<String> = names.iter().cloned().collect(); let resources: HashSet<String> = 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_<id>_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<ScenePart>)> = 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<ScenePart> = Vec::new(); let mut placed: Vec<ScenePart> = Vec::new();
let mut placed_res: HashSet<String> = HashSet::new(); let mut placed_res: HashSet<String> = HashSet::new();
// GN hardpoint frames, world-space, from every composite. // GN hardpoint frames, world-space, from the primary composite.
let mut frames: Vec<ScenePart> = Vec::new(); let mut frames: Vec<ScenePart> = Vec::new();
for c in comps { for node in nodes {
for node in scene_world_nodes(bytes, c) {
if node.resource.starts_with("GN_") { if node.resource.starts_with("GN_") {
frames.push(node); frames.push(node.clone());
} else if let Some(res) = resolve_hull_resource(&node.resource, &resources) { } else if let Some(res) = resolve_hull_resource(&node.resource, &resources) {
// Only the ship's own parts. A composite also mounts shared, // Only the ship's own parts. A composite also mounts shared, cross-id
// cross-id turret models (`e303_wep_*` on an `e106` hull); those // turret models (`e303_wep_*` on an `e106` hull); those need the
// need the per-mount Vessel recipe and are placed separately. // per-mount Vessel recipe and are placed separately.
if ship_id_of(&res) != Some(id) { if ship_id_of(&res) != Some(id) {
continue; continue;
} }
@@ -253,7 +270,6 @@ pub fn assemble_ship(bytes: &[u8], id: &str) -> Vec<ScenePart> {
} }
} }
} }
}
// Place unplaced external parts at their Vessel hardpoint frame. // Place unplaced external parts at their Vessel hardpoint frame.
const CATS: [(&str, &str); 3] = [("brg", "Bridge"), ("sld", "ShieldG"), ("eng", "Engine")]; const CATS: [(&str, &str); 3] = [("brg", "Bridge"), ("sld", "ShieldG"), ("eng", "Engine")];