fix(stage2): preserve byte-exact prototype identities
This commit is contained in:
@@ -570,7 +570,7 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>(
|
|||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
enum PreparedVisualSignature {
|
enum PreparedVisualSignature {
|
||||||
Mesh {
|
Mesh {
|
||||||
archive: String,
|
archive: Vec<u8>,
|
||||||
name: Vec<u8>,
|
name: Vec<u8>,
|
||||||
type_id: Option<u32>,
|
type_id: Option<u32>,
|
||||||
dependency_count: usize,
|
dependency_count: usize,
|
||||||
@@ -583,7 +583,7 @@ enum PreparedVisualSignature {
|
|||||||
fn prepared_visual_signature(proto: &EffectivePrototype) -> PreparedVisualSignature {
|
fn prepared_visual_signature(proto: &EffectivePrototype) -> PreparedVisualSignature {
|
||||||
match &proto.geometry {
|
match &proto.geometry {
|
||||||
PrototypeGeometry::Mesh(key) => PreparedVisualSignature::Mesh {
|
PrototypeGeometry::Mesh(key) => PreparedVisualSignature::Mesh {
|
||||||
archive: key.archive.as_str().to_string(),
|
archive: key.archive.identity_bytes().to_vec(),
|
||||||
name: key.name.0.clone(),
|
name: key.name.0.clone(),
|
||||||
type_id: key.type_id,
|
type_id: key.type_id,
|
||||||
dependency_count: proto.dependencies.len(),
|
dependency_count: proto.dependencies.len(),
|
||||||
@@ -1160,7 +1160,7 @@ fn stable_visual_id(proto: &EffectivePrototype) -> u64 {
|
|||||||
match &proto.geometry {
|
match &proto.geometry {
|
||||||
PrototypeGeometry::Mesh(key) => {
|
PrototypeGeometry::Mesh(key) => {
|
||||||
1_u8.hash(&mut hasher);
|
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.name.0.hash(&mut hasher);
|
||||||
key.type_id.hash(&mut hasher);
|
key.type_id.hash(&mut hasher);
|
||||||
}
|
}
|
||||||
@@ -1269,6 +1269,35 @@ mod tests {
|
|||||||
assert!(matches!(err, AssetError::Texture(_)));
|
assert!(matches!(err, AssetError::Texture(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn prepares_real_unit_asset_plan() {
|
fn prepares_real_unit_asset_plan() {
|
||||||
|
|||||||
@@ -640,22 +640,20 @@ fn resolve_unit_dat_prototype_requests(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(unit) = decode_unit_dat(&bytes) {
|
if let Ok(unit) = decode_unit_dat(&bytes) {
|
||||||
if !unit.records.is_empty() {
|
let mut prototypes = Vec::with_capacity(unit.records.len());
|
||||||
let mut prototypes = Vec::with_capacity(unit.records.len());
|
for record in &unit.records {
|
||||||
for record in &unit.records {
|
let prototype = resolve_unit_component(repository, record)?.ok_or_else(|| {
|
||||||
let prototype = resolve_unit_component(repository, record)?.ok_or_else(|| {
|
PrototypeError::Resource(ResourceError::Format(format!(
|
||||||
PrototypeError::Resource(ResourceError::Format(format!(
|
"unit component {} did not resolve",
|
||||||
"unit component {} did not resolve",
|
String::from_utf8_lossy(cstr_bytes(&record.resource_raw))
|
||||||
String::from_utf8_lossy(cstr_bytes(&record.resource_raw))
|
)))
|
||||||
)))
|
})?;
|
||||||
})?;
|
prototypes.push(prototype);
|
||||||
prototypes.push(prototype);
|
|
||||||
}
|
|
||||||
return Ok(ResolvedPrototypeRequests {
|
|
||||||
expected_count: unit.records.len(),
|
|
||||||
prototypes,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return Ok(ResolvedPrototypeRequests {
|
||||||
|
expected_count: unit.records.len(),
|
||||||
|
prototypes,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let binding = decode_unit_dat_binding(&bytes)?;
|
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 {
|
fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> PrototypeGraphEdge {
|
||||||
let _ = err;
|
match err {
|
||||||
edge
|
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 {
|
fn provenance_for_root(root_index: usize, root: &ResourceName) -> PrototypeGraphProvenance {
|
||||||
@@ -1581,6 +1602,69 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("missing unit DAT"));
|
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]
|
#[test]
|
||||||
fn unit_dat_expands_components_in_order() {
|
fn unit_dat_expands_components_in_order() {
|
||||||
let mut vfs = MemoryVfs::default();
|
let mut vfs = MemoryVfs::default();
|
||||||
@@ -1896,7 +1980,7 @@ mod tests {
|
|||||||
assert_eq!(report.failures[0].resource_raw, b"broken");
|
assert_eq!(report.failures[0].resource_raw, b"broken");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.failures[0].edge,
|
report.failures[0].edge,
|
||||||
PrototypeGraphEdge::MissionToObjectsRegistry
|
PrototypeGraphEdge::PrototypeToMesh
|
||||||
);
|
);
|
||||||
assert!(report.failures[0].message.contains("broken"));
|
assert!(report.failures[0].message.contains("broken"));
|
||||||
assert!(report.failures[0]
|
assert!(report.failures[0]
|
||||||
@@ -1929,6 +2013,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(report.failures.len(), 1);
|
assert_eq!(report.failures.len(), 1);
|
||||||
assert_eq!(report.failures[0].resource_raw, b"broken");
|
assert_eq!(report.failures[0].resource_raw, b"broken");
|
||||||
|
assert_eq!(report.failures[0].edge, PrototypeGraphEdge::PrototypeToMesh);
|
||||||
assert!(report.failures[0]
|
assert!(report.failures[0]
|
||||||
.message
|
.message
|
||||||
.contains("static.rlb:missing.msh"));
|
.contains("static.rlb:missing.msh"));
|
||||||
|
|||||||
Reference in New Issue
Block a user