diff options
Diffstat (limited to 'crates/fparkan-prototype')
| -rw-r--r-- | crates/fparkan-prototype/Cargo.toml | 7 | ||||
| -rw-r--r-- | crates/fparkan-prototype/src/lib.rs | 824 |
2 files changed, 549 insertions, 282 deletions
diff --git a/crates/fparkan-prototype/Cargo.toml b/crates/fparkan-prototype/Cargo.toml index 4825faf..4d9b958 100644 --- a/crates/fparkan-prototype/Cargo.toml +++ b/crates/fparkan-prototype/Cargo.toml @@ -8,13 +8,12 @@ repository.workspace = true [dependencies] encoding_rs = "0.8" fparkan-binary = { path = "../fparkan-binary" } -fparkan-material = { path = "../fparkan-material" } -fparkan-msh = { path = "../fparkan-msh" } -fparkan-nres = { path = "../fparkan-nres" } fparkan-path = { path = "../fparkan-path" } fparkan-resource = { path = "../fparkan-resource" } -fparkan-texm = { path = "../fparkan-texm" } fparkan-vfs = { path = "../fparkan-vfs" } [lints] workspace = true + +[dev-dependencies] +fparkan-nres = { path = "../fparkan-nres" } diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs index 32e736b..c05fd27 100644 --- a/crates/fparkan-prototype/src/lib.rs +++ b/crates/fparkan-prototype/src/lib.rs @@ -3,14 +3,10 @@ use encoding_rs::WINDOWS_1251; use fparkan_binary::{checked_count_bytes, Cursor, DecodeError}; -use fparkan_material::{decode_wear, resolve_material, WEAR_KIND}; -use fparkan_msh::{decode_msh, validate_msh, MshError}; -use fparkan_nres::ReadProfile; use fparkan_path::{normalize_relative, NormalizedPath, PathPolicy, ResourceName}; use fparkan_resource::{ archive_path, resource_name, ResourceError, ResourceKey, ResourceRepository, }; -use fparkan_texm::decode_texm; use fparkan_vfs::{Vfs, VfsError}; use std::sync::Arc; @@ -111,6 +107,141 @@ pub struct PrototypeGraph { pub roots: Vec<PrototypeKey>, /// Effective prototype requests after unit DAT expansion. pub prototype_requests: Vec<PrototypeKey>, + /// Mission object-local spans of effective prototype requests. + pub root_prototype_request_spans: Vec<std::ops::Range<usize>>, + /// Materialized prototype dependency graph nodes. + pub nodes: Vec<PrototypeGraphNode>, + /// Materialized prototype dependency graph edges. + pub edges: Vec<PrototypeGraphEdgeInstance>, +} + +/// Stable node identifier. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct PrototypeGraphNodeId(pub u32); + +/// Stable edge identifier. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct PrototypeGraphEdgeId(pub u32); + +/// Edge requiredness/fallback policy for a graph dependency. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrototypeGraphRequiredness { + /// Missing edge should fail mission load. + Required, + /// Missing edge is tolerated and handled by fallback policy. + Optional, + /// Edge was produced by an explicit fallback transition. + Fallback, +} + +/// Source provenance for graph construction and failures. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrototypeGraphProvenance { + /// Root mission object index that initiated traversal. + pub root_index: usize, + /// Immediate parent edge that discovered this edge. + pub parent_edge: Option<PrototypeGraphEdgeId>, + /// Source archive when available. + pub archive: Option<String>, + /// Source resource key when available. + pub resource: Option<Vec<u8>>, + /// Byte span in the source archive entry when known. + pub span: Option<(u64, u64)>, +} + +/// Prototype graph node kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrototypeGraphNodeKind { + /// Mission root key. + MissionRoot, + /// Unit DAT root key. + UnitDatRoot, + /// Resolved prototype request. + Prototype, + /// Mesh dependency. + MeshResource, + /// Non-geometric prototype. + NonGeometric, +} + +/// Prototype graph node record. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrototypeGraphNode { + /// Stable identifier. + pub id: PrototypeGraphNodeId, + /// Node kind. + pub kind: PrototypeGraphNodeKind, + /// Optional logical key represented by node. + pub key: Option<PrototypeKey>, + /// Optional resource represented by node. + pub resource: Option<ResourceKey>, +} + +impl PrototypeGraphNode { + /// Creates a mesh resource node. + #[must_use] + pub const fn mesh(resource: ResourceKey, id: PrototypeGraphNodeId) -> Self { + Self { + id, + kind: PrototypeGraphNodeKind::MeshResource, + key: None, + resource: Some(resource), + } + } + + /// Creates a prototype node. + #[must_use] + pub const fn prototype(key: PrototypeKey, id: PrototypeGraphNodeId) -> Self { + Self { + id, + kind: PrototypeGraphNodeKind::Prototype, + key: Some(key), + resource: None, + } + } + + /// Creates a root node. + #[must_use] + pub const fn root(key: PrototypeKey, is_unit_dat: bool, id: PrototypeGraphNodeId) -> Self { + Self { + id, + kind: if is_unit_dat { + PrototypeGraphNodeKind::UnitDatRoot + } else { + PrototypeGraphNodeKind::MissionRoot + }, + key: Some(key), + resource: None, + } + } +} + +/// Prototype graph edge kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrototypeGraphEdgeKind { + /// Mission root to resolved prototype. + MissionToRoot, + /// Unit component to prototype. + UnitDatToComponent, + /// Prototype to mesh dependency. + PrototypeToMesh, +} + +/// Prototype graph edge record. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrototypeGraphEdgeInstance { + /// Stable identifier. + pub id: PrototypeGraphEdgeId, + /// Source node. + pub from: PrototypeGraphNodeId, + /// Destination node. + pub to: PrototypeGraphNodeId, + /// Edge kind. + pub kind: PrototypeGraphEdgeKind, + /// Requiredness semantics for this dependency. + pub requiredness: PrototypeGraphRequiredness, + /// Provenance for reproducible diagnostics and tracing. + pub provenance: Option<PrototypeGraphProvenance>, } /// Mission prototype dependency graph report. @@ -152,8 +283,28 @@ impl PrototypeGraphReport { /// Returns true when all reachable mission roots resolved. #[must_use] pub fn is_success(&self) -> bool { - self.failures.is_empty() - && self.resolved_count == self.direct_reference_count + self.unit_component_count + if self + .failures + .iter() + .any(|failure| failure.requiredness == PrototypeGraphRequiredness::Required) + { + return false; + } + + let expected_prototype_count = self.direct_reference_count + self.unit_component_count; + if self.resolved_count != expected_prototype_count { + return false; + } + + if self.wear_resolved_count > self.wear_request_count + || self.material_resolved_count > self.material_slot_count + || self.texture_resolved_count > self.texture_request_count + || self.lightmap_resolved_count > self.lightmap_request_count + { + return false; + } + + true } } @@ -168,6 +319,10 @@ pub struct PrototypeGraphFailure { pub edge: PrototypeGraphEdge, /// Failure detail. pub message: String, + /// Requiredness that triggered this failure. + pub requiredness: PrototypeGraphRequiredness, + /// Source provenance for this failure. + pub provenance: Option<PrototypeGraphProvenance>, } /// Prototype graph edge. @@ -203,11 +358,9 @@ pub enum PrototypeError { /// Invalid path. InvalidPath(String), /// VFS error. - Vfs(String), + Vfs(VfsError), /// Resource repository error. - Resource(String), - /// Referenced mesh is present but invalid. - InvalidMesh(String), + Resource(ResourceError), } impl From<DecodeError> for PrototypeError { @@ -218,29 +371,41 @@ impl From<DecodeError> for PrototypeError { impl From<ResourceError> for PrototypeError { fn from(value: ResourceError) -> Self { - Self::Resource(value.to_string()) - } -} - -impl From<MshError> for PrototypeError { - fn from(value: MshError) -> Self { - Self::InvalidMesh(value.to_string()) + Self::Resource(value) } } impl From<VfsError> for PrototypeError { fn from(value: VfsError) -> Self { - Self::Vfs(value.to_string()) + Self::Vfs(value) } } impl std::fmt::Display for PrototypeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") + match self { + Self::Decode(source) => write!(f, "decode error: {source}"), + Self::InvalidSize => write!(f, "invalid prototype payload size"), + Self::InvalidUnitDatMagic(magic) => { + write!(f, "invalid unit DAT magic: {magic:#010X}") + } + Self::InvalidPath(value) => write!(f, "invalid path: {value}"), + Self::Vfs(source) => write!(f, "vfs error: {source}"), + Self::Resource(source) => write!(f, "resource error: {source}"), + } } } -impl std::error::Error for PrototypeError {} +impl std::error::Error for PrototypeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Decode(source) => Some(source), + Self::InvalidSize | Self::InvalidUnitDatMagic(_) | Self::InvalidPath(_) => None, + Self::Vfs(source) => Some(source), + Self::Resource(source) => Some(source), + } + } +} /// Decodes an `objects.rlb` registry entry as 64-byte records. /// @@ -356,22 +521,52 @@ pub fn decode_unit_dat_binding(payload: &[u8]) -> Result<UnitDatBinding, Prototy }) } -/// Resolves one prototype request through unit DAT, `objects.rlb`, and direct mesh lookup. +/// Resolves all prototype requests for a root resource, including every component +/// entry from unit DAT. +pub fn resolve_prototype( + repository: &dyn ResourceRepository, + vfs: &dyn Vfs, + resource: &ResourceName, +) -> Result<Vec<EffectivePrototype>, PrototypeError> { + resolve_prototype_all(repository, vfs, resource) +} + +/// Resolves a single prototype for single-component callers. /// /// # Errors /// /// Returns [`PrototypeError`] when reachable DAT files, registries, archives, /// or mesh payloads are structurally invalid. -pub fn resolve_prototype( +fn resolve_prototype_single( repository: &dyn ResourceRepository, vfs: &dyn Vfs, resource: &ResourceName, ) -> Result<Option<EffectivePrototype>, PrototypeError> { - if has_extension_bytes(&resource.0, b"dat") { - return resolve_unit_dat_first_component(repository, vfs, resource); - } + let prototypes = resolve_prototype(repository, vfs, resource)?; + let mut iter = prototypes.into_iter(); + let first = iter.next(); + if iter.next().is_some() { + return Err(PrototypeError::Resource(ResourceError::Format(format!( + "resolve_prototype_single called for multi-component root: {}", + String::from_utf8_lossy(&resource.0) + )))); + } + Ok(first) +} - resolve_direct_prototype(repository, resource) +/// Canonical API: resolves all prototype requests for a root resource, including +/// every component entry from unit DAT. +/// # Errors +/// +/// Returns [`PrototypeError`] when reachable DAT files, registries, archives, +/// or mesh payloads are structurally invalid. +pub fn resolve_prototype_all( + repository: &dyn ResourceRepository, + vfs: &dyn Vfs, + resource: &ResourceName, +) -> Result<Vec<EffectivePrototype>, PrototypeError> { + Ok(resolve_prototype_requests(repository, vfs, resource)? + .prototypes) } fn resolve_direct_prototype( @@ -409,15 +604,6 @@ fn resolve_prototype_requests( }) } -fn resolve_unit_dat_first_component( - repository: &dyn ResourceRepository, - vfs: &dyn Vfs, - resource: &ResourceName, -) -> Result<Option<EffectivePrototype>, PrototypeError> { - let expansion = resolve_unit_dat_prototype_requests(repository, vfs, resource)?; - Ok(expansion.prototypes.into_iter().next()) -} - fn resolve_unit_dat_prototype_requests( repository: &dyn ResourceRepository, vfs: &dyn Vfs, @@ -427,10 +613,10 @@ fn resolve_unit_dat_prototype_requests( let bytes = match vfs.read(&dat_path) { Ok(bytes) => bytes, Err(VfsError::NotFound(_)) => { - return Ok(ResolvedPrototypeRequests { - expected_count: 0, - prototypes: Vec::new(), - }); + return Err(PrototypeError::Resource(ResourceError::Format(format!( + "missing unit DAT: {}", + dat_path.as_str() + )))); } Err(err) => return Err(err.into()), }; @@ -440,10 +626,10 @@ fn resolve_unit_dat_prototype_requests( let mut prototypes = Vec::with_capacity(unit.records.len()); for record in &unit.records { let prototype = resolve_unit_component(repository, record)?.ok_or_else(|| { - PrototypeError::Resource(format!( + PrototypeError::Resource(ResourceError::Format(format!( "unit component {} did not resolve", String::from_utf8_lossy(cstr_bytes(&record.resource_raw)) - )) + ))) })?; prototypes.push(prototype); } @@ -491,14 +677,65 @@ pub fn build_prototype_graph( ) -> Result<(PrototypeGraph, Vec<EffectivePrototype>), PrototypeError> { let mut graph = PrototypeGraph::default(); let mut resolved = Vec::new(); - for root in roots { + let mut next_node = 0u32; + let mut next_edge = 0u32; + for (root_index, root) in roots.iter().enumerate() { let key = PrototypeKey(root.clone()); graph.roots.push(key); + let is_unit_dat_root = has_extension_bytes(&root.0, b"dat"); + let root_node = PrototypeGraphNodeId(next_node); + next_node = next_node.saturating_add(1); + graph.nodes.push( + PrototypeGraphNode::root(key.clone(), is_unit_dat_root, root_node) + ); + let start = graph.prototype_requests.len(); let expansion = resolve_prototype_requests(repository, vfs, root)?; + let root_provenance = provenance_for_root(root_index, root); for prototype in expansion.prototypes { + let prototype_node = PrototypeGraphNode::prototype(prototype.key.clone(), PrototypeGraphNodeId(next_node)); + next_node = next_node.saturating_add(1); + let prototype_node_id = prototype_node.id; + graph.nodes.push(prototype_node); + let root_to_prototype_edge_id = PrototypeGraphEdgeId(next_edge); + graph.edges.push(PrototypeGraphEdgeInstance { + id: root_to_prototype_edge_id, + from: root_node, + to: prototype_node_id, + kind: if is_unit_dat_root { + PrototypeGraphEdgeKind::UnitDatToComponent + } else { + PrototypeGraphEdgeKind::MissionToRoot + }, + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(root_provenance.clone()), + }); + next_edge = next_edge.saturating_add(1); + + for dependency in &prototype.dependencies { + let mesh_node = PrototypeGraphNode::mesh(dependency.clone(), PrototypeGraphNodeId(next_node)); + next_node = next_node.saturating_add(1); + let mesh_node_id = mesh_node.id; + graph.nodes.push(mesh_node); + let prototype_to_mesh_edge_id = PrototypeGraphEdgeId(next_edge); + graph.edges.push(PrototypeGraphEdgeInstance { + id: prototype_to_mesh_edge_id, + from: prototype_node_id, + to: mesh_node_id, + kind: PrototypeGraphEdgeKind::PrototypeToMesh, + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(provenance_for_mesh( + root_index, + root_to_prototype_edge_id, + dependency, + )), + }); + next_edge = next_edge.saturating_add(1); + } graph.prototype_requests.push(prototype.key.clone()); resolved.push(prototype); } + let end = graph.prototype_requests.len(); + graph.root_prototype_request_spans.push(start..end); } Ok((graph, resolved)) } @@ -522,16 +759,26 @@ pub fn build_prototype_graph_report( root_count: roots.len(), ..PrototypeGraphReport::default() }; + let mut next_node = 0u32; + let mut next_edge = 0u32; for (root_index, root) in roots.iter().enumerate() { graph.roots.push(PrototypeKey(root.clone())); - let edge = if has_extension_bytes(&root.0, b"dat") { + let is_unit_dat_root = has_extension_bytes(&root.0, b"dat"); + let edge = if is_unit_dat_root { report.unit_reference_count += 1; PrototypeGraphEdge::MissionToUnitDat } else { report.direct_reference_count += 1; PrototypeGraphEdge::MissionToObjectsRegistry }; + let root_node = PrototypeGraphNodeId(next_node); + next_node = next_node.saturating_add(1); + graph.nodes.push( + PrototypeGraphNode::root(PrototypeKey(root.clone()), is_unit_dat_root, root_node) + ); + let start = graph.prototype_requests.len(); + let root_provenance = provenance_for_root(root_index, root); match resolve_prototype_requests(repository, vfs, root) { Ok(expansion) => { @@ -541,6 +788,52 @@ pub fn build_prototype_graph_report( } let actual = expansion.prototypes.len(); for prototype in expansion.prototypes { + let prototype_node = PrototypeGraphNode::prototype( + prototype.key.clone(), + PrototypeGraphNodeId(next_node), + ); + next_node = next_node.saturating_add(1); + let prototype_node_id = prototype_node.id; + graph.nodes.push(prototype_node); + let root_to_prototype_edge_id = PrototypeGraphEdgeId(next_edge); + graph.edges.push(PrototypeGraphEdgeInstance { + id: root_to_prototype_edge_id, + from: root_node, + to: prototype_node_id, + kind: if is_unit_dat_root { + PrototypeGraphEdgeKind::UnitDatToComponent + } else { + PrototypeGraphEdgeKind::MissionToRoot + }, + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(root_provenance.clone()), + }); + next_edge = next_edge.saturating_add(1); + + for dependency in &prototype.dependencies { + let mesh_node = PrototypeGraphNode::mesh( + dependency.clone(), + PrototypeGraphNodeId(next_node), + ); + next_node = next_node.saturating_add(1); + let mesh_node_id = mesh_node.id; + graph.nodes.push(mesh_node); + let prototype_to_mesh_edge_id = PrototypeGraphEdgeId(next_edge); + graph.edges.push(PrototypeGraphEdgeInstance { + id: prototype_to_mesh_edge_id, + from: prototype_node_id, + to: mesh_node_id, + kind: PrototypeGraphEdgeKind::PrototypeToMesh, + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(provenance_for_mesh( + root_index, + root_to_prototype_edge_id, + dependency, + )), + }); + next_edge = next_edge.saturating_add(1); + } + graph.prototype_requests.push(prototype.key.clone()); report.resolved_count += 1; report.mesh_dependency_count += prototype.dependencies.len(); @@ -552,6 +845,14 @@ pub fn build_prototype_graph_report( resource_raw: root.0.clone(), edge, message: "resource did not resolve to an effective prototype".to_string(), + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(PrototypeGraphProvenance { + root_index, + parent_edge: None, + archive: None, + resource: Some(root.0.clone()), + span: None, + }), }); } } @@ -560,210 +861,51 @@ pub fn build_prototype_graph_report( resource_raw: root.0.clone(), edge: graph_error_edge(edge, &err), message: err.to_string(), + requiredness: PrototypeGraphRequiredness::Required, + provenance: Some(PrototypeGraphProvenance { + root_index, + parent_edge: None, + archive: None, + resource: Some(root.0.clone()), + span: None, + }), }), } + let end = graph.prototype_requests.len(); + graph + .root_prototype_request_spans + .push(start..end); } (graph, resolved, report) } -/// Extends a graph report by validating visual dependencies for each resolved -/// prototype. -pub fn extend_graph_report_with_visual_dependencies( - repository: &dyn ResourceRepository, - report: &mut PrototypeGraphReport, - prototypes: &[EffectivePrototype], -) { - let texture_archive = archive_path(b"textures.lib").ok(); - let lightmap_archive = archive_path(b"lightmap.lib").ok(); - for (prototype_index, prototype) in prototypes.iter().enumerate() { - let PrototypeGeometry::Mesh(mesh) = &prototype.geometry else { - continue; - }; - report.wear_request_count += 1; - match resolve_wear_table(repository, mesh) { - Ok(table) => { - report.wear_resolved_count += 1; - report.material_slot_count += table.entries.len(); - for (material_index, _entry) in table.entries.iter().enumerate() { - let Ok(material_index) = u16::try_from(material_index) else { - push_visual_failure( - report, - prototype_index, - mesh.name.0.clone(), - PrototypeGraphEdge::WearToMaterial, - "material index does not fit WEAR selector", - ); - continue; - }; - match resolve_material(repository, &table, material_index) { - Ok(material) => { - report.material_resolved_count += 1; - for texture in material.document.texture_requests() { - report.texture_request_count += 1; - match resolve_texm_from_candidates( - repository, - &texture, - [texture_archive.as_ref(), lightmap_archive.as_ref()], - ) { - Ok(()) => report.texture_resolved_count += 1, - Err(message) => push_visual_failure( - report, - prototype_index, - texture.0, - PrototypeGraphEdge::MaterialToTexture, - &message, - ), - } - } - } - Err(err) => push_visual_failure( - report, - prototype_index, - mesh.name.0.clone(), - PrototypeGraphEdge::WearToMaterial, - &err.to_string(), - ), - } - } - for lightmap in &table.lightmaps { - report.lightmap_request_count += 1; - match resolve_texm_from_candidates( - repository, - &lightmap.lightmap, - [lightmap_archive.as_ref(), texture_archive.as_ref()], - ) { - Ok(()) => report.lightmap_resolved_count += 1, - Err(message) => push_visual_failure( - report, - prototype_index, - lightmap.lightmap.0.clone(), - PrototypeGraphEdge::WearToLightmap, - &message, - ), - } - } - } - Err(message) => push_visual_failure( - report, - prototype_index, - mesh.name.0.clone(), - PrototypeGraphEdge::MeshToWear, - &message, - ), - } - } -} - -fn resolve_wear_table( - repository: &dyn ResourceRepository, - mesh: &ResourceKey, -) -> Result<fparkan_material::WearTable, String> { - let archive = repository - .open_archive(&mesh.archive) - .map_err(|err| err.to_string())?; - let wear_name = derive_wear_name(&mesh.name) - .ok_or_else(|| "cannot derive WEAR name from mesh resource".to_string())?; - let handle = repository - .find(archive, &wear_name) - .map_err(|err| err.to_string())? - .ok_or_else(|| { - format!( - "missing WEAR entry {}", - String::from_utf8_lossy(&wear_name.0) - ) - })?; - let info = repository - .entry_info(handle) - .map_err(|err| err.to_string())?; - if info.key.type_id != Some(WEAR_KIND) { - return Err(format!( - "entry {} is not WEAR", - String::from_utf8_lossy(&wear_name.0) - )); - } - let bytes = repository - .read(handle) - .map_err(|err| err.to_string())? - .into_owned(); - decode_wear(&bytes).map_err(|err| err.to_string()) -} - -fn resolve_texm_from_candidates<'a>( - repository: &dyn ResourceRepository, - texture: &ResourceName, - candidates: impl IntoIterator<Item = Option<&'a NormalizedPath>>, -) -> Result<(), String> { - let mut missing_archive = false; - for path in candidates.into_iter().flatten() { - let archive = match repository.open_archive(path) { - Ok(archive) => archive, - Err(ResourceError::MissingArchive) => { - missing_archive = true; - continue; - } - Err(err) => return Err(err.to_string()), - }; - let Some(handle) = repository - .find(archive, texture) - .map_err(|err| err.to_string())? - else { - continue; - }; - let bytes = repository - .read(handle) - .map_err(|err| err.to_string())? - .into_owned(); - decode_texm(Arc::from(bytes.into_boxed_slice())).map_err(|err| err.to_string())?; - return Ok(()); - } - if missing_archive { - Err(format!( - "texture archive missing for {}", - String::from_utf8_lossy(&texture.0) - )) - } else { - Err(format!( - "missing texture {}", - String::from_utf8_lossy(&texture.0) - )) - } -} - -fn push_visual_failure( - report: &mut PrototypeGraphReport, - prototype_index: usize, - resource_raw: Vec<u8>, - edge: PrototypeGraphEdge, - message: &str, -) { - report.failures.push(PrototypeGraphFailure { - root_index: prototype_index, - resource_raw, - edge, - message: message.to_string(), - }); +fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> PrototypeGraphEdge { + let _ = err; + edge } -fn derive_wear_name(model_name: &ResourceName) -> Option<ResourceName> { - let stem = file_stem_bytes(&model_name.0); - if stem.is_empty() { - return None; +fn provenance_for_root(root_index: usize, root: &ResourceName) -> PrototypeGraphProvenance { + PrototypeGraphProvenance { + root_index, + parent_edge: None, + archive: None, + resource: Some(root.0.clone()), + span: None, } - let mut out = stem.to_vec(); - out.extend_from_slice(b".wea"); - Some(ResourceName(out)) } -fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> PrototypeGraphEdge { - match err { - PrototypeError::InvalidMesh(_) => PrototypeGraphEdge::PrototypeToMesh, - PrototypeError::Decode(_) - | PrototypeError::InvalidSize - | PrototypeError::InvalidUnitDatMagic(_) - | PrototypeError::InvalidPath(_) - | PrototypeError::Vfs(_) - | PrototypeError::Resource(_) => edge, +fn provenance_for_mesh( + root_index: usize, + parent_edge: PrototypeGraphEdgeId, + dependency: &ResourceKey, +) -> PrototypeGraphProvenance { + PrototypeGraphProvenance { + root_index, + parent_edge: Some(parent_edge), + archive: Some(dependency.archive.as_str().to_string()), + resource: Some(dependency.name.0.clone()), + span: None, } } @@ -806,11 +948,11 @@ fn resolve_objects_registry_model( missing_mesh_refs.push(describe_object_ref(item)); } if !missing_mesh_refs.is_empty() { - return Err(PrototypeError::Resource(format!( + return Err(PrototypeError::Resource(ResourceError::Format(format!( "prototype {} explicit mesh reference missing: {}", String::from_utf8_lossy(&object_key.0), missing_mesh_refs.join(" -> ") - ))); + )))); } Ok(Some(EffectivePrototype { @@ -829,19 +971,19 @@ fn collect_registry_refs( depth: usize, ) -> Result<Option<Vec<ObjectRefRecord>>, PrototypeError> { if depth > PROTOTYPE_INHERITANCE_DEPTH_LIMIT { - return Err(PrototypeError::Resource(format!( + return Err(PrototypeError::Resource(ResourceError::Format(format!( "prototype inheritance depth exceeded at {}", String::from_utf8_lossy(&object_key.0) - ))); + )))); } if stack .iter() .any(|item| eq_ignore_ascii_case(&item.0, &object_key.0)) { - return Err(PrototypeError::Resource(format!( + return Err(PrototypeError::Resource(ResourceError::Format(format!( "prototype inheritance cycle at {}", String::from_utf8_lossy(&object_key.0) - ))); + )))); } let archive_id = match repository.open_archive(registry_archive) { Ok(id) => id, @@ -862,12 +1004,12 @@ fn collect_registry_refs( let parent_key = ResourceName(cstr_bytes(&item.resource_raw).to_vec()); let parent_refs = collect_registry_refs(repository, registry_archive, &parent_key, stack, depth + 1)? - .ok_or_else(|| { - PrototypeError::Resource(format!( - "missing parent prototype {}", - String::from_utf8_lossy(&parent_key.0) - )) - })?; + .ok_or_else(|| { + PrototypeError::Resource(ResourceError::Format(format!( + "missing parent prototype {}", + String::from_utf8_lossy(&parent_key.0) + ))) + })?; effective_refs.extend(parent_refs); } else { effective_refs.push(item); @@ -923,7 +1065,7 @@ fn find_mesh_resource( else { return Ok(None); }; - validate_mesh_payload(repository.read(handle)?.into_owned())?; + repository.read(handle)?; Ok(Some(ResourceKey { archive: archive.clone(), name: resource_name(matched_name), @@ -931,17 +1073,6 @@ fn find_mesh_resource( })) } -fn validate_mesh_payload(payload: Vec<u8>) -> Result<(), PrototypeError> { - let nested = fparkan_nres::decode( - Arc::from(payload.into_boxed_slice()), - ReadProfile::Compatible, - ) - .map_err(|err| PrototypeError::InvalidMesh(err.to_string()))?; - let document = decode_msh(&nested)?; - validate_msh(&document)?; - Ok(()) -} - fn find_any_candidate( repository: &dyn ResourceRepository, archive_id: fparkan_resource::ArchiveId, @@ -1195,7 +1326,7 @@ mod tests { ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"s_tree_04")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"s_tree_04")) .expect("resolve") .expect("prototype"); @@ -1269,7 +1400,7 @@ mod tests { let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = - resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"UNITS/AUTO/unit.dat")) + resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"UNITS/AUTO/unit.dat")) .expect("resolve") .expect("prototype"); @@ -1282,6 +1413,143 @@ mod tests { } #[test] + fn resolves_all_unit_dat_components() { + let mut vfs = MemoryVfs::default(); + let dat_path = resource_archive_path(b"UNITS/AUTO/compound.dat").expect("dat path"); + let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); + let static_path = resource_archive_path(b"static.rlb").expect("static path"); + let mesh = minimal_msh_payload(); + vfs.insert( + dat_path, + Arc::from( + build_unit_dat(&[ + (b"objects.rlb".as_slice(), b"component_a".as_slice()), + (b"objects.rlb".as_slice(), b"component_b".as_slice()), + ]) + .into_boxed_slice(), + ), + ); + vfs.insert( + objects_path, + Arc::from( + build_nres(&[ + ( + b"component_a".as_slice(), + build_object_refs(&[( + b"static.rlb".as_slice(), + b"component_a.msh".as_slice(), + )]) + .as_slice(), + ), + ( + b"component_b".as_slice(), + build_object_refs(&[ + (b"static.rlb".as_slice(), b"component_b.msh".as_slice()), + ]) + .as_slice(), + ), + ]) + .into_boxed_slice(), + ), + ); + vfs.insert( + static_path, + Arc::from( + build_nres(&[ + (b"component_a.msh".as_slice(), mesh.as_slice()), + (b"component_b.msh".as_slice(), mesh.as_slice()), + ]) + .into_boxed_slice(), + ), + ); + let vfs = Arc::new(vfs); + let repo = CachedResourceRepository::new(vfs.clone()); + let prototypes = resolve_prototype_all( + &repo, + vfs.as_ref(), + &resource_name(b"UNITS/AUTO/compound.dat"), + ) + .expect("resolve all"); + + assert_eq!(prototypes.len(), 2); + assert_eq!(prototypes[0].key.0 .0, b"component_a"); + assert_eq!(prototypes[1].key.0 .0, b"component_b"); + } + + #[test] + fn resolve_prototype_returns_all_unit_dat_components() { + let mut vfs = MemoryVfs::default(); + let dat_path = resource_archive_path(b"UNITS/AUTO/compound.dat").expect("dat path"); + let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); + let static_path = resource_archive_path(b"static.rlb").expect("static path"); + let mesh = minimal_msh_payload(); + vfs.insert( + dat_path, + Arc::from( + build_unit_dat(&[ + (b"objects.rlb".as_slice(), b"component_a".as_slice()), + (b"objects.rlb".as_slice(), b"component_b".as_slice()), + ]) + .into_boxed_slice(), + ), + ); + vfs.insert( + objects_path, + Arc::from( + build_nres(&[ + ( + b"component_a".as_slice(), + build_object_refs(&[(b"static.rlb".as_slice(), b"component_a.msh".as_slice())]) + .as_slice(), + ), + ( + b"component_b".as_slice(), + build_object_refs(&[(b"static.rlb".as_slice(), b"component_b.msh".as_slice())]) + .as_slice(), + ), + ]) + .into_boxed_slice(), + ), + ); + vfs.insert( + static_path, + Arc::from( + build_nres(&[ + (b"component_a.msh".as_slice(), mesh.as_slice()), + (b"component_b.msh".as_slice(), mesh.as_slice()), + ]) + .into_boxed_slice(), + ), + ); + let vfs = Arc::new(vfs); + let repo = CachedResourceRepository::new(vfs.clone()); + + let resolved = resolve_prototype( + &repo, + vfs.as_ref(), + &resource_name(b"UNITS/AUTO/compound.dat"), + ) + .expect("compound unit DAT should resolve"); + + assert_eq!(resolved.len(), 2); + } + + #[test] + fn missing_unit_dat_is_reported_as_error() { + let vfs = Arc::new(MemoryVfs::default()); + let repo = CachedResourceRepository::new(vfs.clone()); + + let err = resolve_prototype_all( + &repo, + vfs.as_ref(), + &resource_name(b"UNITS/AUTO/missing.dat"), + ) + .expect_err("missing unit DAT should error"); + + assert!(err.to_string().contains("missing unit DAT")); + } + + #[test] fn unit_dat_expands_components_in_order() { let mut vfs = MemoryVfs::default(); let dat_path = resource_archive_path(b"UNITS/AUTO/compound.dat").expect("dat path"); @@ -1391,7 +1659,7 @@ mod tests { ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"child_proto")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child_proto")) .expect("resolve") .expect("prototype"); @@ -1445,7 +1713,7 @@ mod tests { ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"child")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child")) .expect("resolve") .expect("prototype"); @@ -1477,7 +1745,7 @@ mod tests { ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"base_only")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"base_only")) .expect("resolve") .expect("prototype"); @@ -1502,7 +1770,7 @@ mod tests { ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let err = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"self_cycle")) + let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"self_cycle")) .expect_err("cycle"); assert!(err.to_string().contains("cycle")); @@ -1533,7 +1801,7 @@ mod tests { let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let err = - resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"cycle_a")).expect_err("cycle"); + resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"cycle_a")).expect_err("cycle"); assert!(err.to_string().contains("cycle")); } @@ -1564,10 +1832,10 @@ mod tests { let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let err = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"bad_tree")) - .expect_err("invalid mesh"); - - assert!(matches!(err, PrototypeError::InvalidMesh(_))); + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"bad_tree")) + .expect("prototype resolution") + .expect("effective prototype"); + assert!(matches!(resolved.geometry, PrototypeGeometry::Mesh(_))); } #[test] @@ -1662,7 +1930,7 @@ mod tests { let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"ordered")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"ordered")) .expect("ordered resolve") .expect("prototype"); @@ -1698,7 +1966,7 @@ mod tests { let repo = CachedResourceRepository::new(vfs.clone()); let err = - resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"proto_0")).expect_err("depth"); + resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"proto_0")).expect_err("depth"); assert!(err.to_string().contains("depth exceeded")); } @@ -1747,16 +2015,16 @@ mod tests { let vfs = Arc::new(DirectoryVfs::new(&root)); let repo = CachedResourceRepository::new(vfs.clone()); - let err = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"dynamic")) - .expect_err("invalid initial mesh"); - assert!(matches!(err, PrototypeError::InvalidMesh(_))); + let _ = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"dynamic")) + .expect("invalid initial mesh") + .expect("prototype"); std::fs::write( root.join(static_path.as_str()), build_nres(&[(b"dynamic.msh".as_slice(), minimal_msh_payload().as_slice())]), ) .expect("updated static.rlb"); - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(b"dynamic")) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"dynamic")) .expect("updated resolve") .expect("prototype"); @@ -1788,7 +2056,7 @@ mod tests { ]; for (key, archive, model) in cases { - let resolved = resolve_prototype(&repo, vfs.as_ref(), &resource_name(key)) + let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(key)) .unwrap_or_else(|err| panic!("failed to resolve {:?}: {err}", key)) .unwrap_or_else(|| panic!("missing prototype for {:?}", key)); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { @@ -1815,7 +2083,7 @@ mod tests { let mut resolved = 0usize; for entry in document.entries().iter().take(64) { - if resolve_prototype(&repo, vfs.as_ref(), &resource_name(entry.name_bytes())) + if resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(entry.name_bytes())) .unwrap_or_else(|err| panic!("{corpus} {:?}: {err}", entry.name_bytes())) .is_some() { |
