formats: ship module — reconstruct capital ships from XBG7 part families
Project Sylpheed never stores a capital ship as a single mesh. Each ship is modelled, then split into destructible parts (hull segments, bridge, engines, integral turrets), each shipped as its own XBG7 resource inside a hidden/resource3d/Stage_SNN.xpr container. The split lets the game hide/replace a part when the player destroys it. Crucially each part keeps its ship-local position, so reconstructing the whole ship is: draw every base part of one id together, untransformed. New `ship` module: Faction (from the e/f/n id prefix — ADAN/TCAF/Neutral), ShipModel, ship_id_of, is_base_part (skips _l/_m/_d LODs, _bNN/_dead/_break destruction geometry, e_rou_ scene nodes), and ships_in_container to group a stage's resources into whole ships. Supporting: - mesh::xbg7_resource_names — list a container's XBG7 resource names (directory walk only, no geometry decode). - Xbg7Model::models_named — decode only the named resources (assemble a ship from its ~8 parts instead of the whole ~300-resource stage). - game_data::Vessel gains `model` (the rou_<id> hull stem) to link stats to the 3D part family. Tests: id/part classification + a disc-gated reconstruction of the ADAN e108 frigate from Stage_S02. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,37 @@ pub fn count_xbg7(bytes: &[u8]) -> usize {
|
||||
n
|
||||
}
|
||||
|
||||
/// List every XBG7 resource name in an XPR2 container, in directory order.
|
||||
///
|
||||
/// Cheap: walks only the resource directory (no geometry decode). Used to
|
||||
/// discover which container holds a named hull / part model when assembling a
|
||||
/// multi-part ship from a [`crate::game_data::Vessel`] recipe.
|
||||
pub fn xbg7_resource_names(bytes: &[u8]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
||||
return out;
|
||||
}
|
||||
let mut cur = Cursor::new(bytes);
|
||||
let header = match Xpr2Header::read(&mut cur) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return out,
|
||||
};
|
||||
const DIR_BASE: usize = 0x10;
|
||||
for _ in 0..header.num_resources {
|
||||
let e = match Xpr2ResourceEntry::read(&mut cur) {
|
||||
Ok(e) => e,
|
||||
Err(_) => break,
|
||||
};
|
||||
if &e.type_tag != b"XBG7" {
|
||||
continue;
|
||||
}
|
||||
if let Some(n) = read_cstr(bytes, e.name_offset as usize + DIR_BASE) {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl Xbg7Model {
|
||||
/// Total vertex / triangle counts across all sub-meshes.
|
||||
pub fn totals(&self) -> (usize, usize) {
|
||||
@@ -402,6 +433,18 @@ impl Xbg7Model {
|
||||
Self::anchor_models_cancellable(bytes, min_consistency, &|| false)
|
||||
}
|
||||
|
||||
/// Decode only the named XBG7 resources from a container (content-anchored,
|
||||
/// abortable). Used to assemble a whole ship from just its part family
|
||||
/// (`e106_bdy_*`, `e106_brg_*`, …) instead of decoding the whole ~300-resource
|
||||
/// stage. Order follows the container's directory. See [`crate::ship`].
|
||||
pub fn models_named(
|
||||
bytes: &[u8],
|
||||
wanted: &std::collections::HashSet<String>,
|
||||
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||
) -> Vec<Xbg7Model> {
|
||||
Self::anchor_models_filtered(bytes, 0.0, should_cancel, Some(wanted))
|
||||
}
|
||||
|
||||
/// [`Xbg7Model::anchor_models`] with a cancellation poll checked between
|
||||
/// resources (a large stage container holds hundreds). See
|
||||
/// [`Xbg7Model::stage_models_cancellable`].
|
||||
@@ -409,6 +452,15 @@ impl Xbg7Model {
|
||||
bytes: &[u8],
|
||||
min_consistency: f32,
|
||||
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||
) -> Vec<Xbg7Model> {
|
||||
Self::anchor_models_filtered(bytes, min_consistency, should_cancel, None)
|
||||
}
|
||||
|
||||
fn anchor_models_filtered(
|
||||
bytes: &[u8],
|
||||
min_consistency: f32,
|
||||
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||
wanted: Option<&std::collections::HashSet<String>>,
|
||||
) -> Vec<Xbg7Model> {
|
||||
let mut out = Vec::new();
|
||||
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
||||
@@ -464,6 +516,11 @@ impl Xbg7Model {
|
||||
}
|
||||
let name = read_cstr(bytes, e.name_offset as usize + DIR_BASE)
|
||||
.unwrap_or_else(|| "XBG7".to_string());
|
||||
if let Some(w) = wanted {
|
||||
if !w.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
resources.push(Res {
|
||||
name,
|
||||
markers,
|
||||
|
||||
Reference in New Issue
Block a user