feat(stage2): materialize visual dependency graph
This commit is contained in:
@@ -165,9 +165,9 @@ fn inspect_prototype(args: &[String]) -> Result<(), String> {
|
|||||||
let vfs = Arc::new(DirectoryVfs::new(root));
|
let vfs = Arc::new(DirectoryVfs::new(root));
|
||||||
let repository = CachedResourceRepository::new(vfs.clone());
|
let repository = CachedResourceRepository::new(vfs.clone());
|
||||||
let roots = [resource_name(key.as_bytes())];
|
let roots = [resource_name(key.as_bytes())];
|
||||||
let (graph, resolved, mut report) =
|
let (mut graph, resolved, mut report) =
|
||||||
build_prototype_graph_report(&repository, vfs.as_ref(), &roots);
|
build_prototype_graph_report(&repository, vfs.as_ref(), &roots);
|
||||||
extend_graph_report_with_visual_dependencies(&repository, &mut report, &graph, &resolved);
|
extend_graph_report_with_visual_dependencies(&repository, &mut report, &mut graph, &resolved);
|
||||||
println!("{}", prototype_inspect_json(&key, &graph, &report));
|
println!("{}", prototype_inspect_json(&key, &graph, &report));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
)]
|
)]
|
||||||
//! Asset manager ports and transactional preparation models.
|
//! Asset manager ports and transactional preparation models.
|
||||||
|
|
||||||
use fparkan_material::{decode_wear, resolve_material, MaterialError, WEAR_KIND};
|
use fparkan_material::{decode_wear, resolve_material, MaterialError, MAT0_KIND, WEAR_KIND};
|
||||||
use fparkan_mission_format::{decode_tma, decode_tma_land_path};
|
use fparkan_mission_format::{decode_tma, decode_tma_land_path};
|
||||||
pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
|
pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
|
||||||
use fparkan_msh::{decode_msh, validate_msh, MshError};
|
use fparkan_msh::{decode_msh, validate_msh, MshError};
|
||||||
@@ -601,22 +601,71 @@ fn prepared_visual_signature(proto: &EffectivePrototype) -> PreparedVisualSignat
|
|||||||
pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
||||||
repository: &R,
|
repository: &R,
|
||||||
report: &mut PrototypeGraphReport,
|
report: &mut PrototypeGraphReport,
|
||||||
graph: &PrototypeGraph,
|
graph: &mut PrototypeGraph,
|
||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
) {
|
) {
|
||||||
let texture_archive = parse_path(TEXTURES_ARCHIVE).ok();
|
let material_archive = parse_path("material.lib")
|
||||||
let lightmap_archive = parse_path(LIGHTMAP_ARCHIVE).ok();
|
.expect("static material archive path must satisfy host-compatible normalization");
|
||||||
|
let mut next_node = graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node| node.id.0)
|
||||||
|
.max()
|
||||||
|
.map_or(0, |value| value.saturating_add(1));
|
||||||
|
let mut next_edge = graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.map(|edge| edge.id.0)
|
||||||
|
.max()
|
||||||
|
.map_or(0, |value| value.saturating_add(1));
|
||||||
|
|
||||||
for (prototype_index, prototype) in prototypes.iter().enumerate() {
|
for (prototype_index, prototype) in prototypes.iter().enumerate() {
|
||||||
let PrototypeGeometry::Mesh(mesh) = &prototype.geometry else {
|
let PrototypeGeometry::Mesh(mesh) = &prototype.geometry else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
report.mesh_dependency_count += prototype.dependencies.len();
|
|
||||||
report.wear_request_count += 1;
|
report.wear_request_count += 1;
|
||||||
|
let Some(prototype_node_id) = prototype_node_id(graph, prototype_index) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(mesh_node_id) = prototype_mesh_node_id(graph, prototype_node_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mesh_parent_edge = mesh_edge_id(graph, prototype_node_id);
|
||||||
|
let root_index = root_index_for_prototype(graph, prototype_index);
|
||||||
|
|
||||||
match resolve_wear_table(repository, mesh) {
|
match resolve_wear_table(repository, mesh) {
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
report.wear_resolved_count += 1;
|
report.wear_resolved_count += 1;
|
||||||
|
let wear_key = match wear_resource_key(mesh) {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(message) => {
|
||||||
|
push_visual_failure(
|
||||||
|
report,
|
||||||
|
graph,
|
||||||
|
prototype_index,
|
||||||
|
mesh.name.0.clone(),
|
||||||
|
PrototypeGraphEdge::MeshToWear,
|
||||||
|
PrototypeGraphRequiredness::Required,
|
||||||
|
&message.to_string(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let wear_node_id = push_graph_resource_node(
|
||||||
|
graph,
|
||||||
|
PrototypeGraphNodeKind::WearResource,
|
||||||
|
wear_key.clone(),
|
||||||
|
&mut next_node,
|
||||||
|
);
|
||||||
|
let wear_edge_id = push_graph_edge(
|
||||||
|
graph,
|
||||||
|
mesh_node_id,
|
||||||
|
wear_node_id,
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::MeshToWear,
|
||||||
|
PrototypeGraphRequiredness::Required,
|
||||||
|
Some(provenance_for_resource(root_index, mesh_parent_edge, &wear_key)),
|
||||||
|
&mut next_edge,
|
||||||
|
);
|
||||||
report.material_slot_count += table.entries.len();
|
report.material_slot_count += table.entries.len();
|
||||||
for (material_index, _entry) in table.entries.iter().enumerate() {
|
for (material_index, _entry) in table.entries.iter().enumerate() {
|
||||||
let Ok(material_index) = u16::try_from(material_index) else {
|
let Ok(material_index) = u16::try_from(material_index) else {
|
||||||
@@ -634,14 +683,59 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
|||||||
match resolve_material(repository, &table, material_index) {
|
match resolve_material(repository, &table, material_index) {
|
||||||
Ok(material) => {
|
Ok(material) => {
|
||||||
report.material_resolved_count += 1;
|
report.material_resolved_count += 1;
|
||||||
|
let material_key = ResourceKey {
|
||||||
|
archive: material_archive.clone(),
|
||||||
|
name: material.name.clone(),
|
||||||
|
type_id: Some(MAT0_KIND),
|
||||||
|
};
|
||||||
|
let material_node_id = push_graph_resource_node(
|
||||||
|
graph,
|
||||||
|
PrototypeGraphNodeKind::MaterialResource,
|
||||||
|
material_key.clone(),
|
||||||
|
&mut next_node,
|
||||||
|
);
|
||||||
|
let material_edge_id = push_graph_edge(
|
||||||
|
graph,
|
||||||
|
wear_node_id,
|
||||||
|
material_node_id,
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::WearToMaterial,
|
||||||
|
PrototypeGraphRequiredness::Required,
|
||||||
|
Some(provenance_for_resource(
|
||||||
|
root_index,
|
||||||
|
Some(wear_edge_id),
|
||||||
|
&material_key,
|
||||||
|
)),
|
||||||
|
&mut next_edge,
|
||||||
|
);
|
||||||
for texture in material.document.texture_requests() {
|
for texture in material.document.texture_requests() {
|
||||||
report.texture_request_count += 1;
|
report.texture_request_count += 1;
|
||||||
match resolve_texm_from_candidates(
|
match resolve_texture(repository, &texture) {
|
||||||
repository,
|
Ok(()) => {
|
||||||
&texture,
|
report.texture_resolved_count += 1;
|
||||||
[texture_archive.as_ref(), lightmap_archive.as_ref()],
|
if let Ok(texture_key) =
|
||||||
) {
|
texm_resource_key(TEXTURES_ARCHIVE, &texture)
|
||||||
Ok(()) => report.texture_resolved_count += 1,
|
{
|
||||||
|
let texture_node_id = push_graph_resource_node(
|
||||||
|
graph,
|
||||||
|
PrototypeGraphNodeKind::TextureResource,
|
||||||
|
texture_key.clone(),
|
||||||
|
&mut next_node,
|
||||||
|
);
|
||||||
|
push_graph_edge(
|
||||||
|
graph,
|
||||||
|
material_node_id,
|
||||||
|
texture_node_id,
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::MaterialToTexture,
|
||||||
|
PrototypeGraphRequiredness::Required,
|
||||||
|
Some(provenance_for_resource(
|
||||||
|
root_index,
|
||||||
|
Some(material_edge_id),
|
||||||
|
&texture_key,
|
||||||
|
)),
|
||||||
|
&mut next_edge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
let message = message.to_string();
|
let message = message.to_string();
|
||||||
push_visual_failure(
|
push_visual_failure(
|
||||||
@@ -670,12 +764,33 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
for lightmap in &table.lightmaps {
|
for lightmap in &table.lightmaps {
|
||||||
report.lightmap_request_count += 1;
|
report.lightmap_request_count += 1;
|
||||||
match resolve_texm_from_candidates(
|
match resolve_lightmap(repository, &lightmap.lightmap) {
|
||||||
repository,
|
Ok(()) => {
|
||||||
&lightmap.lightmap,
|
report.lightmap_resolved_count += 1;
|
||||||
[lightmap_archive.as_ref(), texture_archive.as_ref()],
|
if let Ok(lightmap_key) =
|
||||||
) {
|
texm_resource_key(LIGHTMAP_ARCHIVE, &lightmap.lightmap)
|
||||||
Ok(()) => report.lightmap_resolved_count += 1,
|
{
|
||||||
|
let lightmap_node_id = push_graph_resource_node(
|
||||||
|
graph,
|
||||||
|
PrototypeGraphNodeKind::LightmapResource,
|
||||||
|
lightmap_key.clone(),
|
||||||
|
&mut next_node,
|
||||||
|
);
|
||||||
|
push_graph_edge(
|
||||||
|
graph,
|
||||||
|
wear_node_id,
|
||||||
|
lightmap_node_id,
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::WearToLightmap,
|
||||||
|
PrototypeGraphRequiredness::Required,
|
||||||
|
Some(provenance_for_resource(
|
||||||
|
root_index,
|
||||||
|
Some(wear_edge_id),
|
||||||
|
&lightmap_key,
|
||||||
|
)),
|
||||||
|
&mut next_edge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
let message = message.to_string();
|
let message = message.to_string();
|
||||||
push_visual_failure(
|
push_visual_failure(
|
||||||
@@ -704,6 +819,89 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_graph_resource_node(
|
||||||
|
graph: &mut PrototypeGraph,
|
||||||
|
kind: PrototypeGraphNodeKind,
|
||||||
|
resource: ResourceKey,
|
||||||
|
next_node: &mut u32,
|
||||||
|
) -> fparkan_prototype::PrototypeGraphNodeId {
|
||||||
|
let id = fparkan_prototype::PrototypeGraphNodeId(*next_node);
|
||||||
|
*next_node = (*next_node).saturating_add(1);
|
||||||
|
graph
|
||||||
|
.nodes
|
||||||
|
.push(fparkan_prototype::PrototypeGraphNode::resource(kind, resource, id));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_graph_edge(
|
||||||
|
graph: &mut PrototypeGraph,
|
||||||
|
from: fparkan_prototype::PrototypeGraphNodeId,
|
||||||
|
to: fparkan_prototype::PrototypeGraphNodeId,
|
||||||
|
kind: fparkan_prototype::PrototypeGraphEdgeKind,
|
||||||
|
requiredness: PrototypeGraphRequiredness,
|
||||||
|
provenance: Option<PrototypeGraphProvenance>,
|
||||||
|
next_edge: &mut u32,
|
||||||
|
) -> 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,
|
||||||
|
});
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prototype_mesh_node_id(
|
||||||
|
graph: &PrototypeGraph,
|
||||||
|
prototype_node: fparkan_prototype::PrototypeGraphNodeId,
|
||||||
|
) -> Option<fparkan_prototype::PrototypeGraphNodeId> {
|
||||||
|
graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.find(|edge| {
|
||||||
|
edge.from == prototype_node
|
||||||
|
&& matches!(
|
||||||
|
edge.kind,
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::PrototypeToMesh
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|edge| edge.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provenance_for_resource(
|
||||||
|
root_index: usize,
|
||||||
|
parent_edge: Option<fparkan_prototype::PrototypeGraphEdgeId>,
|
||||||
|
resource: &ResourceKey,
|
||||||
|
) -> PrototypeGraphProvenance {
|
||||||
|
PrototypeGraphProvenance {
|
||||||
|
root_index,
|
||||||
|
parent_edge,
|
||||||
|
archive: Some(resource.archive.display_lossy().to_string()),
|
||||||
|
resource: Some(resource.name.0.clone()),
|
||||||
|
span: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wear_resource_key(mesh: &ResourceKey) -> Result<ResourceKey, AssetError> {
|
||||||
|
Ok(ResourceKey {
|
||||||
|
archive: mesh.archive.clone(),
|
||||||
|
name: sibling_name(mesh, "wea")?,
|
||||||
|
type_id: Some(WEAR_KIND),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texm_resource_key(archive: &str, name: &ResourceName) -> Result<ResourceKey, AssetError> {
|
||||||
|
Ok(ResourceKey {
|
||||||
|
archive: parse_path(archive)?,
|
||||||
|
name: name.clone(),
|
||||||
|
type_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Validates a prototype visual without resolving cross-resource dependencies.
|
/// Validates a prototype visual without resolving cross-resource dependencies.
|
||||||
///
|
///
|
||||||
/// This is useful for tests and API callers that only need a stable visual id.
|
/// This is useful for tests and API callers that only need a stable visual id.
|
||||||
@@ -913,52 +1111,6 @@ fn resolve_wear_table<R: ResourceRepository>(
|
|||||||
decode_wear(&bytes).map_err(AssetError::Material)
|
decode_wear(&bytes).map_err(AssetError::Material)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_texm_from_candidates<'a, R: ResourceRepository>(
|
|
||||||
repository: &R,
|
|
||||||
texture: &ResourceName,
|
|
||||||
candidates: impl IntoIterator<Item = Option<&'a NormalizedPath>>,
|
|
||||||
) -> Result<(), AssetError> {
|
|
||||||
let mut missing_archive = false;
|
|
||||||
for path in candidates.into_iter().flatten() {
|
|
||||||
let key = ResourceKey {
|
|
||||||
archive: path.to_owned(),
|
|
||||||
name: texture.clone(),
|
|
||||||
type_id: None,
|
|
||||||
};
|
|
||||||
let archive = match repository.open_archive(path) {
|
|
||||||
Ok(archive) => archive,
|
|
||||||
Err(ResourceError::MissingArchive { .. }) => {
|
|
||||||
missing_archive = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(err) => return Err(map_resource_error("texm", &key, err)),
|
|
||||||
};
|
|
||||||
let Some(handle) = repository
|
|
||||||
.find(archive, texture)
|
|
||||||
.map_err(|err| map_resource_error("texm", &key, err))?
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let bytes = repository
|
|
||||||
.read(handle)
|
|
||||||
.map_err(|err| map_resource_error("texm", &key, err))?
|
|
||||||
.into_owned();
|
|
||||||
decode_texm(Arc::from(bytes)).map_err(AssetError::Texture)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if missing_archive {
|
|
||||||
Err(AssetError::MissingDependency(format!(
|
|
||||||
"texm archive missing for {}",
|
|
||||||
String::from_utf8_lossy(&texture.0)
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Err(AssetError::MissingDependency(format!(
|
|
||||||
"missing texm {}",
|
|
||||||
String::from_utf8_lossy(&texture.0)
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_visual_failure(
|
fn push_visual_failure(
|
||||||
report: &mut PrototypeGraphReport,
|
report: &mut PrototypeGraphReport,
|
||||||
graph: &PrototypeGraph,
|
graph: &PrototypeGraph,
|
||||||
@@ -1298,6 +1450,185 @@ mod tests {
|
|||||||
assert_ne!(prepared_visual_signature(&first), prepared_visual_signature(&second));
|
assert_ne!(prepared_visual_signature(&first), prepared_visual_signature(&second));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graph_materializes_visual_dependency_nodes_and_edges() {
|
||||||
|
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.clone()),
|
||||||
|
source: fparkan_prototype::PrototypeSource::DirectArchive,
|
||||||
|
dependencies: vec![mesh_key.clone()],
|
||||||
|
};
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| node.kind == PrototypeGraphNodeKind::WearResource));
|
||||||
|
assert!(graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| node.kind == PrototypeGraphNodeKind::MaterialResource));
|
||||||
|
assert!(graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| node.kind == PrototypeGraphNodeKind::TextureResource));
|
||||||
|
assert!(graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| node.kind == PrototypeGraphNodeKind::LightmapResource));
|
||||||
|
assert!(graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::MeshToWear));
|
||||||
|
assert!(graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::WearToMaterial));
|
||||||
|
assert!(graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::MaterialToTexture));
|
||||||
|
assert!(graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|edge| edge.kind == fparkan_prototype::PrototypeGraphEdgeKind::WearToLightmap));
|
||||||
|
assert_eq!(report.wear_request_count, 1);
|
||||||
|
assert_eq!(report.wear_resolved_count, 1);
|
||||||
|
assert_eq!(report.material_slot_count, 1);
|
||||||
|
assert_eq!(report.material_resolved_count, 1);
|
||||||
|
assert_eq!(report.texture_request_count, 1);
|
||||||
|
assert_eq!(report.texture_resolved_count, 1);
|
||||||
|
assert_eq!(report.lightmap_request_count, 1);
|
||||||
|
assert_eq!(report.lightmap_resolved_count, 1);
|
||||||
|
assert!(report.failures.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graph_report_uses_strict_texture_archive_policy() {
|
||||||
|
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 repo = repository_with_archives_meta(&[
|
||||||
|
(
|
||||||
|
"static.rlb",
|
||||||
|
&[TestNresEntry {
|
||||||
|
name: b"tree.wea",
|
||||||
|
payload: b"1\n0 MAT_A\n",
|
||||||
|
type_id: WEAR_KIND,
|
||||||
|
attr2: 0,
|
||||||
|
}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"material.lib",
|
||||||
|
&[TestNresEntry {
|
||||||
|
name: b"MAT_A",
|
||||||
|
payload: &mat0,
|
||||||
|
type_id: MAT0_KIND,
|
||||||
|
attr2: 0,
|
||||||
|
}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
LIGHTMAP_ARCHIVE,
|
||||||
|
&[TestNresEntry {
|
||||||
|
name: b"TEX_A",
|
||||||
|
payload: &texm,
|
||||||
|
type_id: 0,
|
||||||
|
attr2: 0,
|
||||||
|
}],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let prototype = EffectivePrototype {
|
||||||
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree")),
|
||||||
|
geometry: PrototypeGeometry::Mesh(mesh_key.clone()),
|
||||||
|
source: fparkan_prototype::PrototypeSource::DirectArchive,
|
||||||
|
dependencies: vec![mesh_key.clone()],
|
||||||
|
};
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn prepares_real_unit_asset_plan() {
|
fn prepares_real_unit_asset_plan() {
|
||||||
@@ -1375,6 +1706,25 @@ mod tests {
|
|||||||
CachedResourceRepository::new(Arc::new(vfs))
|
CachedResourceRepository::new(Arc::new(vfs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct TestNresEntry<'a> {
|
||||||
|
name: &'a [u8],
|
||||||
|
payload: &'a [u8],
|
||||||
|
type_id: u32,
|
||||||
|
attr2: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository_with_archives_meta(
|
||||||
|
archives: &[(&str, &[TestNresEntry<'_>])],
|
||||||
|
) -> CachedResourceRepository {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
CachedResourceRepository::new(Arc::new(vfs))
|
||||||
|
}
|
||||||
|
|
||||||
fn texm_payload() -> Vec<u8> {
|
fn texm_payload() -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
out.extend_from_slice(&0x6d78_6554_u32.to_le_bytes());
|
out.extend_from_slice(&0x6d78_6554_u32.to_le_bytes());
|
||||||
@@ -1389,6 +1739,58 @@ mod tests {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mat0_with_texture(texture: &[u8]) -> Vec<u8> {
|
||||||
|
let mut bytes = vec![0; 4 + 34];
|
||||||
|
bytes[0..2].copy_from_slice(&1_u16.to_le_bytes());
|
||||||
|
let len = texture.len().min(16);
|
||||||
|
bytes[22..22 + len].copy_from_slice(&texture[..len]);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prototype_graph_for_mesh(prototype: &EffectivePrototype) -> PrototypeGraph {
|
||||||
|
let root_node = fparkan_prototype::PrototypeGraphNode::root(
|
||||||
|
fparkan_prototype::PrototypeKey(prototype.key.0.clone()),
|
||||||
|
false,
|
||||||
|
fparkan_prototype::PrototypeGraphNodeId(0),
|
||||||
|
);
|
||||||
|
let prototype_node = fparkan_prototype::PrototypeGraphNode::prototype(
|
||||||
|
prototype.key.clone(),
|
||||||
|
fparkan_prototype::PrototypeGraphNodeId(1),
|
||||||
|
);
|
||||||
|
let mesh_key = match &prototype.geometry {
|
||||||
|
PrototypeGeometry::Mesh(mesh) => mesh.clone(),
|
||||||
|
PrototypeGeometry::NonGeometric => panic!("mesh prototype expected"),
|
||||||
|
};
|
||||||
|
let mesh_node = fparkan_prototype::PrototypeGraphNode::mesh(
|
||||||
|
mesh_key,
|
||||||
|
fparkan_prototype::PrototypeGraphNodeId(2),
|
||||||
|
);
|
||||||
|
PrototypeGraph {
|
||||||
|
roots: vec![prototype.key.clone()],
|
||||||
|
prototype_requests: vec![prototype.key.clone()],
|
||||||
|
root_prototype_request_spans: std::iter::once(0..1).collect(),
|
||||||
|
nodes: vec![root_node, prototype_node, mesh_node],
|
||||||
|
edges: vec![
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeInstance {
|
||||||
|
id: fparkan_prototype::PrototypeGraphEdgeId(0),
|
||||||
|
from: fparkan_prototype::PrototypeGraphNodeId(0),
|
||||||
|
to: fparkan_prototype::PrototypeGraphNodeId(1),
|
||||||
|
kind: fparkan_prototype::PrototypeGraphEdgeKind::MissionToRoot,
|
||||||
|
requiredness: PrototypeGraphRequiredness::Required,
|
||||||
|
provenance: None,
|
||||||
|
},
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeInstance {
|
||||||
|
id: fparkan_prototype::PrototypeGraphEdgeId(1),
|
||||||
|
from: fparkan_prototype::PrototypeGraphNodeId(1),
|
||||||
|
to: fparkan_prototype::PrototypeGraphNodeId(2),
|
||||||
|
kind: fparkan_prototype::PrototypeGraphEdgeKind::PrototypeToMesh,
|
||||||
|
requiredness: PrototypeGraphRequiredness::Required,
|
||||||
|
provenance: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_nres(entries: &[(&[u8], &[u8])]) -> Vec<u8> {
|
fn build_nres(entries: &[(&[u8], &[u8])]) -> Vec<u8> {
|
||||||
let mut out = vec![0; 16];
|
let mut out = vec![0; 16];
|
||||||
let mut offsets = Vec::with_capacity(entries.len());
|
let mut offsets = Vec::with_capacity(entries.len());
|
||||||
@@ -1421,6 +1823,38 @@ mod tests {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_nres_with_meta(entries: &[TestNresEntry<'_>]) -> Vec<u8> {
|
||||||
|
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, entry.attr2);
|
||||||
|
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());
|
||||||
|
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 push_u32(out: &mut Vec<u8>, value: u32) {
|
fn push_u32(out: &mut Vec<u8>, value: u32) {
|
||||||
out.extend_from_slice(&value.to_le_bytes());
|
out.extend_from_slice(&value.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,16 @@ pub enum PrototypeGraphNodeKind {
|
|||||||
Prototype,
|
Prototype,
|
||||||
/// Mesh dependency.
|
/// Mesh dependency.
|
||||||
MeshResource,
|
MeshResource,
|
||||||
|
/// WEAR dependency.
|
||||||
|
WearResource,
|
||||||
|
/// MAT0 dependency.
|
||||||
|
MaterialResource,
|
||||||
|
/// TEXM texture dependency.
|
||||||
|
TextureResource,
|
||||||
|
/// TEXM lightmap dependency.
|
||||||
|
LightmapResource,
|
||||||
|
/// Effect dependency placeholder for later stages.
|
||||||
|
EffectResource,
|
||||||
/// Non-geometric prototype.
|
/// Non-geometric prototype.
|
||||||
NonGeometric,
|
NonGeometric,
|
||||||
}
|
}
|
||||||
@@ -197,6 +207,17 @@ pub struct PrototypeGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PrototypeGraphNode {
|
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.
|
/// Creates a mesh resource node.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn mesh(resource: ResourceKey, id: PrototypeGraphNodeId) -> Self {
|
pub const fn mesh(resource: ResourceKey, id: PrototypeGraphNodeId) -> Self {
|
||||||
@@ -244,6 +265,16 @@ pub enum PrototypeGraphEdgeKind {
|
|||||||
UnitDatToComponent,
|
UnitDatToComponent,
|
||||||
/// Prototype to mesh dependency.
|
/// Prototype to mesh dependency.
|
||||||
PrototypeToMesh,
|
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,
|
||||||
|
/// MAT0 material to effect dependency.
|
||||||
|
MaterialToEffect,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prototype graph edge record.
|
/// Prototype graph edge record.
|
||||||
|
|||||||
@@ -516,12 +516,12 @@ fn load_mission_with_options(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|object| resource_name(&object.resource_name.raw))
|
.map(|object| resource_name(&object.resource_name.raw))
|
||||||
.collect();
|
.collect();
|
||||||
let (prototype_graph, resolved_prototypes, mut prototype_report) =
|
let (mut prototype_graph, resolved_prototypes, mut prototype_report) =
|
||||||
build_prototype_graph_report(&repository, vfs.as_ref(), &graph_roots);
|
build_prototype_graph_report(&repository, vfs.as_ref(), &graph_roots);
|
||||||
extend_graph_report_with_visual_dependencies(
|
extend_graph_report_with_visual_dependencies(
|
||||||
&repository,
|
&repository,
|
||||||
&mut prototype_report,
|
&mut prototype_report,
|
||||||
&prototype_graph,
|
&mut prototype_graph,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
);
|
);
|
||||||
if !prototype_report.is_success() {
|
if !prototype_report.is_success() {
|
||||||
|
|||||||
Reference in New Issue
Block a user