aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-03 20:44:26 +0300
committerValentin Popov <valentin@popov.link>2026-07-03 20:44:26 +0300
commitf34c422b20fd1fc1b4bf71ebf448b3c42f104226 (patch)
treec6478b2e605eecbab2a29d645d227ec8befa61ca /crates
parenta89bd9af2aa6a4c85a7dce5cbc61eb38c6a91f49 (diff)
downloadfparkan-f34c422b20fd1fc1b4bf71ebf448b3c42f104226.tar.xz
fparkan-f34c422b20fd1fc1b4bf71ebf448b3c42f104226.zip
test: realign stage 2 acceptance coverage
Diffstat (limited to 'crates')
-rw-r--r--crates/fparkan-assets/src/lib.rs346
-rw-r--r--crates/fparkan-runtime/src/lib.rs56
2 files changed, 383 insertions, 19 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 460b3c6..8fc2501 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -771,7 +771,11 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
wear_node_id,
fparkan_prototype::PrototypeGraphEdgeKind::MeshToWear,
PrototypeGraphRequiredness::Required,
- Some(provenance_for_resource(root_index, mesh_parent_edge, &wear_key)),
+ Some(provenance_for_resource(
+ root_index,
+ mesh_parent_edge,
+ &wear_key,
+ )),
&mut next_edge,
);
report.material_slot_count += table.entries.len();
@@ -937,7 +941,9 @@ fn push_graph_resource_node(
*next_node = (*next_node).saturating_add(1);
graph
.nodes
- .push(fparkan_prototype::PrototypeGraphNode::resource(kind, resource, id));
+ .push(fparkan_prototype::PrototypeGraphNode::resource(
+ kind, resource, id,
+ ));
id
}
@@ -952,14 +958,16 @@ fn push_graph_edge(
) -> fparkan_prototype::PrototypeGraphEdgeId {
let id = fparkan_prototype::PrototypeGraphEdgeId(*next_edge);
*next_edge = (*next_edge).saturating_add(1);
- graph.edges.push(fparkan_prototype::PrototypeGraphEdgeInstance {
- id,
- from,
- to,
- kind,
- requiredness,
- provenance,
- });
+ graph
+ .edges
+ .push(fparkan_prototype::PrototypeGraphEdgeInstance {
+ id,
+ from,
+ to,
+ kind,
+ requiredness,
+ provenance,
+ });
id
}
@@ -1144,11 +1152,8 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
});
for texture in texture_requests {
- let prepared_texture = prepare_texture(
- repository,
- &texture,
- PreparedTextureUsage::Diffuse,
- )?;
+ let prepared_texture =
+ prepare_texture(repository, &texture, PreparedTextureUsage::Diffuse)?;
texture_ids.push(prepared_texture.id);
prepared_textures.push(prepared_texture);
texture_count += 1;
@@ -1664,7 +1669,10 @@ mod tests {
};
assert_ne!(stable_visual_id(&first), stable_visual_id(&second));
- assert_ne!(prepared_visual_signature(&first), prepared_visual_signature(&second));
+ assert_ne!(
+ prepared_visual_signature(&first),
+ prepared_visual_signature(&second)
+ );
}
#[test]
@@ -1781,6 +1789,222 @@ mod tests {
}
#[test]
+ fn graph_visual_dependency_edges_preserve_root_and_parent_provenance() {
+ let mesh_key = ResourceKey {
+ archive: parse_path("static.rlb").expect("archive"),
+ name: resource_name(b"tree.msh"),
+ type_id: Some(0x4853_454D),
+ };
+ let mat0 = mat0_with_texture(b"TEX_A");
+ let texm = texm_payload();
+ let lightmap_texm = texm_payload();
+ let repo = repository_with_archives_meta(&[
+ (
+ "static.rlb",
+ &[TestNresEntry {
+ name: b"tree.wea",
+ payload: b"1\n0 MAT_A\n\nLIGHTMAPS\n1\n0 LM_A\n",
+ type_id: WEAR_KIND,
+ attr2: 0,
+ }],
+ ),
+ (
+ "material.lib",
+ &[TestNresEntry {
+ name: b"MAT_A",
+ payload: &mat0,
+ type_id: MAT0_KIND,
+ attr2: 0,
+ }],
+ ),
+ (
+ TEXTURES_ARCHIVE,
+ &[TestNresEntry {
+ name: b"TEX_A",
+ payload: &texm,
+ type_id: 0,
+ attr2: 0,
+ }],
+ ),
+ (
+ LIGHTMAP_ARCHIVE,
+ &[TestNresEntry {
+ name: b"LM_A",
+ payload: &lightmap_texm,
+ type_id: 0,
+ attr2: 0,
+ }],
+ ),
+ ]);
+ let prototype = EffectivePrototype {
+ key: fparkan_prototype::PrototypeKey(resource_name(b"tree")),
+ geometry: PrototypeGeometry::Mesh(mesh_key),
+ source: fparkan_prototype::PrototypeSource::DirectArchive,
+ dependencies: Vec::new(),
+ };
+ let mut graph = prototype_graph_for_mesh(&prototype);
+ let mut report = PrototypeGraphReport {
+ root_count: 1,
+ direct_reference_count: 1,
+ resolved_count: 1,
+ mesh_dependency_count: 1,
+ ..PrototypeGraphReport::default()
+ };
+
+ extend_graph_report_with_visual_dependencies(
+ &repo,
+ &mut report,
+ &mut graph,
+ std::slice::from_ref(&prototype),
+ );
+
+ let wear_edge = graph
+ .edges
+ .iter()
+ .find(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::MeshToWear)
+ .expect("wear edge");
+ let material_edge = graph
+ .edges
+ .iter()
+ .find(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::WearToMaterial)
+ .expect("material edge");
+ let texture_edge = graph
+ .edges
+ .iter()
+ .find(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::MaterialToTexture)
+ .expect("texture edge");
+ let lightmap_edge = graph
+ .edges
+ .iter()
+ .find(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::WearToLightmap)
+ .expect("lightmap edge");
+
+ assert_eq!(
+ wear_edge
+ .provenance
+ .as_ref()
+ .expect("wear provenance")
+ .parent_edge,
+ Some(fparkan_prototype::PrototypeGraphEdgeId(1))
+ );
+ assert_eq!(
+ material_edge
+ .provenance
+ .as_ref()
+ .expect("material provenance")
+ .parent_edge,
+ Some(wear_edge.id)
+ );
+ assert_eq!(
+ texture_edge
+ .provenance
+ .as_ref()
+ .expect("texture provenance")
+ .parent_edge,
+ Some(material_edge.id)
+ );
+ assert_eq!(
+ lightmap_edge
+ .provenance
+ .as_ref()
+ .expect("lightmap provenance")
+ .parent_edge,
+ Some(wear_edge.id)
+ );
+ assert!(graph
+ .edges
+ .iter()
+ .filter_map(|edge| edge.provenance.as_ref())
+ .all(|provenance| provenance.root_index == 0));
+ }
+
+ #[test]
+ fn prepare_single_visual_mission_assets_materialize_model_wear_material_and_texture_payloads() {
+ let mesh_key = ResourceKey {
+ archive: parse_path("static.rlb").expect("archive"),
+ name: resource_name(b"tree.msh"),
+ type_id: Some(0x4853_454D),
+ };
+ let msh = minimal_model_archive();
+ let mat0 = mat0_with_texture(b"TEX_A");
+ let texm = texm_payload();
+ let lightmap_texm = texm_payload();
+ let repo = repository_with_archives_meta(&[
+ (
+ "static.rlb",
+ &[
+ TestNresEntry {
+ name: b"tree.msh",
+ payload: &msh,
+ type_id: 0x4853_454D,
+ attr2: 0,
+ },
+ TestNresEntry {
+ name: b"tree.wea",
+ payload: b"1\n0 MAT_A\n\nLIGHTMAPS\n1\n0 LM_A\n",
+ type_id: WEAR_KIND,
+ attr2: 0,
+ },
+ ],
+ ),
+ (
+ "material.lib",
+ &[TestNresEntry {
+ name: b"MAT_A",
+ payload: &mat0,
+ type_id: MAT0_KIND,
+ attr2: 0,
+ }],
+ ),
+ (
+ TEXTURES_ARCHIVE,
+ &[TestNresEntry {
+ name: b"TEX_A",
+ payload: &texm,
+ type_id: 0,
+ attr2: 0,
+ }],
+ ),
+ (
+ LIGHTMAP_ARCHIVE,
+ &[TestNresEntry {
+ name: b"LM_A",
+ payload: &lightmap_texm,
+ type_id: 0,
+ attr2: 0,
+ }],
+ ),
+ ]);
+ let prototype = EffectivePrototype {
+ key: fparkan_prototype::PrototypeKey(resource_name(b"tree")),
+ geometry: PrototypeGeometry::Mesh(mesh_key),
+ source: fparkan_prototype::PrototypeSource::DirectArchive,
+ dependencies: Vec::new(),
+ };
+
+ let assets = prepare_mission_assets_with_repository(
+ &repo,
+ std::slice::from_ref(&(0..1)),
+ std::slice::from_ref(&prototype),
+ )
+ .expect("prepared mission assets");
+
+ assert_eq!(assets.models.len(), 1);
+ assert_eq!(assets.wears.len(), 1);
+ assert_eq!(assets.materials.len(), 1);
+ assert_eq!(assets.textures.len(), 2);
+ assert_eq!(assets.visuals.len(), 1);
+ assert_eq!(assets.object_visuals, vec![vec![assets.visuals[0].id]]);
+
+ let visual = &assets.visuals[0];
+ assert_eq!(visual.model_id, Some(assets.models[0].id));
+ assert_eq!(visual.wear_id, Some(assets.wears[0].id));
+ assert_eq!(visual.material_ids, vec![assets.materials[0].id]);
+ assert_eq!(visual.texture_ids.len(), 1);
+ assert_eq!(visual.lightmap_ids.len(), 1);
+ }
+
+ #[test]
fn graph_report_uses_strict_texture_archive_policy() {
let mesh_key = ResourceKey {
archive: parse_path("static.rlb").expect("archive"),
@@ -1843,7 +2067,10 @@ mod tests {
assert_eq!(report.texture_request_count, 1);
assert_eq!(report.texture_resolved_count, 0);
assert_eq!(report.failures.len(), 1);
- assert_eq!(report.failures[0].edge, PrototypeGraphEdge::MaterialToTexture);
+ assert_eq!(
+ report.failures[0].edge,
+ PrototypeGraphEdge::MaterialToTexture
+ );
}
#[test]
@@ -1937,7 +2164,10 @@ mod tests {
let mut vfs = MemoryVfs::default();
for (archive, entries) in archives {
let path = parse_path(archive).expect("archive path");
- vfs.insert(path, Arc::from(build_nres_with_meta(entries).into_boxed_slice()));
+ vfs.insert(
+ path,
+ Arc::from(build_nres_with_meta(entries).into_boxed_slice()),
+ );
}
CachedResourceRepository::new(Arc::new(vfs))
}
@@ -1964,6 +2194,81 @@ mod tests {
bytes
}
+ fn minimal_model_archive() -> Vec<u8> {
+ struct MshEntry<'a> {
+ type_id: u32,
+ attr3: u32,
+ name: &'a [u8],
+ payload: &'a [u8],
+ }
+
+ let entries = [
+ MshEntry {
+ type_id: 1,
+ attr3: 38,
+ name: b"Res1",
+ payload: &[],
+ },
+ MshEntry {
+ type_id: 2,
+ attr3: 0,
+ name: b"Res2",
+ payload: &[0; 0x8c],
+ },
+ MshEntry {
+ type_id: 3,
+ attr3: 0,
+ name: b"Res3",
+ payload: &[],
+ },
+ MshEntry {
+ type_id: 6,
+ attr3: 0,
+ name: b"Res6",
+ payload: &[],
+ },
+ MshEntry {
+ type_id: 13,
+ attr3: 0,
+ name: b"Res13",
+ payload: &[],
+ },
+ ];
+
+ 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<usize> = (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];
+ let len = name_raw.len().saturating_sub(1).min(entry.name.len());
+ name_raw[..len].copy_from_slice(&entry.name[..len]);
+ 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 prototype_graph_for_mesh(prototype: &EffectivePrototype) -> PrototypeGraph {
let root_node = fparkan_prototype::PrototypeGraphNode::root(
fparkan_prototype::PrototypeKey(prototype.key.0.clone()),
@@ -2055,7 +2360,10 @@ mod tests {
push_u32(&mut out, entry.type_id);
push_u32(&mut out, 0);
push_u32(&mut out, entry.attr2);
- push_u32(&mut out, u32::try_from(entry.payload.len()).expect("payload"));
+ push_u32(
+ &mut out,
+ u32::try_from(entry.payload.len()).expect("payload"),
+ );
push_u32(&mut out, 0);
let mut name_raw = [0; 36];
let len = name_raw.len().saturating_sub(1).min(entry.name.len());
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index d10647d..6a4e478 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -1115,6 +1115,50 @@ mod tests {
}
#[test]
+ #[ignore = "requires local testdata corpus"]
+ fn selected_is_and_is2_missions_preserve_runtime_object_drafts() {
+ for case in [
+ ("IS", "MISSIONS/Autodemo.00/data.tma"),
+ ("IS2", "MISSIONS/Autodemo.00/data.tma"),
+ ] {
+ let root = local_testdata_root(case.0);
+ let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
+ let mut engine = create(
+ EngineConfig {
+ mode: EngineMode::Headless,
+ },
+ EngineServices::new(vfs),
+ )
+ .expect("engine");
+ let loaded = load_mission(
+ &mut engine,
+ MissionRequest {
+ key: case.1.to_string(),
+ },
+ )
+ .expect("load mission");
+ let drafts = loaded_mission_object_drafts(&engine).expect("object drafts");
+ let assets = loaded_mission_assets(&engine).expect("mission assets");
+
+ assert_eq!(drafts.len(), loaded.object_count);
+ assert!(drafts.iter().any(|draft| !draft.visual_ids.is_empty()));
+ for (index, draft) in drafts.iter().enumerate() {
+ assert_eq!(
+ draft.original_id,
+ u32::try_from(index).ok().map(OriginalObjectId)
+ );
+ assert_eq!(draft.visual_ids, assets.visuals_for_object(index));
+ assert!(draft.position.iter().all(|component| component.is_finite()));
+ assert!(draft
+ .orientation_raw
+ .iter()
+ .all(|component| component.is_finite()));
+ assert!(draft.scale.iter().all(|component| component.is_finite()));
+ }
+ }
+ }
+
+ #[test]
#[ignore = "requires licensed corpus"]
fn licensed_corpora_load_all_mission_foundations() {
let part1 = load_all(&licensed_root("IS"));
@@ -1326,6 +1370,18 @@ mod tests {
root
}
+ fn local_testdata_root(name: &str) -> PathBuf {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../testdata")
+ .join(name);
+ assert!(
+ root.is_dir(),
+ "local testdata root is missing: {}",
+ root.display()
+ );
+ root
+ }
+
#[derive(Clone, Copy)]
enum DenyRule {
Suffix(&'static str),