#![forbid(unsafe_code)] //! Prototype registry and unit DAT primitives. use encoding_rs::WINDOWS_1251; use fparkan_binary::{checked_allocation_len, checked_count_bytes, Cursor, DecodeError, Limits}; use fparkan_path::{normalize_relative, NormalizedPath, PathPolicy, ResourceName}; use fparkan_resource::{ archive_path, resource_name, ResourceError, ResourceKey, ResourceRepository, }; use fparkan_vfs::{Vfs, VfsError}; use std::sync::Arc; const MESH_KIND: u32 = 0x4853_454D; const UNIT_DAT_MIN_SIZE: usize = 0x48; const UNIT_DAT_MAGIC: u32 = 0x0000_F0F1; const PROTOTYPE_INHERITANCE_DEPTH_LIMIT: usize = 32; /// `objects.rlb` external geometry entry type (`EXTO`). pub const PROTOTYPE_TYPE_EXTO: u32 = u32::from_le_bytes(*b"EXTO"); /// Unit DAT second-header-word bit that enables native BAS placement handling. pub const UNIT_DAT_BAS_PLACEMENT_FLAG: u32 = 0x8000_0000; /// Prototype key. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct PrototypeKey(pub ResourceName); /// 64-byte object reference record. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ObjectRefRecord { /// Archive raw bytes. pub archive_raw: [u8; 32], /// Resource raw bytes. pub resource_raw: [u8; 32], } /// Unit DAT document. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnitDat { /// Opaque eight-byte header before component records. pub header_opaque: [u8; 8], /// Component records. pub records: Vec, } /// BAS payload with two ordered point-record sections. /// /// The `first` and `second` fields describe stream order only. The native /// loader reads one record shape in the first section and a point-only shape /// in the second, but this type does not assign gameplay or geometry meaning /// to either section. #[derive(Clone, Debug, PartialEq)] pub struct BasGeometry { /// First ordered section, whose records also carry two opaque word arrays. pub first: Vec, /// Second ordered section of point sequences. pub second: Vec, } /// BAS record from the first ordered section. #[derive(Clone, Debug, PartialEq)] pub struct BasGeometryRecord { /// Count value stored before the point sequence. pub declared_count: u32, /// `declared_count + 1` points copied by the native loader. /// /// Point sequences are preserved as read; closure is not imposed by the /// binary decoder. pub points: Vec<[f32; 3]>, /// First opaque word array, containing `declared_count` entries. pub opaque_words_a: Vec, /// Second opaque word array, containing `declared_count` entries. pub opaque_words_b: Vec, } /// BAS point sequence from the second ordered section. #[derive(Clone, Debug, PartialEq)] pub struct BasPointSequence { /// Count value stored before the point sequence. pub declared_count: u32, /// `declared_count + 1` points copied by the native loader. /// /// Point sequences are preserved as read; closure is not imposed by the /// binary decoder. pub points: Vec<[f32; 3]>, } /// Unit DAT binding used by mission object references. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnitDatBinding { /// Flags. pub flags: u32, /// Archive raw bytes. pub archive_raw: [u8; 32], /// Model key raw bytes. pub model_raw: [u8; 32], } /// Unit DAT component. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnitComponentRecord { /// Archive raw bytes. pub archive_raw: [u8; 32], /// Resource raw bytes. pub resource_raw: [u8; 32], /// Component kind. pub kind: u32, /// Parent or link. pub parent_or_link: i32, /// Description raw bytes. pub description_raw: [u8; 32], /// Opaque record word at offset `0x68`. pub tail0: u32, /// Number of immediate children in the preorder unit tree. pub immediate_child_count: u32, } /// One node in a decoded Unit DAT preorder tree. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnitComponentTreeNode { /// Original preorder record index. pub record_index: usize, /// Parent record index, or `None` for the root. pub parent_index: Option, /// Direct child record indices in source order. pub children: Vec, } /// Prototype geometry. #[derive(Clone, Debug, Eq, PartialEq)] pub enum PrototypeGeometry { /// Mesh resource. Mesh(ResourceKey), /// Valid non-geometric prototype. NonGeometric, } /// Effective prototype. #[derive(Clone, Debug, Eq, PartialEq)] pub struct EffectivePrototype { /// Key. pub key: PrototypeKey, /// Geometry. pub geometry: PrototypeGeometry, /// Resolution source. pub source: PrototypeSource, /// Source registry entry type, when the prototype came from an NRes /// object registry (for example `EXTO`, `INTO`, or `BTLU`). pub source_type: Option, /// Resource dependencies discovered while resolving this prototype. pub dependencies: Vec, /// Explicit control-data resource referenced by this prototype's resolved /// `objects.rlb` registry inheritance chain, when present. /// /// This is deliberately separate from visual dependencies: controller /// state belongs to each component instance and must not alter shared /// mesh/material cache identity. pub control_resource: Option, /// Explicit component point resource referenced by this prototype's /// resolved `objects.rlb` registry inheritance chain, when present. pub point_resource: Option, /// Explicit component NDP/provider resource referenced by this /// prototype's resolved `objects.rlb` registry inheritance chain. pub node_data_resource: Option, /// Explicit BAS geometry resource referenced by this prototype's resolved /// `objects.rlb` registry inheritance chain. pub bas_resource: Option, } /// Prototype resolution source. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PrototypeSource { /// Direct archive/key lookup. DirectArchive, /// `objects.rlb` registry lookup. ObjectsRegistry, /// Unit DAT binding. UnitDat, } /// Prototype graph. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct PrototypeGraph { /// Requested keys. pub roots: Vec, /// Effective prototype requests after unit DAT expansion. pub prototype_requests: Vec, /// Mission object-local spans of effective prototype requests. pub root_prototype_request_spans: Vec>, /// Ordered raw unit DAT component records for each mission root. /// /// This vector is aligned with [`Self::roots`]. Direct roots and legacy /// unit bindings have an empty record list. Records are preserved without /// assigning runtime semantics to their `kind`, links, descriptions, or /// opaque tails. pub root_unit_components: Vec>, /// Second word of each standard component Unit DAT header. This vector is /// aligned with [`Self::roots`]; direct prototype roots, legacy one-model /// bindings, and failed decodes are `None`. pub root_unit_dat_header_flags: Vec>, /// Whether visual dependency expansion has already been applied. pub visual_dependencies_expanded: bool, /// Materialized prototype dependency graph nodes. pub nodes: Vec, /// Materialized prototype dependency graph edges. pub edges: Vec, } /// 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, /// Ordered Unit DAT record selected for this path, when the root is a /// decoded multi-component unit. /// /// The index identifies source provenance only; it does not classify the /// component as physics, Control, animation, or any other subsystem. pub unit_component_index: Option, /// Source archive when available. pub archive: Option, /// Source resource key when available. pub resource: Option>, /// 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, /// WEAR dependency. WearResource, /// MAT0 dependency. MaterialResource, /// TEXM texture dependency. TextureResource, /// TEXM lightmap dependency. LightmapResource, /// 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, /// Optional resource represented by node. pub resource: Option, } impl PrototypeGraphNode { /// Creates a typed resource node. #[must_use] pub fn resource( kind: PrototypeGraphNodeKind, resource: ResourceKey, id: PrototypeGraphNodeId, ) -> Self { Self { id, kind, key: None, resource: Some(resource), } } /// 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, /// Mesh resource to WEAR table. MeshToWear, /// WEAR table to MAT0 material. WearToMaterial, /// MAT0 material to TEXM texture. MaterialToTexture, /// WEAR table to TEXM lightmap. WearToLightmap, } /// 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, } /// Mission prototype dependency graph report. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct PrototypeGraphReport { /// Requested mission roots. pub root_count: usize, /// Roots that point at unit DAT files. pub unit_reference_count: usize, /// Roots that point directly at prototype keys. pub direct_reference_count: usize, /// Component records reached from unit DAT files. pub unit_component_count: usize, /// Prototype requests that resolved to an effective prototype. pub resolved_count: usize, /// Mesh dependencies reached by resolved prototypes. pub mesh_dependency_count: usize, /// WEAR requests derived from reached mesh dependencies. pub wear_request_count: usize, /// WEAR entries successfully decoded. pub wear_resolved_count: usize, /// Material slots requested by decoded WEAR tables. pub material_slot_count: usize, /// MAT0 material entries successfully decoded. pub material_resolved_count: usize, /// Texture requests derived from MAT0 texture phases. pub texture_request_count: usize, /// Texm texture entries successfully decoded. pub texture_resolved_count: usize, /// Lightmap requests declared by decoded WEAR tables. pub lightmap_request_count: usize, /// Lightmap Texm entries successfully decoded. pub lightmap_resolved_count: usize, /// Graph failures tied to mission root edges. pub failures: Vec, } impl PrototypeGraphReport { /// Returns true when all reachable mission roots resolved. #[must_use] pub fn is_success(&self) -> bool { 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 } } /// Prototype graph failure tied to a root edge. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrototypeGraphFailure { /// Root index in the requested mission order. pub root_index: usize, /// Raw mission resource bytes. pub resource_raw: Vec, /// Edge that failed. pub edge: PrototypeGraphEdge, /// Failure detail. pub message: String, /// Requiredness that triggered this failure. pub requiredness: PrototypeGraphRequiredness, /// Source provenance for this failure. pub provenance: Option, } /// Prototype graph edge. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PrototypeGraphEdge { /// Mission object to unit DAT binding. MissionToUnitDat, /// Mission object to `objects.rlb` registry. MissionToObjectsRegistry, /// Unit DAT component to prototype key. UnitDatToComponent, /// Resolved prototype to mesh archive/resource. PrototypeToMesh, /// Mesh resource to matching WEAR table. MeshToWear, /// WEAR material slot to MAT0. WearToMaterial, /// MAT0 phase to Texm. MaterialToTexture, /// WEAR lightmap slot to lightmap Texm. WearToLightmap, } /// Prototype error. #[derive(Debug)] pub enum PrototypeError { /// Decode error. Decode(DecodeError), /// Invalid size. InvalidSize, /// Invalid unit DAT magic. InvalidUnitDatMagic(u32), /// Unit DAT child counts do not describe one complete preorder tree. InvalidUnitHierarchy(String), /// Invalid path. InvalidPath(String), /// VFS error. Vfs(VfsError), /// Resource repository error. Resource(ResourceError), } impl From for PrototypeError { fn from(value: DecodeError) -> Self { Self::Decode(value) } } impl From for PrototypeError { fn from(value: ResourceError) -> Self { Self::Resource(value) } } impl From for PrototypeError { fn from(value: VfsError) -> Self { Self::Vfs(value) } } impl std::fmt::Display for PrototypeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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::InvalidUnitHierarchy(message) => { write!(f, "invalid unit DAT preorder hierarchy: {message}") } 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 { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Decode(source) => Some(source), Self::InvalidSize | Self::InvalidUnitDatMagic(_) | Self::InvalidUnitHierarchy(_) | Self::InvalidPath(_) => None, Self::Vfs(source) => Some(source), Self::Resource(source) => Some(source), } } } /// Decodes an `objects.rlb` registry entry as 64-byte records. /// /// # Errors /// /// Returns [`PrototypeError::InvalidSize`] when the payload is not composed of /// whole 64-byte records. pub fn decode_registry_entry(payload: &[u8]) -> Result, PrototypeError> { if !payload.len().is_multiple_of(64) { return Err(PrototypeError::InvalidSize); } let mut out = Vec::with_capacity(payload.len() / 64); for chunk in payload.chunks_exact(64) { let mut archive_raw = [0; 32]; let mut resource_raw = [0; 32]; archive_raw.copy_from_slice(&chunk[..32]); resource_raw.copy_from_slice(&chunk[32..64]); out.push(ObjectRefRecord { archive_raw, resource_raw, }); } Ok(out) } /// Decodes unit DAT as an eight-byte header followed by `N * 112` bytes. /// /// # Errors /// /// Returns [`PrototypeError`] when the payload is too small or contains a /// partial component record. pub fn decode_unit_dat(payload: &[u8]) -> Result { if payload.len() < 8 { return Err(PrototypeError::InvalidSize); } let mut header_opaque = [0; 8]; header_opaque.copy_from_slice(&payload[..8]); let remaining = payload.len().saturating_sub(8) as u64; if !remaining.is_multiple_of(112) { return Err(PrototypeError::InvalidSize); } let record_count = remaining / 112; let bytes = checked_count_bytes(record_count, 112, remaining)?; if bytes as u64 != remaining { return Err(PrototypeError::InvalidSize); } let mut cursor = Cursor::new(&payload[8..]); let mut records = Vec::with_capacity( usize::try_from(record_count).map_err(|_| DecodeError::IntegerOverflow)?, ); for _ in 0..record_count { let mut archive_raw = [0; 32]; let mut resource_raw = [0; 32]; let mut description_raw = [0; 32]; archive_raw.copy_from_slice(cursor.read_exact(32)?); resource_raw.copy_from_slice(cursor.read_exact(32)?); let kind = cursor.read_u32_le()?; let parent_or_link = cursor.read_i32_le()?; description_raw.copy_from_slice(cursor.read_exact(32)?); let tail0 = cursor.read_u32_le()?; let immediate_child_count = cursor.read_u32_le()?; records.push(UnitComponentRecord { archive_raw, resource_raw, kind, parent_or_link, description_raw, tail0, immediate_child_count, }); } cursor.require_eof()?; Ok(UnitDat { header_opaque, records, }) } /// Decodes a BAS payload as two ordered sections of point records. /// /// Each section begins with a record count. Every record stores a declared /// count followed by that count plus one little-endian `Vec3` values. Records /// in the first section then carry two arrays of `declared_count` opaque /// words. The decoder preserves points and words without assigning geometric /// meaning or requiring the point sequence to be closed. /// /// # Errors /// /// Returns [`PrototypeError::Decode`] when a count exceeds the shared binary /// limits, a record is truncated, a point is not finite, or trailing bytes /// remain. pub fn decode_bas_geometry(payload: &[u8]) -> Result { let limits = Limits::default(); let payload_len = u64::try_from(payload.len()).map_err(|_| DecodeError::IntegerOverflow)?; if payload_len > limits.max_file_bytes { return Err(DecodeError::LimitExceeded { count: payload_len, limit: limits.max_file_bytes, } .into()); } let mut cursor = Cursor::new(payload); let mut budget = BasAllocationBudget::new(limits.max_decoded_bytes); let first_count = read_bas_count(&mut cursor, limits.max_entries)?; check_bas_section_minimum(&cursor, first_count)?; let first_capacity = budget.reserve::(u64::from(first_count), limits.max_entries)?; let mut first = Vec::with_capacity(first_capacity); for _ in 0..first_count { let declared_count = read_bas_count(&mut cursor, limits.max_array_items)?; let point_count = bas_point_count(declared_count, limits.max_array_items)?; check_bas_first_record(&cursor, declared_count, point_count)?; let points = read_bas_points(&mut cursor, point_count, &mut budget, limits)?; let opaque_words_a = read_bas_words(&mut cursor, declared_count, &mut budget, limits)?; let opaque_words_b = read_bas_words(&mut cursor, declared_count, &mut budget, limits)?; first.push(BasGeometryRecord { declared_count, points, opaque_words_a, opaque_words_b, }); } let second_count = read_bas_count(&mut cursor, limits.max_entries)?; check_bas_section_minimum(&cursor, second_count)?; let second_capacity = budget.reserve::(u64::from(second_count), limits.max_entries)?; let mut second = Vec::with_capacity(second_capacity); for _ in 0..second_count { let declared_count = read_bas_count(&mut cursor, limits.max_array_items)?; let point_count = bas_point_count(declared_count, limits.max_array_items)?; let remaining = u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?; checked_count_bytes(point_count, 12, remaining)?; let points = read_bas_points(&mut cursor, point_count, &mut budget, limits)?; second.push(BasPointSequence { declared_count, points, }); } cursor.require_eof()?; Ok(BasGeometry { first, second }) } fn read_bas_count(cursor: &mut Cursor<'_>, limit: u32) -> Result { let count = cursor.read_u32_le()?; if count > limit { return Err(DecodeError::LimitExceeded { count: u64::from(count), limit: u64::from(limit), }); } Ok(count) } fn bas_point_count(declared_count: u32, limit: u32) -> Result { let point_count = u64::from(declared_count) .checked_add(1) .ok_or(DecodeError::IntegerOverflow)?; if point_count > u64::from(limit) { return Err(DecodeError::LimitExceeded { count: point_count, limit: u64::from(limit), }); } Ok(point_count) } fn check_bas_section_minimum(cursor: &Cursor<'_>, record_count: u32) -> Result<(), DecodeError> { let remaining = u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?; // Each record has a count word and at least one point (count zero). checked_count_bytes(u64::from(record_count), 16, remaining)?; Ok(()) } fn check_bas_first_record( cursor: &Cursor<'_>, declared_count: u32, point_count: u64, ) -> Result<(), DecodeError> { let remaining = u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?; let point_bytes = checked_count_bytes(point_count, 12, remaining)?; let after_points = remaining .checked_sub(u64::try_from(point_bytes).map_err(|_| DecodeError::IntegerOverflow)?) .ok_or(DecodeError::IntegerOverflow)?; // The two unnamed word arrays each contain `declared_count` u32 entries. checked_count_bytes(u64::from(declared_count), 8, after_points)?; Ok(()) } fn read_bas_points( cursor: &mut Cursor<'_>, point_count: u64, budget: &mut BasAllocationBudget, limits: Limits, ) -> Result, DecodeError> { let capacity = budget.reserve::<[f32; 3]>(point_count, limits.max_array_items)?; let mut points = Vec::with_capacity(capacity); for _ in 0..point_count { let point = [ cursor.read_f32_le()?, cursor.read_f32_le()?, cursor.read_f32_le()?, ]; if !point.iter().all(|value| value.is_finite()) { return Err(DecodeError::Invalid("BAS point is not finite")); } points.push(point); } Ok(points) } fn read_bas_words( cursor: &mut Cursor<'_>, word_count: u32, budget: &mut BasAllocationBudget, limits: Limits, ) -> Result, DecodeError> { let capacity = budget.reserve::(u64::from(word_count), limits.max_array_items)?; let mut words = Vec::with_capacity(capacity); for _ in 0..word_count { words.push(cursor.read_u32_le()?); } Ok(words) } struct BasAllocationBudget { used: u64, limit: u64, } impl BasAllocationBudget { const fn new(limit: u64) -> Self { Self { used: 0, limit } } fn reserve(&mut self, count: u64, count_limit: u32) -> Result { let capacity = checked_allocation_len(count, u64::from(count_limit))?; let element_size = u64::try_from(std::mem::size_of::()).map_err(|_| DecodeError::IntegerOverflow)?; let allocation = count .checked_mul(element_size) .ok_or(DecodeError::IntegerOverflow)?; let total = self .used .checked_add(allocation) .ok_or(DecodeError::IntegerOverflow)?; if total > self.limit { return Err(DecodeError::LimitExceeded { count: total, limit: self.limit, }); } self.used = total; Ok(capacity) } } /// Builds the immediate-child tree encoded by Unit DAT record counts. /// /// Records are stored in preorder. The final word of each 112-byte record is /// the number of immediate children, so the tree can be reconstructed without /// treating `parent_or_link` as a parent index. That field is retained as the /// native mount-socket value for attachment consumers. /// /// # Errors /// /// Returns [`PrototypeError::InvalidUnitHierarchy`] when child counts consume /// too few or too many records. pub fn unit_component_tree( records: &[UnitComponentRecord], ) -> Result, PrototypeError> { if records.is_empty() { return Ok(Vec::new()); } let root_child_count = usize::try_from(records[0].immediate_child_count).map_err(|_| { PrototypeError::InvalidUnitHierarchy("record 0 child count does not fit usize".to_string()) })?; let mut nodes = Vec::with_capacity(records.len()); nodes.push(UnitComponentTreeNode { record_index: 0, parent_index: None, children: Vec::new(), }); // Each open frame holds a preorder node and the direct children still due. // This keeps hostile deep chains off the call stack. let mut open_parents = vec![(0_usize, root_child_count)]; let mut cursor = 1_usize; while cursor < records.len() { while open_parents .last() .is_some_and(|(_, remaining_children)| *remaining_children == 0) { open_parents.pop(); } let Some((parent_node_index, remaining_children)) = open_parents.last_mut() else { return Err(PrototypeError::InvalidUnitHierarchy(format!( "root tree consumed {cursor} records, but {} were encoded", records.len() ))); }; let parent_node_index = *parent_node_index; *remaining_children -= 1; let record_index = cursor; let record = &records[record_index]; let child_count = usize::try_from(record.immediate_child_count).map_err(|_| { PrototypeError::InvalidUnitHierarchy(format!( "record {record_index} child count does not fit usize" )) })?; let node_index = nodes.len(); nodes.push(UnitComponentTreeNode { record_index, parent_index: Some(nodes[parent_node_index].record_index), children: Vec::new(), }); nodes[parent_node_index].children.push(node_index); cursor += 1; open_parents.push((node_index, child_count)); } while open_parents .last() .is_some_and(|(_, remaining_children)| *remaining_children == 0) { open_parents.pop(); } if !open_parents.is_empty() { return Err(PrototypeError::InvalidUnitHierarchy(format!( "child count reached record {cursor}, but only {} records exist", records.len() ))); } Ok(nodes) } /// Decodes a mission unit DAT binding. /// /// # Errors /// /// Returns [`PrototypeError`] when the DAT file is too small, has the wrong /// magic, or does not contain both archive and model keys. pub fn decode_unit_dat_binding(payload: &[u8]) -> Result { if payload.len() < UNIT_DAT_MIN_SIZE { return Err(PrototypeError::InvalidSize); } let magic = u32::from_le_bytes( payload[0..4] .try_into() .map_err(|_| PrototypeError::InvalidSize)?, ); if magic != UNIT_DAT_MAGIC { return Err(PrototypeError::InvalidUnitDatMagic(magic)); } let flags = u32::from_le_bytes( payload[4..8] .try_into() .map_err(|_| PrototypeError::InvalidSize)?, ); let mut archive_raw = [0; 32]; let mut model_raw = [0; 32]; archive_raw.copy_from_slice(&payload[0x08..0x28]); model_raw.copy_from_slice(&payload[0x28..0x48]); if cstr_bytes(&archive_raw).is_empty() || cstr_bytes(&model_raw).is_empty() { return Err(PrototypeError::InvalidSize); } Ok(UnitDatBinding { flags, archive_raw, model_raw, }) } /// 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( repository: &dyn ResourceRepository, vfs: &dyn Vfs, resource: &ResourceName, ) -> Result, PrototypeError> { resolve_prototype_all(repository, vfs, 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, PrototypeError> { Ok(resolve_prototype_requests(repository, vfs, resource)?.prototypes) } #[cfg(test)] fn resolve_prototype_single( repository: &dyn ResourceRepository, vfs: &dyn Vfs, resource: &ResourceName, ) -> Result, PrototypeError> { 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) } fn resolve_direct_prototype( repository: &dyn ResourceRepository, resource: &ResourceName, ) -> Result, PrototypeError> { let objects = archive_path(b"objects.rlb").map_err(|err| PrototypeError::InvalidPath(err.to_string()))?; resolve_archive_model( repository, &objects, resource, PrototypeSource::ObjectsRegistry, ) } struct ResolvedPrototypeRequests { expected_count: usize, prototypes: Vec, unit_components: Vec, unit_dat_header_flags: Option, } fn resolve_prototype_requests( repository: &dyn ResourceRepository, vfs: &dyn Vfs, resource: &ResourceName, ) -> Result { if has_extension_bytes(&resource.0, b"dat") { return resolve_unit_dat_prototype_requests(repository, vfs, resource); } let prototype = resolve_direct_prototype(repository, resource)?; Ok(ResolvedPrototypeRequests { expected_count: 1, prototypes: prototype.into_iter().collect(), unit_components: Vec::new(), unit_dat_header_flags: None, }) } fn resolve_unit_dat_prototype_requests( repository: &dyn ResourceRepository, vfs: &dyn Vfs, resource: &ResourceName, ) -> Result { let dat_path = normalized_path_from_name(resource)?; let bytes = match vfs.read(&dat_path) { Ok(bytes) => bytes, Err(VfsError::NotFound(_)) => { return Err(PrototypeError::Resource(ResourceError::Format(format!( "missing unit DAT: {}", dat_path.as_str() )))); } Err(err) => return Err(err.into()), }; if let Ok(unit) = decode_unit_dat(&bytes) { 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(ResourceError::Format(format!( "unit component {} did not resolve", String::from_utf8_lossy(cstr_bytes(&record.resource_raw)) ))) })?; prototypes.push(prototype); } return Ok(ResolvedPrototypeRequests { expected_count: unit.records.len(), prototypes, unit_components: unit.records, unit_dat_header_flags: Some(u32::from_le_bytes( unit.header_opaque[4..8] .try_into() .expect("fixed Unit DAT header slice"), )), }); } let binding = decode_unit_dat_binding(&bytes)?; let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&binding.archive_raw).to_vec()))?; let model = ResourceName(cstr_bytes(&binding.model_raw).to_vec()); let prototype = resolve_archive_model(repository, &archive, &model, PrototypeSource::UnitDat)?; Ok(ResolvedPrototypeRequests { expected_count: 1, prototypes: prototype.into_iter().collect(), unit_components: Vec::new(), unit_dat_header_flags: None, }) } fn resolve_unit_component( repository: &dyn ResourceRepository, record: &UnitComponentRecord, ) -> Result, PrototypeError> { let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&record.archive_raw).to_vec()))?; let resource = ResourceName(cstr_bytes(&record.resource_raw).to_vec()); if resource.0.is_empty() { return Ok(None); } resolve_archive_model(repository, &archive, &resource, PrototypeSource::UnitDat) } /// Resolves many roots and records every resolved root in a graph. /// /// # Errors /// /// Returns [`PrototypeError`] when any reachable root fails with a structural /// error. pub fn build_prototype_graph( repository: &dyn ResourceRepository, vfs: &dyn Vfs, roots: &[ResourceName], ) -> Result<(PrototypeGraph, Vec), PrototypeError> { let mut graph = PrototypeGraph::default(); let mut resolved = Vec::new(); let mut next_node = 0u32; let mut next_edge = 0u32; for (root_index, root) in roots.iter().enumerate() { let key = PrototypeKey(root.clone()); 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, )); graph.roots.push(key); let start = graph.prototype_requests.len(); let expansion = resolve_prototype_requests(repository, vfs, root)?; graph .root_unit_components .push(expansion.unit_components.clone()); graph .root_unit_dat_header_flags .push(expansion.unit_dat_header_flags); let unit_component_count = expansion.unit_components.len(); for (prototype_index, prototype) in expansion.prototypes.into_iter().enumerate() { let unit_component_index = is_unit_dat_root .then_some(prototype_index) .filter(|index| *index < unit_component_count); let root_provenance = provenance_for_root(root_index, root, unit_component_index); 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, unit_component_index, 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)) } /// Resolves many mission roots and records edge-specific graph failures. /// /// This function reports per-root failures in [`PrototypeGraphReport`] instead /// of returning early. pub fn build_prototype_graph_report( repository: &dyn ResourceRepository, vfs: &dyn Vfs, roots: &[ResourceName], ) -> ( PrototypeGraph, Vec, PrototypeGraphReport, ) { let mut graph = PrototypeGraph::default(); let mut resolved = Vec::new(); let mut report = PrototypeGraphReport { 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 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(); match resolve_prototype_requests(repository, vfs, root) { Ok(expansion) => { let expected = expansion.expected_count; graph .root_unit_components .push(expansion.unit_components.clone()); graph .root_unit_dat_header_flags .push(expansion.unit_dat_header_flags); if edge == PrototypeGraphEdge::MissionToUnitDat { report.unit_component_count += expected; } let actual = expansion.prototypes.len(); let unit_component_count = expansion.unit_components.len(); for (prototype_index, prototype) in expansion.prototypes.into_iter().enumerate() { let unit_component_index = is_unit_dat_root .then_some(prototype_index) .filter(|index| *index < unit_component_count); let root_provenance = provenance_for_root(root_index, root, unit_component_index); 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, unit_component_index, 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(); resolved.push(prototype); } if actual < expected { report.failures.push(PrototypeGraphFailure { root_index, 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, unit_component_index: None, archive: None, resource: Some(root.0.clone()), span: None, }), }); } } Err(err) => report.failures.push(PrototypeGraphFailure { root_index, 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, unit_component_index: None, archive: None, resource: Some(root.0.clone()), span: None, }), }), } if graph.root_unit_components.len() <= root_index { graph.root_unit_components.push(Vec::new()); } if graph.root_unit_dat_header_flags.len() <= root_index { graph.root_unit_dat_header_flags.push(None); } let end = graph.prototype_requests.len(); graph.root_prototype_request_spans.push(start..end); } (graph, resolved, report) } fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> PrototypeGraphEdge { match err { PrototypeError::Decode(_) | PrototypeError::InvalidSize | PrototypeError::InvalidUnitDatMagic(_) if edge == PrototypeGraphEdge::MissionToUnitDat => { PrototypeGraphEdge::MissionToUnitDat } PrototypeError::Resource(ResourceError::Format(message)) if message.starts_with("missing unit DAT:") => { PrototypeGraphEdge::MissionToUnitDat } PrototypeError::Resource(ResourceError::Format(message)) if message.starts_with("unit component ") => { PrototypeGraphEdge::UnitDatToComponent } PrototypeError::Resource(ResourceError::Format(message)) if message.contains("explicit mesh reference missing:") => { PrototypeGraphEdge::PrototypeToMesh } _ => edge, } } fn provenance_for_root( root_index: usize, root: &ResourceName, unit_component_index: Option, ) -> PrototypeGraphProvenance { PrototypeGraphProvenance { root_index, parent_edge: None, unit_component_index, archive: None, resource: Some(root.0.clone()), span: None, } } fn provenance_for_mesh( root_index: usize, parent_edge: PrototypeGraphEdgeId, unit_component_index: Option, dependency: &ResourceKey, ) -> PrototypeGraphProvenance { PrototypeGraphProvenance { root_index, parent_edge: Some(parent_edge), unit_component_index, archive: Some(dependency.archive.as_str().to_string()), resource: Some(dependency.name.0.clone()), span: None, } } fn resolve_archive_model( repository: &dyn ResourceRepository, archive: &NormalizedPath, model_key: &ResourceName, source: PrototypeSource, ) -> Result, PrototypeError> { if archive.as_str().eq_ignore_ascii_case("objects.rlb") { if let Some(prototype) = resolve_objects_registry_model(repository, archive, model_key)? { return Ok(Some(prototype)); } } let Some(mesh) = find_mesh_resource(repository, archive, model_key)? else { return Ok(None); }; Ok(Some(effective( model_key.clone(), mesh, source, None, None, None, None, None, ))) } fn resolve_objects_registry_model( repository: &dyn ResourceRepository, registry_archive: &NormalizedPath, object_key: &ResourceName, ) -> Result, PrototypeError> { let source_type = match repository.open_archive(registry_archive) { Ok(archive_id) => { find_any_candidate(repository, archive_id, &mesh_name_candidates(&object_key.0))? .map(|(entry, _)| repository.entry_info(entry)) .transpose()? .and_then(|info| info.key.type_id) } Err(ResourceError::MissingArchive { .. }) => return Ok(None), Err(error) => return Err(error.into()), }; let Some(refs) = collect_registry_refs(repository, registry_archive, object_key, &mut Vec::new(), 0)? else { return Ok(None); }; let control_resource = resolve_explicit_control_resource(object_key, &refs)?; let point_resource = resolve_explicit_point_resource(object_key, &refs)?; let node_data_resource = resolve_explicit_node_data_resource(object_key, &refs)?; let bas_resource = resolve_explicit_bas_resource(object_key, &refs)?; let mut missing_mesh_refs = Vec::new(); for item in refs.iter().filter(|item| is_explicit_mesh_ref(item)) { if let Some(prototype) = resolve_object_ref_model( repository, object_key, item, cstr_bytes(&item.resource_raw), source_type, control_resource.clone(), point_resource.clone(), node_data_resource.clone(), bas_resource.clone(), )? { return Ok(Some(prototype)); } missing_mesh_refs.push(describe_object_ref(item)); } if !missing_mesh_refs.is_empty() { 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 { key: PrototypeKey(object_key.clone()), geometry: PrototypeGeometry::NonGeometric, source: PrototypeSource::ObjectsRegistry, source_type, dependencies: Vec::new(), control_resource, point_resource, node_data_resource, bas_resource, })) } fn collect_registry_refs( repository: &dyn ResourceRepository, registry_archive: &NormalizedPath, object_key: &ResourceName, stack: &mut Vec, depth: usize, ) -> Result>, PrototypeError> { if depth > PROTOTYPE_INHERITANCE_DEPTH_LIMIT { 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(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, Err(ResourceError::MissingArchive { .. }) => return Ok(None), Err(err) => return Err(err.into()), }; let Some((registry_entry, _matched_name)) = find_any_candidate(repository, archive_id, &mesh_name_candidates(&object_key.0))? else { return Ok(None); }; let payload = repository.read(registry_entry)?.into_owned(); let refs = decode_registry_entry(&payload)?; let mut effective_refs = Vec::new(); stack.push(object_key.clone()); for item in refs { if archive_name_is(&item.archive_raw, b"objects.rlb") { 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(ResourceError::Format(format!( "missing parent prototype {}", String::from_utf8_lossy(&parent_key.0) ))) })?; effective_refs.extend(parent_refs); } else { effective_refs.push(item); } } stack.pop(); Ok(Some(effective_refs)) } fn resolve_object_ref_model( repository: &dyn ResourceRepository, requested: &ResourceName, item: &ObjectRefRecord, model_name: &[u8], source_type: Option, control_resource: Option, point_resource: Option, node_data_resource: Option, bas_resource: Option, ) -> Result, PrototypeError> { let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?; let Some(mesh) = find_mesh_resource(repository, &archive, &ResourceName(model_name.to_vec()))? else { return Ok(None); }; Ok(Some(effective( requested.clone(), mesh, PrototypeSource::ObjectsRegistry, source_type, control_resource, point_resource, node_data_resource, bas_resource, ))) } fn is_explicit_mesh_ref(item: &ObjectRefRecord) -> bool { has_extension_bytes(cstr_bytes(&item.resource_raw), b"msh") } fn is_explicit_control_ref(item: &ObjectRefRecord) -> bool { has_extension_bytes(cstr_bytes(&item.resource_raw), b"ctl") } fn is_explicit_point_ref(item: &ObjectRefRecord) -> bool { has_extension_bytes(cstr_bytes(&item.resource_raw), b"cpt") } fn is_explicit_node_data_ref(item: &ObjectRefRecord) -> bool { has_extension_bytes(cstr_bytes(&item.resource_raw), b"ndp") } fn is_explicit_bas_ref(item: &ObjectRefRecord) -> bool { has_extension_bytes(cstr_bytes(&item.resource_raw), b"bas") } fn resolve_explicit_control_resource( requested: &ResourceName, refs: &[ObjectRefRecord], ) -> Result, PrototypeError> { resolve_explicit_resource(requested, refs, is_explicit_control_ref, "control") } fn resolve_explicit_point_resource( requested: &ResourceName, refs: &[ObjectRefRecord], ) -> Result, PrototypeError> { resolve_explicit_resource(requested, refs, is_explicit_point_ref, "point") } fn resolve_explicit_node_data_resource( requested: &ResourceName, refs: &[ObjectRefRecord], ) -> Result, PrototypeError> { resolve_explicit_resource(requested, refs, is_explicit_node_data_ref, "node data") } fn resolve_explicit_bas_resource( requested: &ResourceName, refs: &[ObjectRefRecord], ) -> Result, PrototypeError> { resolve_explicit_resource(requested, refs, is_explicit_bas_ref, "BAS geometry") } fn resolve_explicit_resource( requested: &ResourceName, refs: &[ObjectRefRecord], is_match: fn(&ObjectRefRecord) -> bool, label: &str, ) -> Result, PrototypeError> { let mut resolved = Vec::new(); for item in refs.iter().filter(|item| is_match(item)) { let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?; let name = resource_name(cstr_bytes(&item.resource_raw)); // Keep exact registry references; asset preparation decides how to // handle an absent optional sidecar and validates present payloads. let key = ResourceKey { archive, name, type_id: None, }; if !resolved .iter() .any(|existing| same_resource_key(existing, &key)) { resolved.push(key); } } match resolved.len() { 0 => Ok(None), 1 => Ok(resolved.pop()), _ => Err(PrototypeError::Resource(ResourceError::Format(format!( "prototype {} has ambiguous explicit {label} resources: {}", String::from_utf8_lossy(&requested.0), resolved .iter() .map(|key| format!( "{}:{}", key.archive.as_str(), String::from_utf8_lossy(&key.name.0) )) .collect::>() .join(", "), )))), } } fn same_resource_key(left: &ResourceKey, right: &ResourceKey) -> bool { left.archive .as_str() .eq_ignore_ascii_case(right.archive.as_str()) && eq_ignore_ascii_case(&left.name.0, &right.name.0) && left.type_id == right.type_id } fn describe_object_ref(item: &ObjectRefRecord) -> String { format!( "{}:{}", String::from_utf8_lossy(cstr_bytes(&item.archive_raw)), String::from_utf8_lossy(cstr_bytes(&item.resource_raw)) ) } fn find_mesh_resource( repository: &dyn ResourceRepository, archive: &NormalizedPath, model_key: &ResourceName, ) -> Result, PrototypeError> { let archive_id = match repository.open_archive(archive) { Ok(id) => id, Err(ResourceError::MissingArchive { .. }) => return Ok(None), Err(err) => return Err(err.into()), }; let candidates = mesh_name_candidates(&model_key.0); let Some((handle, matched_name)) = find_any_candidate(repository, archive_id, &candidates)? else { return Ok(None); }; repository.read(handle)?; Ok(Some(ResourceKey { archive: archive.clone(), name: resource_name(matched_name), type_id: Some(MESH_KIND), })) } fn find_any_candidate( repository: &dyn ResourceRepository, archive_id: fparkan_resource::ArchiveId, candidates: &[Vec], ) -> Result)>, PrototypeError> { for candidate in candidates { if let Some(handle) = repository.find(archive_id, &resource_name(candidate))? { return Ok(Some((handle, candidate.clone()))); } } Ok(None) } fn effective( requested: ResourceName, mesh: ResourceKey, source: PrototypeSource, source_type: Option, control_resource: Option, point_resource: Option, node_data_resource: Option, bas_resource: Option, ) -> EffectivePrototype { EffectivePrototype { key: PrototypeKey(requested), geometry: PrototypeGeometry::Mesh(mesh.clone()), source, source_type, dependencies: vec![mesh], control_resource, point_resource, node_data_resource, bas_resource, } } fn mesh_name_candidates(name: &[u8]) -> Vec> { let trimmed = trim_ascii(name); if trimmed.is_empty() { return Vec::new(); } let mut out = Vec::new(); push_unique_bytes(&mut out, trimmed.to_vec()); if has_extension_bytes(trimmed, b"msh") { let stem = file_stem_bytes(trimmed); if !stem.is_empty() { push_unique_bytes(&mut out, stem.to_vec()); } } else { let mut with_suffix = trimmed.to_vec(); with_suffix.extend_from_slice(b".msh"); push_unique_bytes(&mut out, with_suffix); } out } fn push_unique_bytes(items: &mut Vec>, value: Vec) { if !items.iter().any(|item| eq_ignore_ascii_case(item, &value)) { items.push(value); } } fn normalized_path_from_name(name: &ResourceName) -> Result { let text = legacy_path_text(cstr_bytes(&name.0)); normalize_relative(text.as_bytes(), PathPolicy::StrictLegacy) .map_err(|err| PrototypeError::InvalidPath(err.to_string())) } fn legacy_path_text(raw: &[u8]) -> String { if let Ok(text) = std::str::from_utf8(raw) { text.to_string() } else { let (decoded, _, _) = WINDOWS_1251.decode(raw); decoded.into_owned() } } fn cstr_bytes(raw: &[u8]) -> &[u8] { let len = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len()); trim_ascii(&raw[..len]) } fn archive_name_is(raw: &[u8], expected: &[u8]) -> bool { cstr_bytes(raw).eq_ignore_ascii_case(expected) } fn trim_ascii(bytes: &[u8]) -> &[u8] { let mut start = 0usize; let mut end = bytes.len(); while start < end && bytes[start].is_ascii_whitespace() { start += 1; } while end > start && bytes[end - 1].is_ascii_whitespace() { end -= 1; } &bytes[start..end] } fn has_extension_bytes(name: &[u8], ext: &[u8]) -> bool { let Some(pos) = name.iter().rposition(|byte| *byte == b'.') else { return false; }; eq_ignore_ascii_case(&name[pos + 1..], ext) } fn file_stem_bytes(name: &[u8]) -> &[u8] { let file_name = name .iter() .rposition(|byte| *byte == b'/' || *byte == b'\\') .map_or(name, |pos| &name[pos + 1..]); let Some(dot) = file_name.iter().rposition(|byte| *byte == b'.') else { return file_name; }; &file_name[..dot] } fn eq_ignore_ascii_case(left: &[u8], right: &[u8]) -> bool { left.eq_ignore_ascii_case(right) } /// Decodes FX/prototype bytes by preserving them for future typed support. #[must_use] pub fn preserve_payload(payload: &[u8]) -> Arc<[u8]> { Arc::from(payload.to_vec().into_boxed_slice()) } #[cfg(test)] mod tests { use super::*; use fparkan_resource::{archive_path as resource_archive_path, CachedResourceRepository}; use fparkan_vfs::{DirectoryVfs, MemoryVfs}; use std::path::Path; #[test] fn registry_requires_record_multiple() { assert!(decode_registry_entry(&[0; 63]).is_err()); assert_eq!(decode_registry_entry(&[0; 64]).expect("record").len(), 1); } #[test] fn registry_zero_records_payload_is_empty() { let records = decode_registry_entry(&[]).expect("empty registry"); assert!(records.is_empty()); } #[test] fn registry_preserves_bounded_name_tails_and_order() { let mut bytes = Vec::new(); let mut first = [0u8; 64]; first[..9].copy_from_slice(b"arch\0tail"); first[32..40].copy_from_slice(b"res\0tail"); bytes.extend_from_slice(&first); let mut second = [0u8; 64]; second[..10].copy_from_slice(b"other.rlb\0"); second[32..43].copy_from_slice(b"second.msh\0"); bytes.extend_from_slice(&second); let records = decode_registry_entry(&bytes).expect("registry records"); assert_eq!(records.len(), 2); assert_eq!(&records[0].archive_raw[..9], b"arch\0tail"); assert_eq!(&records[0].resource_raw[..8], b"res\0tail"); assert_eq!(cstr_bytes(&records[0].archive_raw), b"arch"); assert_eq!(cstr_bytes(&records[1].resource_raw), b"second.msh"); } #[test] fn bas_geometry_preserves_both_sections_and_open_point_sequences() { let bytes = build_bas_geometry_payload(); let geometry = decode_bas_geometry(&bytes).expect("BAS geometry"); assert_eq!(geometry.first.len(), 2); assert_eq!(geometry.first[0].declared_count, 1); assert_eq!( geometry.first[0].points, vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] ); assert_eq!(geometry.first[0].opaque_words_a, vec![0x1122_3344]); assert_eq!(geometry.first[0].opaque_words_b, vec![0x5566_7788]); assert_eq!(geometry.first[1].declared_count, 2); assert_eq!( geometry.first[1].points, vec![[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]] ); assert_eq!(geometry.first[1].opaque_words_a, vec![7, 8]); assert_eq!(geometry.first[1].opaque_words_b, vec![9, 10]); assert_eq!(geometry.second.len(), 1); assert_eq!(geometry.second[0].declared_count, 1); assert_eq!( geometry.second[0].points, vec![[20.0, 21.0, 22.0], [23.0, 24.0, 25.0]] ); } #[test] fn bas_geometry_rejects_truncated_records_and_extreme_counts() { let mut truncated = build_bas_geometry_payload(); truncated.pop(); assert!(matches!( decode_bas_geometry(&truncated), Err(PrototypeError::Decode(DecodeError::UnexpectedEof { .. })) )); assert!(matches!( decode_bas_geometry(&u32::MAX.to_le_bytes()), Err(PrototypeError::Decode(DecodeError::LimitExceeded { .. })) )); } #[test] fn bas_geometry_rejects_non_finite_points_and_trailing_bytes() { let mut non_finite = build_bas_geometry_payload(); // The first point follows the first section and first record counts. non_finite[8..12].copy_from_slice(&f32::NAN.to_bits().to_le_bytes()); assert!(matches!( decode_bas_geometry(&non_finite), Err(PrototypeError::Decode(DecodeError::Invalid( "BAS point is not finite" ))) )); let mut trailing = build_bas_geometry_payload(); trailing.push(0); assert!(matches!( decode_bas_geometry(&trailing), Err(PrototypeError::Decode(DecodeError::TrailingBytes { .. })) )); } #[test] fn unit_zero_records_uses_exact_size() { let bytes = [0_u8; 8]; let unit = decode_unit_dat(&bytes).expect("unit"); assert!(unit.records.is_empty()); } #[test] fn unit_dat_one_record_uses_exact_size_formula() { let bytes = build_unit_dat(&[(b"objects.rlb".as_slice(), b"component".as_slice())]); let unit = decode_unit_dat(&bytes).expect("unit"); assert_eq!(bytes.len(), 8 + 112); assert_eq!(unit.records.len(), 1); assert_eq!(cstr_bytes(&unit.records[0].archive_raw), b"objects.rlb"); assert_eq!(cstr_bytes(&unit.records[0].resource_raw), b"component"); } #[test] fn unit_dat_rejects_truncated_record() { let mut bytes = build_unit_dat(&[(b"objects.rlb".as_slice(), b"component".as_slice())]); bytes.pop(); assert!(matches!( decode_unit_dat(&bytes), Err(PrototypeError::InvalidSize) )); } #[test] fn unit_dat_preserves_header_description_tail_and_parent_link() { let mut bytes = build_unit_dat(&[(b"objects.rlb".as_slice(), b"component".as_slice())]); bytes[0..8].copy_from_slice(&[0xF1, 0xF0, 1, 2, 3, 4, 5, 6]); bytes[8 + 68..8 + 72].copy_from_slice(&(-7_i32).to_le_bytes()); let description = b"desc\0tail"; bytes[8 + 72..8 + 72 + description.len()].copy_from_slice(description); bytes[8 + 104..8 + 108].copy_from_slice(&0x1122_3344_u32.to_le_bytes()); bytes[8 + 108..8 + 112].copy_from_slice(&0x5566_7788_u32.to_le_bytes()); let unit = decode_unit_dat(&bytes).expect("unit"); let record = &unit.records[0]; assert_eq!(unit.header_opaque, [0xF1, 0xF0, 1, 2, 3, 4, 5, 6]); assert_eq!( u32::from_le_bytes(unit.header_opaque[4..8].try_into().unwrap()), 0x0605_0403 ); assert_eq!(record.parent_or_link, -7); assert_eq!(&record.description_raw[..description.len()], description); assert_eq!(record.tail0, 0x1122_3344); assert_eq!(record.immediate_child_count, 0x5566_7788); } #[test] fn unit_dat_accepts_full_description_without_nul() { let mut bytes = build_unit_dat(&[(b"objects.rlb".as_slice(), b"component".as_slice())]); bytes[8 + 72..8 + 104].copy_from_slice(b"12345678901234567890123456789012"); let unit = decode_unit_dat(&bytes).expect("unit"); assert_eq!( &unit.records[0].description_raw, b"12345678901234567890123456789012" ); } #[test] fn unit_dat_preserves_positive_parent_link() { let mut bytes = build_unit_dat(&[(b"objects.rlb".as_slice(), b"component".as_slice())]); bytes[8 + 68..8 + 72].copy_from_slice(&12_i32.to_le_bytes()); let unit = decode_unit_dat(&bytes).expect("unit"); assert_eq!(unit.records[0].parent_or_link, 12); } #[test] fn unit_component_tree_follows_preorder_child_counts_and_keeps_mount_links() { let mut bytes = build_unit_dat(&[ (b"objects.rlb".as_slice(), b"root".as_slice()), (b"objects.rlb".as_slice(), b"child".as_slice()), (b"objects.rlb".as_slice(), b"grandchild".as_slice()), ]); bytes[8 + 68..8 + 72].copy_from_slice(&(-1_i32).to_le_bytes()); bytes[8 + 108..8 + 112].copy_from_slice(&1_u32.to_le_bytes()); bytes[8 + 112 + 68..8 + 112 + 72].copy_from_slice(&7_i32.to_le_bytes()); bytes[8 + 112 + 108..8 + 112 + 112].copy_from_slice(&1_u32.to_le_bytes()); bytes[8 + 224 + 68..8 + 224 + 72].copy_from_slice(&3_i32.to_le_bytes()); let unit = decode_unit_dat(&bytes).expect("unit"); let tree = unit_component_tree(&unit.records).expect("preorder tree"); assert_eq!(tree.len(), 3); assert_eq!(tree[0].record_index, 0); assert_eq!(tree[0].parent_index, None); assert_eq!(tree[0].children, vec![1]); assert_eq!(tree[1].parent_index, Some(0)); assert_eq!(tree[1].children, vec![2]); assert_eq!(tree[2].parent_index, Some(1)); assert_eq!(tree[2].children, Vec::::new()); assert_eq!(unit.records[1].parent_or_link, 7); assert_eq!(unit.records[2].parent_or_link, 3); } #[test] fn unit_component_tree_handles_deep_preorder_chains_iteratively() { const DEPTH: usize = 32_768; let records: Vec<_> = (0..DEPTH) .map(|index| UnitComponentRecord { archive_raw: [0; 32], resource_raw: [0; 32], kind: 0, parent_or_link: 0, description_raw: [0; 32], tail0: 0, immediate_child_count: u32::from(index + 1 < DEPTH), }) .collect(); let tree = unit_component_tree(&records).expect("deep preorder chain"); assert_eq!(tree.len(), DEPTH); assert_eq!(tree[0].parent_index, None); assert_eq!(tree[0].children, vec![1]); assert_eq!(tree[DEPTH / 2].parent_index, Some(DEPTH / 2 - 1)); assert_eq!(tree[DEPTH - 2].children, vec![DEPTH - 1]); assert_eq!(tree[DEPTH - 1].parent_index, Some(DEPTH - 2)); assert!(tree[DEPTH - 1].children.is_empty()); } #[test] fn unit_component_tree_rejects_missing_child_records() { let bytes = build_unit_dat(&[ (b"objects.rlb".as_slice(), b"root".as_slice()), (b"objects.rlb".as_slice(), b"child".as_slice()), ]); let mut unit = decode_unit_dat(&bytes).expect("unit"); unit.records[0].immediate_child_count = 2; assert!(matches!( unit_component_tree(&unit.records), Err(PrototypeError::InvalidUnitHierarchy(message)) if message.contains("child count reached record 2") )); } #[test] fn unit_component_tree_rejects_unconsumed_preorder_records() { let bytes = build_unit_dat(&[ (b"objects.rlb".as_slice(), b"root".as_slice()), (b"objects.rlb".as_slice(), b"child".as_slice()), ]); let unit = decode_unit_dat(&bytes).expect("unit"); assert!(matches!( unit_component_tree(&unit.records), Err(PrototypeError::InvalidUnitHierarchy(_)) )); } #[test] fn resolves_synthetic_objects_registry_model() { let mut vfs = MemoryVfs::default(); 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( objects_path, Arc::from( build_nres(&[( b"s_tree_04".as_slice(), build_object_refs(&[(b"static.rlb".as_slice(), b"s_tree_0_04.msh".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"s_tree_0_04.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"s_tree_04")) .expect("resolve") .expect("prototype"); assert_eq!(resolved.source, PrototypeSource::ObjectsRegistry); assert!(resolved.control_resource.is_none()); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected mesh"); }; assert_eq!(mesh.archive.as_str(), "static.rlb"); assert!(mesh.name.0.eq_ignore_ascii_case(b"s_tree_0_04.msh")); } #[test] fn graph_report_records_resolved_roots_and_failures() { let mut vfs = MemoryVfs::default(); 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( objects_path, Arc::from( build_nres(&[( b"s_tree_04".as_slice(), build_object_refs(&[(b"static.rlb".as_slice(), b"s_tree_0_04.msh".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"s_tree_0_04.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let roots = [resource_name(b"s_tree_04"), resource_name(b"missing_key")]; let (graph, resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &roots); assert_eq!(graph.roots.len(), 2); assert_eq!(graph.root_unit_dat_header_flags, vec![None, None]); assert_eq!(resolved.len(), 1); assert_eq!(report.root_count, 2); assert_eq!(report.direct_reference_count, 2); assert_eq!(report.unit_reference_count, 0); assert_eq!(report.resolved_count, 1); assert_eq!(report.failures.len(), 1); assert_eq!(report.failures[0].root_index, 1); assert_eq!( report.failures[0].edge, PrototypeGraphEdge::MissionToObjectsRegistry ); assert!(!report.is_success()); } #[test] fn resolves_synthetic_unit_dat_binding() { let mut vfs = MemoryVfs::default(); let dat_path = resource_archive_path(b"UNITS/AUTO/unit.dat").expect("dat path"); let archive_path = resource_archive_path(b"units.rlb").expect("archive path"); let mesh = minimal_msh_payload(); vfs.insert( dat_path, Arc::from(build_unit_dat_binding(b"units.rlb", b"unit_model").into_boxed_slice()), ); vfs.insert( archive_path, Arc::from( build_nres(&[(b"unit_model.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let (graph, _, report) = build_prototype_graph_report( &repo, vfs.as_ref(), &[resource_name(b"UNITS/AUTO/unit.dat")], ); assert!(report.is_success()); assert_eq!(graph.root_unit_dat_header_flags, vec![None]); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"UNITS/AUTO/unit.dat")) .expect("resolve") .expect("prototype"); assert_eq!(resolved.source, PrototypeSource::UnitDat); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected mesh"); }; assert_eq!(mesh.archive.as_str(), "units.rlb"); assert!(mesh.name.0.eq_ignore_ascii_case(b"unit_model.msh")); } #[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(); let mut unit_dat = build_unit_dat(&[ (b"objects.rlb".as_slice(), b"component_a".as_slice()), (b"objects.rlb".as_slice(), b"component_b".as_slice()), ]); unit_dat[4..8].copy_from_slice(&UNIT_DAT_BAS_PLACEMENT_FLAG.to_le_bytes()); vfs.insert(dat_path, Arc::from(unit_dat.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 empty_unit_dat_resolves_as_zero_component_root() { let mut vfs = MemoryVfs::default(); let dat_path = resource_archive_path(b"UNITS/AUTO/empty.dat").expect("dat path"); vfs.insert(dat_path, Arc::from(vec![0_u8; 8].into_boxed_slice())); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let roots = [resource_name(b"UNITS/AUTO/empty.dat")]; let (graph, resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &roots); assert_eq!(graph.roots.len(), 1); assert!(graph.prototype_requests.is_empty()); assert!(resolved.is_empty()); assert_eq!(report.unit_reference_count, 1); assert_eq!(report.unit_component_count, 0); assert_eq!(report.resolved_count, 0); assert!(report.failures.is_empty()); assert!(report.is_success()); } #[test] fn malformed_unit_dat_failure_is_classified_on_unit_edge() { let mut vfs = MemoryVfs::default(); let dat_path = resource_archive_path(b"UNITS/AUTO/bad.dat").expect("dat path"); vfs.insert( dat_path, Arc::from([1_u8, 2, 3].to_vec().into_boxed_slice()), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let (_graph, _resolved, report) = build_prototype_graph_report( &repo, vfs.as_ref(), &[resource_name(b"UNITS/AUTO/bad.dat")], ); assert_eq!(report.failures.len(), 1); assert_eq!( report.failures[0].edge, PrototypeGraphEdge::MissionToUnitDat ); } #[test] fn missing_unit_component_failure_is_classified_on_component_edge() { 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"); vfs.insert( dat_path, Arc::from( build_unit_dat(&[(b"objects.rlb".as_slice(), b"missing_component".as_slice())]) .into_boxed_slice(), ), ); vfs.insert(objects_path, Arc::from(build_nres(&[]).into_boxed_slice())); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let (_graph, _resolved, report) = build_prototype_graph_report( &repo, vfs.as_ref(), &[resource_name(b"UNITS/AUTO/compound.dat")], ); assert_eq!(report.failures.len(), 1); assert_eq!( report.failures[0].edge, PrototypeGraphEdge::UnitDatToComponent ); assert!(report.failures[0].message.contains("missing_component")); } #[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"); 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(); let mut unit_dat = build_unit_dat(&[ (b"objects.rlb".as_slice(), b"component_a".as_slice()), (b"objects.rlb".as_slice(), b"component_b".as_slice()), ]); unit_dat[4..8].copy_from_slice(&UNIT_DAT_BAS_PLACEMENT_FLAG.to_le_bytes()); vfs.insert(dat_path, Arc::from(unit_dat.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 roots = [resource_name(b"UNITS/AUTO/compound.dat")]; let (graph, resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &roots); assert_eq!(graph.roots.len(), 1); assert_eq!(graph.prototype_requests.len(), 2); assert_eq!(graph.prototype_requests[0].0 .0, b"component_a"); assert_eq!(graph.prototype_requests[1].0 .0, b"component_b"); assert_eq!(graph.root_unit_components.len(), 1); assert_eq!(graph.root_unit_components[0].len(), 2); assert_eq!( graph.root_unit_dat_header_flags, vec![Some(UNIT_DAT_BAS_PLACEMENT_FLAG)] ); assert_eq!( cstr_bytes(&graph.root_unit_components[0][0].resource_raw), b"component_a" ); assert_eq!( cstr_bytes(&graph.root_unit_components[0][1].resource_raw), b"component_b" ); let component_edges: Vec<_> = graph .edges .iter() .filter(|edge| edge.kind == PrototypeGraphEdgeKind::UnitDatToComponent) .collect(); assert_eq!(component_edges.len(), 2); assert_eq!( component_edges[0] .provenance .as_ref() .and_then(|provenance| provenance.unit_component_index), Some(0) ); assert_eq!( component_edges[1] .provenance .as_ref() .and_then(|provenance| provenance.unit_component_index), Some(1) ); assert!(graph .edges .iter() .filter(|edge| edge.kind == PrototypeGraphEdgeKind::PrototypeToMesh) .all(|edge| edge .provenance .as_ref() .and_then(|value| value.unit_component_index) .is_some())); assert_eq!(resolved.len(), 2); assert_eq!(report.unit_reference_count, 1); assert_eq!(report.unit_component_count, 2); assert_eq!(report.resolved_count, 2); assert!(report.is_success()); } #[test] fn objects_registry_inheritance_merges_parent_then_local_refs() { let mut vfs = MemoryVfs::default(); 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 fortif_path = resource_archive_path(b"fortif.rlb").expect("fortif path"); let mesh = minimal_msh_payload(); vfs.insert( objects_path, Arc::from( build_nres(&[ ( b"parent_proto".as_slice(), build_object_refs(&[ (b"static.rlb".as_slice(), b"parent_proto.msh".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.ndp".as_slice()), ]) .as_slice(), ), ( b"child_proto".as_slice(), build_object_refs(&[ (b"objects.rlb".as_slice(), b"parent_proto".as_slice()), (b"fortif.rlb".as_slice(), b"child_proto.bas".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()), (b"fortif.rlb".as_slice(), b"parent_proto.ndp".as_slice()), ]) .as_slice(), ), ]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"parent_proto.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); vfs.insert( fortif_path, Arc::from( build_nres(&[ (b"child_proto.bas".as_slice(), b"base"), (b"parent_proto.ctl".as_slice(), b"control"), (b"parent_proto.cpt".as_slice(), b"points"), (b"parent_proto.ndp".as_slice(), b"node data"), ]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child_proto")) .expect("resolve") .expect("prototype"); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected inherited mesh"); }; assert_eq!(mesh.archive.as_str(), "static.rlb"); assert!(mesh.name.0.eq_ignore_ascii_case(b"parent_proto.msh")); let control = resolved.control_resource.expect("inherited explicit CTL"); assert_eq!(control.archive.as_str(), "fortif.rlb"); assert!(control.name.0.eq_ignore_ascii_case(b"parent_proto.ctl")); let points = resolved.point_resource.expect("inherited explicit CPT"); assert_eq!(points.archive.as_str(), "fortif.rlb"); assert!(points.name.0.eq_ignore_ascii_case(b"parent_proto.cpt")); let node_data = resolved .node_data_resource .expect("inherited explicit NDP provider table"); assert_eq!(node_data.archive.as_str(), "fortif.rlb"); assert!(node_data.name.0.eq_ignore_ascii_case(b"parent_proto.ndp")); let bas = resolved .bas_resource .expect("child-local explicit BAS geometry"); assert_eq!(bas.archive.as_str(), "fortif.rlb"); assert!(bas.name.0.eq_ignore_ascii_case(b"child_proto.bas")); } #[test] fn objects_registry_retains_missing_explicit_control_metadata() { let mut vfs = MemoryVfs::default(); 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( objects_path, Arc::from( build_nres(&[( b"optional_control".as_slice(), build_object_refs(&[ (b"static.rlb".as_slice(), b"optional_control.msh".as_slice()), ( b"missing.rlb".as_slice(), b"optional_control.ctl".as_slice(), ), ( b"explicit_points.rlb".as_slice(), b"other_points.cpt".as_slice(), ), ]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"optional_control.msh".as_slice(), mesh.as_slice())]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repository = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single( &repository, vfs.as_ref(), &resource_name(b"optional_control"), ) .expect("resolve despite absent optional CTL") .expect("prototype"); let control = resolved .control_resource .expect("preserve explicit missing CTL reference"); assert_eq!(control.archive.as_str(), "missing.rlb"); assert_eq!(control.name, resource_name(b"optional_control.ctl")); assert_eq!(control.type_id, None); let points = resolved .point_resource .expect("preserve independent explicit CPT reference"); assert_eq!(points.archive.as_str(), "explicit_points.rlb"); assert_eq!(points.name, resource_name(b"other_points.cpt")); } #[test] fn objects_registry_rejects_distinct_explicit_controls() { let mut vfs = MemoryVfs::default(); 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 controls_path = resource_archive_path(b"controls.rlb").expect("controls path"); let mesh = minimal_msh_payload(); vfs.insert( objects_path, Arc::from( build_nres(&[( b"prototype".as_slice(), build_object_refs(&[ (b"static.rlb".as_slice(), b"prototype.msh".as_slice()), (b"controls.rlb".as_slice(), b"first.ctl".as_slice()), (b"controls.rlb".as_slice(), b"second.ctl".as_slice()), ]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"prototype.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); vfs.insert( controls_path, Arc::from( build_nres(&[ (b"first.ctl".as_slice(), b"first control"), (b"second.ctl".as_slice(), b"second control"), ]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let error = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"prototype")) .expect_err("distinct explicit controls are ambiguous"); assert!(matches!( error, PrototypeError::Resource(ResourceError::Format(message)) if message.contains("ambiguous explicit control resources") )); } #[test] fn objects_registry_rejects_distinct_explicit_node_data() { let raw_name = |value: &[u8]| { let mut raw = [0; 32]; raw[..value.len()].copy_from_slice(value); raw }; let refs = [ ObjectRefRecord { archive_raw: raw_name(b"first.rlb"), resource_raw: raw_name(b"first.ndp"), }, ObjectRefRecord { archive_raw: raw_name(b"second.rlb"), resource_raw: raw_name(b"second.ndp"), }, ]; let error = resolve_explicit_node_data_resource(&resource_name(b"prototype"), &refs) .expect_err("distinct explicit node data resources are ambiguous"); assert!(matches!( error, PrototypeError::Resource(ResourceError::Format(message)) if message.contains("ambiguous explicit node data resources") )); } #[test] fn objects_registry_rejects_distinct_explicit_bas_resources() { let raw_name = |value: &[u8]| { let mut raw = [0; 32]; raw[..value.len()].copy_from_slice(value); raw }; let refs = [ ObjectRefRecord { archive_raw: raw_name(b"first.rlb"), resource_raw: raw_name(b"first.bas"), }, ObjectRefRecord { archive_raw: raw_name(b"second.rlb"), resource_raw: raw_name(b"second.bas"), }, ]; let error = resolve_explicit_bas_resource(&resource_name(b"prototype"), &refs) .expect_err("distinct explicit BAS resources are ambiguous"); assert!(matches!( error, PrototypeError::Resource(ResourceError::Format(message)) if message.contains("ambiguous explicit BAS geometry resources") )); } #[test] fn objects_registry_inheritance_resolves_multiple_levels() { let mut vfs = MemoryVfs::default(); 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( objects_path, Arc::from( build_nres(&[ ( b"grandparent".as_slice(), build_object_refs(&[( b"static.rlb".as_slice(), b"grandparent.msh".as_slice(), )]) .as_slice(), ), ( b"parent".as_slice(), build_object_refs(&[( b"objects.rlb".as_slice(), b"grandparent".as_slice(), )]) .as_slice(), ), ( b"child".as_slice(), build_object_refs(&[(b"objects.rlb".as_slice(), b"parent".as_slice())]) .as_slice(), ), ]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"grandparent.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child")) .expect("resolve") .expect("prototype"); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected inherited mesh"); }; assert!(mesh.name.0.eq_ignore_ascii_case(b"grandparent.msh")); } #[test] fn base_only_registry_entry_is_nongeometric() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); let fortif_path = resource_archive_path(b"fortif.rlb").expect("fortif path"); vfs.insert( objects_path, Arc::from( build_nres(&[( b"base_only".as_slice(), build_object_refs(&[(b"fortif.rlb".as_slice(), b"base_only.bas".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( fortif_path, Arc::from(build_nres(&[(b"base_only.bas".as_slice(), b"base")]).into_boxed_slice()), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"base_only")) .expect("resolve") .expect("prototype"); assert_eq!(resolved.geometry, PrototypeGeometry::NonGeometric); assert!(resolved.dependencies.is_empty()); } #[test] fn objects_registry_inheritance_rejects_direct_cycle() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); vfs.insert( objects_path, Arc::from( build_nres(&[( b"self_cycle".as_slice(), build_object_refs(&[(b"objects.rlb".as_slice(), b"self_cycle".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"self_cycle")) .expect_err("cycle"); assert!(err.to_string().contains("cycle")); } #[test] fn objects_registry_inheritance_rejects_indirect_cycle() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); vfs.insert( objects_path, Arc::from( build_nres(&[ ( b"cycle_a".as_slice(), build_object_refs(&[(b"objects.rlb".as_slice(), b"cycle_b".as_slice())]) .as_slice(), ), ( b"cycle_b".as_slice(), build_object_refs(&[(b"objects.rlb".as_slice(), b"cycle_a".as_slice())]) .as_slice(), ), ]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"cycle_a")) .expect_err("cycle"); assert!(err.to_string().contains("cycle")); } #[test] fn invalid_referenced_msh_is_error() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); let static_path = resource_archive_path(b"static.rlb").expect("static path"); vfs.insert( objects_path, Arc::from( build_nres(&[( b"bad_tree".as_slice(), build_object_refs(&[(b"static.rlb".as_slice(), b"bad_tree.msh".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"bad_tree.msh".as_slice(), b"not an nres".as_slice())]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); 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] fn missing_referenced_archive_reports_root_chain() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); vfs.insert( objects_path, Arc::from( build_nres(&[( b"broken".as_slice(), build_object_refs(&[(b"missing.rlb".as_slice(), b"broken.msh".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let (_graph, _resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &[resource_name(b"broken")]); assert_eq!(report.failures.len(), 1); assert_eq!(report.failures[0].resource_raw, b"broken"); assert_eq!(report.failures[0].edge, PrototypeGraphEdge::PrototypeToMesh); assert!(report.failures[0].message.contains("broken")); assert!(report.failures[0] .message .contains("missing.rlb:broken.msh")); } #[test] fn missing_referenced_resource_reports_root_chain() { let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); let static_path = resource_archive_path(b"static.rlb").expect("static path"); vfs.insert( objects_path, Arc::from( build_nres(&[( b"broken".as_slice(), build_object_refs(&[(b"static.rlb".as_slice(), b"missing.msh".as_slice())]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert(static_path, Arc::from(build_nres(&[]).into_boxed_slice())); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let (_graph, _resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &[resource_name(b"broken")]); assert_eq!(report.failures.len(), 1); assert_eq!(report.failures[0].resource_raw, b"broken"); assert_eq!(report.failures[0].edge, PrototypeGraphEdge::PrototypeToMesh); assert!(report.failures[0] .message .contains("static.rlb:missing.msh")); } #[test] fn first_existing_explicit_msh_is_selected_in_order() { let mut vfs = MemoryVfs::default(); 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( objects_path, Arc::from( build_nres(&[( b"ordered".as_slice(), build_object_refs(&[ (b"static.rlb".as_slice(), b"missing.msh".as_slice()), (b"static.rlb".as_slice(), b"ordered.msh".as_slice()), ]) .as_slice(), )]) .into_boxed_slice(), ), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"ordered.msh".as_slice(), mesh.as_slice())]).into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"ordered")) .expect("ordered resolve") .expect("prototype"); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected mesh"); }; assert!(mesh.name.0.eq_ignore_ascii_case(b"ordered.msh")); } #[test] fn objects_registry_inheritance_rejects_depth_limit() { let mut names = Vec::new(); let mut payloads = Vec::new(); for index in 0..34usize { names.push(format!("proto_{index}").into_bytes()); payloads.push(build_object_refs(&[( b"objects.rlb".as_slice(), format!("proto_{}", index + 1).as_bytes(), )])); } let entries = names .iter() .zip(payloads.iter()) .map(|(name, payload)| (name.as_slice(), payload.as_slice())) .collect::>(); let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); vfs.insert( objects_path, Arc::from(build_nres(&entries).into_boxed_slice()), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"proto_0")) .expect_err("depth"); assert!(err.to_string().contains("depth exceeded")); } #[test] fn generated_acyclic_prototype_graph_resolves_deterministically() { let first = generated_acyclic_graph(&[0, 1, 2, 3, 4, 5]); let second = generated_acyclic_graph(&[5, 4, 3, 2, 1, 0]); assert_eq!(first.0, second.0); assert_eq!(first.1, second.1); assert_eq!(first.2, second.2); } #[test] fn arbitrary_unit_and_registry_bytes_are_bounded_and_panic_free() { for len in 0..256usize { let bytes = vec![0xA5; len]; let unit = std::panic::catch_unwind(|| decode_unit_dat(&bytes)); let registry = std::panic::catch_unwind(|| decode_registry_entry(&bytes)); assert!(unit.is_ok()); assert!(registry.is_ok()); } } #[test] fn resolver_cache_invalidates_when_archive_fingerprint_changes() { let root = temp_dir("resolver-cache"); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); let static_path = resource_archive_path(b"static.rlb").expect("static path"); std::fs::write( root.join(objects_path.as_str()), build_nres(&[( b"dynamic".as_slice(), build_object_refs(&[(b"static.rlb".as_slice(), b"dynamic.msh".as_slice())]) .as_slice(), )]), ) .expect("objects.rlb"); std::fs::write( root.join(static_path.as_str()), build_nres(&[(b"dynamic.msh".as_slice(), b"not an nres".as_slice())]), ) .expect("initial static.rlb"); let vfs = Arc::new(DirectoryVfs::new(&root)); let repo = CachedResourceRepository::new(vfs.clone()); 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_single(&repo, vfs.as_ref(), &resource_name(b"dynamic")) .expect("updated resolve") .expect("prototype"); let PrototypeGeometry::Mesh(mesh) = resolved.geometry else { panic!("expected mesh"); }; assert!(mesh.name.0.eq_ignore_ascii_case(b"dynamic.msh")); let _ = std::fs::remove_dir_all(root); } #[test] #[ignore = "requires licensed corpus"] fn resolves_known_part1_registry_cases() { let root = corpus_root("IS"); let vfs = Arc::new(DirectoryVfs::new(&root)); let repo = CachedResourceRepository::new(vfs.clone()); let cases = [ (b"r_h_01".as_slice(), "bases.rlb", b"r_h_01.msh".as_slice()), ( b"s_tree_04".as_slice(), "static.rlb", b"s_tree_0_04.msh".as_slice(), ), ( b"fr_m_brige".as_slice(), "fortif.rlb", b"fr_m_brige.msh".as_slice(), ), ]; for (key, archive, model) in cases { 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 { panic!("expected mesh"); }; assert_eq!(mesh.archive.as_str().to_ascii_lowercase(), archive); assert!(mesh.name.0.eq_ignore_ascii_case(model)); } let plant = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"fr_b_plant")) .expect("resolve factory prototype") .expect("factory prototype"); let bas = plant.bas_resource.expect("explicit BAS registry reference"); assert_eq!(bas.archive.as_str().to_ascii_lowercase(), "fortif.rlb"); assert!(bas.name.0.eq_ignore_ascii_case(b"fr_b_plant.bas")); let bas_archive = repo .open_archive(&bas.archive) .expect("open explicit BAS archive"); let bas_entry = repo .find(bas_archive, &bas.name) .expect("find explicit BAS entry") .expect("BAS entry exists"); let bas_payload = repo.read(bas_entry).expect("read explicit BAS entry"); let geometry = decode_bas_geometry(bas_payload.as_slice()).expect("decode licensed BAS geometry"); assert_eq!(geometry.first.len(), 1); assert_eq!(geometry.second.len(), 1); assert_eq!(geometry.first[0].points.len(), 21); assert_eq!(geometry.second[0].points.len(), 15); } #[test] #[ignore = "requires licensed corpus"] fn resolves_some_registry_entries_in_both_corpora() { for corpus in ["IS", "IS2"] { let root = corpus_root(corpus); let objects = std::fs::read(root.join("objects.rlb")).expect("objects.rlb"); let document = fparkan_nres::decode( Arc::from(objects.into_boxed_slice()), fparkan_nres::ReadProfile::Compatible, ) .expect("objects.rlb document"); let vfs = Arc::new(DirectoryVfs::new(&root)); let repo = CachedResourceRepository::new(vfs.clone()); let mut resolved = 0usize; for entry in document.entries().iter().take(64) { 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() { resolved += 1; } } assert!(resolved > 0, "{corpus}: no registry entries resolved"); } } #[test] #[ignore = "requires licensed corpus"] fn licensed_corpora_unit_dat_parse_counts() { let cases = [("IS", 425, 5_219), ("IS2", 676, 8_145)]; for (corpus, expected_files, expected_records) in cases { let root = corpus_root(corpus); let mut dat_paths = Vec::new(); collect_unit_dat_files(&root, &mut dat_paths); dat_paths.sort(); let mut records = 0usize; for path in &dat_paths { let bytes = std::fs::read(path).expect("unit DAT"); let unit = decode_unit_dat(&bytes).expect("unit DAT decode"); for record in &unit.records { assert!( archive_name_is(&record.archive_raw, b"objects.rlb"), "{}: unexpected component archive {:?}", path.display(), cstr_bytes(&record.archive_raw) ); assert_eq!( record.kind, 1, "{}: unexpected component kind", path.display() ); } records += unit.records.len(); } assert_eq!(dat_paths.len(), expected_files, "{corpus} unit DAT files"); assert_eq!(records, expected_records, "{corpus} unit DAT records"); } } #[test] #[ignore = "requires licensed corpus"] fn licensed_corpora_registry_payloads_are_record_aligned() { for corpus in ["IS", "IS2"] { let root = corpus_root(corpus); let objects = std::fs::read(root.join("objects.rlb")).expect("objects.rlb"); let document = fparkan_nres::decode( Arc::from(objects.into_boxed_slice()), fparkan_nres::ReadProfile::Compatible, ) .expect("objects.rlb document"); assert!(document.entry_count() > 0, "{corpus}: empty objects.rlb"); for entry in document.entries() { let payload = document.payload(entry.id()).expect("registry payload"); assert!( payload.len().is_multiple_of(64), "{corpus}: registry payload for {:?} is not 64-byte aligned", entry.name_bytes() ); decode_registry_entry(payload).expect("registry payload decode"); } } } fn collect_unit_dat_files(dir: &Path, out: &mut Vec) { let mut children: Vec<_> = std::fs::read_dir(dir) .expect("read dir") .map(|entry| entry.expect("entry").path()) .collect(); children.sort(); for child in children { if child.is_dir() { collect_unit_dat_files(&child, out); } else if child .extension() .and_then(|ext| ext.to_str()) .is_some_and(|ext| ext.eq_ignore_ascii_case("dat")) && child.components().any(|component| { component .as_os_str() .to_str() .is_some_and(|text| text.eq_ignore_ascii_case("UNITS")) }) { out.push(child); } } } fn corpus_root(name: &str) -> std::path::PathBuf { let variable = match name { "IS" => "FPARKAN_CORPUS_PART1_ROOT", "IS2" => "FPARKAN_CORPUS_PART2_ROOT", _ => panic!("unknown licensed corpus part: {name}"), }; let root = std::env::var_os(variable) .map(std::path::PathBuf::from) .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); assert!( root.is_dir(), "licensed corpus root is missing: {}", root.display() ); root } fn generated_acyclic_graph( order: &[usize], ) -> ( PrototypeGraph, Vec, PrototypeGraphReport, ) { let names = (0..6usize) .map(|index| format!("node_{index}").into_bytes()) .collect::>(); let payloads = (0..6usize) .map(|index| { if index == 0 { build_object_refs(&[(b"static.rlb".as_slice(), b"node_0.msh".as_slice())]) } else { build_object_refs(&[( b"objects.rlb".as_slice(), format!("node_{}", index - 1).as_bytes(), )]) } }) .collect::>(); let entries = order .iter() .map(|index| (names[*index].as_slice(), payloads[*index].as_slice())) .collect::>(); let mut vfs = MemoryVfs::default(); let objects_path = resource_archive_path(b"objects.rlb").expect("objects path"); let static_path = resource_archive_path(b"static.rlb").expect("static path"); vfs.insert( objects_path, Arc::from(build_nres(&entries).into_boxed_slice()), ); vfs.insert( static_path, Arc::from( build_nres(&[(b"node_0.msh".as_slice(), minimal_msh_payload().as_slice())]) .into_boxed_slice(), ), ); let vfs = Arc::new(vfs); let repo = CachedResourceRepository::new(vfs.clone()); build_prototype_graph_report( &repo, vfs.as_ref(), &[resource_name(b"node_5"), resource_name(b"node_3")], ) } fn temp_dir(name: &str) -> std::path::PathBuf { let path = std::env::temp_dir().join(format!( "fparkan-prototype-{name}-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock") .as_nanos() )); std::fs::create_dir_all(&path).expect("temp dir"); path } fn build_unit_dat_binding(archive: &[u8], model: &[u8]) -> Vec { let mut out = vec![0; UNIT_DAT_MIN_SIZE]; out[0..4].copy_from_slice(&UNIT_DAT_MAGIC.to_le_bytes()); copy_cstr(&mut out[0x08..0x28], archive); copy_cstr(&mut out[0x28..0x48], model); out } fn build_unit_dat(components: &[(&[u8], &[u8])]) -> Vec { let mut out = vec![0; 8]; out[0..4].copy_from_slice(&UNIT_DAT_MAGIC.to_le_bytes()); for (index, (archive, resource)) in components.iter().enumerate() { let mut record = [0; 112]; copy_cstr(&mut record[0..32], archive); copy_cstr(&mut record[32..64], resource); record[64..68].copy_from_slice(&1_u32.to_le_bytes()); record[68..72].copy_from_slice( &i32::try_from(index) .map_or(-1, |value| value.saturating_sub(1)) .to_le_bytes(), ); copy_cstr(&mut record[72..104], b"component"); out.extend_from_slice(&record); } out } fn build_bas_geometry_payload() -> Vec { let mut out = Vec::new(); out.extend_from_slice(&2_u32.to_le_bytes()); push_bas_record_with_words( &mut out, 1, &[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], &[0x1122_3344], &[0x5566_7788], ); push_bas_record_with_words( &mut out, 2, &[[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]], &[7, 8], &[9, 10], ); out.extend_from_slice(&1_u32.to_le_bytes()); out.extend_from_slice(&1_u32.to_le_bytes()); push_bas_points(&mut out, &[[20.0, 21.0, 22.0], [23.0, 24.0, 25.0]]); out } fn push_bas_record_with_words( out: &mut Vec, declared_count: u32, points: &[[f32; 3]], words_a: &[u32], words_b: &[u32], ) { out.extend_from_slice(&declared_count.to_le_bytes()); push_bas_points(out, points); for word in words_a.iter().chain(words_b) { out.extend_from_slice(&word.to_le_bytes()); } } fn push_bas_points(out: &mut Vec, points: &[[f32; 3]]) { for point in points { for value in point { out.extend_from_slice(&value.to_le_bytes()); } } } fn build_object_refs(items: &[(&[u8], &[u8])]) -> Vec { let mut out = Vec::with_capacity(items.len() * 64); for (archive, resource) in items { let mut chunk = [0; 64]; copy_cstr(&mut chunk[..32], archive); copy_cstr(&mut chunk[32..], resource); out.extend_from_slice(&chunk); } out } fn build_nres(entries: &[(&[u8], &[u8])]) -> Vec { let entries = entries .iter() .map(|(name, payload)| TestEntry { type_id: 0, attr3: 0, name, payload, }) .collect::>(); build_nres_typed(&entries) } fn minimal_msh_payload() -> Vec { build_nres_typed(&[ TestEntry { type_id: 1, attr3: 38, name: b"Res1", payload: &[], }, TestEntry { type_id: 2, attr3: 0, name: b"Res2", payload: &[0; 0x8c], }, TestEntry { type_id: 3, attr3: 0, name: b"Res3", payload: &[], }, TestEntry { type_id: 6, attr3: 0, name: b"Res6", payload: &[], }, TestEntry { type_id: 13, attr3: 0, name: b"Res13", payload: &[], }, ]) } struct TestEntry<'a> { type_id: u32, attr3: u32, name: &'a [u8], payload: &'a [u8], } fn build_nres_typed(entries: &[TestEntry<'_>]) -> Vec { let mut out = vec![0; 16]; let mut offsets = Vec::with_capacity(entries.len()); for entry in entries { offsets.push(u32::try_from(out.len()).expect("offset")); out.extend_from_slice(entry.payload); let padding = (8 - (out.len() % 8)) % 8; out.resize(out.len() + padding, 0); } let mut order: Vec = (0..entries.len()).collect(); order.sort_by(|left, right| entries[*left].name.cmp(entries[*right].name)); for (idx, entry) in entries.iter().enumerate() { push_u32(&mut out, entry.type_id); push_u32(&mut out, 0); push_u32(&mut out, 0); push_u32( &mut out, u32::try_from(entry.payload.len()).expect("payload"), ); push_u32(&mut out, entry.attr3); let mut name_raw = [0; 36]; copy_cstr(&mut name_raw, entry.name); out.extend_from_slice(&name_raw); push_u32(&mut out, offsets[idx]); push_u32(&mut out, u32::try_from(order[idx]).expect("sort index")); } out[0..4].copy_from_slice(b"NRes"); out[4..8].copy_from_slice(&0x100_u32.to_le_bytes()); out[8..12].copy_from_slice(&u32::try_from(entries.len()).expect("count").to_le_bytes()); let total_size = u32::try_from(out.len()).expect("total size"); out[12..16].copy_from_slice(&total_size.to_le_bytes()); out } fn copy_cstr(dst: &mut [u8], src: &[u8]) { let len = dst.len().saturating_sub(1).min(src.len()); dst[..len].copy_from_slice(&src[..len]); } fn push_u32(out: &mut Vec, value: u32) { out.extend_from_slice(&value.to_le_bytes()); } }