diff options
| author | Valentin Popov <valentin@popov.link> | 2026-07-03 15:58:17 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-07-03 15:58:17 +0300 |
| commit | ee9b318172bf50fadd90e0cfc364330fb85f778a (patch) | |
| tree | e100304120ca431016d217d26e210bbe62d77d15 | |
| parent | 29bf4830cf9fffbf662051f4d9c55cbfa1dfd073 (diff) | |
| download | fparkan-ee9b318172bf50fadd90e0cfc364330fb85f778a.tar.xz fparkan-ee9b318172bf50fadd90e0cfc364330fb85f778a.zip | |
fix(stage2): preserve byte-exact prototype identities
| -rw-r--r-- | crates/fparkan-assets/src/lib.rs | 35 | ||||
| -rw-r--r-- | crates/fparkan-prototype/src/lib.rs | 121 |
2 files changed, 135 insertions, 21 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 26244ee..b128822 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -570,7 +570,7 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>( #[derive(Clone, Debug, Eq, PartialEq)] enum PreparedVisualSignature { Mesh { - archive: String, + archive: Vec<u8>, name: Vec<u8>, type_id: Option<u32>, dependency_count: usize, @@ -583,7 +583,7 @@ enum PreparedVisualSignature { fn prepared_visual_signature(proto: &EffectivePrototype) -> PreparedVisualSignature { match &proto.geometry { PrototypeGeometry::Mesh(key) => PreparedVisualSignature::Mesh { - archive: key.archive.as_str().to_string(), + archive: key.archive.identity_bytes().to_vec(), name: key.name.0.clone(), type_id: key.type_id, dependency_count: proto.dependencies.len(), @@ -1160,7 +1160,7 @@ fn stable_visual_id(proto: &EffectivePrototype) -> u64 { match &proto.geometry { PrototypeGeometry::Mesh(key) => { 1_u8.hash(&mut hasher); - key.archive.as_str().hash(&mut hasher); + key.archive.identity_bytes().hash(&mut hasher); key.name.0.hash(&mut hasher); key.type_id.hash(&mut hasher); } @@ -1270,6 +1270,35 @@ mod tests { } #[test] + fn stable_visual_id_uses_archive_identity_bytes() { + let first = EffectivePrototype { + key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")), + geometry: PrototypeGeometry::Mesh(ResourceKey { + archive: normalize_relative(b"DATA/\xFF.lib", PathPolicy::HostCompatible) + .expect("archive"), + name: resource_name(b"mesh.msh"), + type_id: Some(0x4853_454D), + }), + source: fparkan_prototype::PrototypeSource::DirectArchive, + dependencies: Vec::new(), + }; + let second = EffectivePrototype { + key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")), + geometry: PrototypeGeometry::Mesh(ResourceKey { + archive: normalize_relative(b"DATA/\xFE.lib", PathPolicy::HostCompatible) + .expect("archive"), + name: resource_name(b"mesh.msh"), + type_id: Some(0x4853_454D), + }), + source: fparkan_prototype::PrototypeSource::DirectArchive, + dependencies: Vec::new(), + }; + + assert_ne!(stable_visual_id(&first), stable_visual_id(&second)); + assert_ne!(prepared_visual_signature(&first), prepared_visual_signature(&second)); + } + + #[test] #[ignore = "requires licensed corpus"] fn prepares_real_unit_asset_plan() { let root = fixture_root("IS"); diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs index 0b1e460..e85322b 100644 --- a/crates/fparkan-prototype/src/lib.rs +++ b/crates/fparkan-prototype/src/lib.rs @@ -640,22 +640,20 @@ fn resolve_unit_dat_prototype_requests( }; if let Ok(unit) = decode_unit_dat(&bytes) { - if !unit.records.is_empty() { - 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, - }); + 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, + }); } let binding = decode_unit_dat_binding(&bytes)?; @@ -906,8 +904,31 @@ pub fn build_prototype_graph_report( } fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> PrototypeGraphEdge { - let _ = err; - edge + 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) -> PrototypeGraphProvenance { @@ -1582,6 +1603,69 @@ mod tests { } #[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"); @@ -1896,7 +1980,7 @@ mod tests { assert_eq!(report.failures[0].resource_raw, b"broken"); assert_eq!( report.failures[0].edge, - PrototypeGraphEdge::MissionToObjectsRegistry + PrototypeGraphEdge::PrototypeToMesh ); assert!(report.failures[0].message.contains("broken")); assert!(report.failures[0] @@ -1929,6 +2013,7 @@ mod tests { 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")); |
