aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 19:23:53 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 19:23:53 +0300
commit6e5ee47d5192ddeb9e19cfb1dc072fe2e25123a5 (patch)
treee5f9c6b9fbe08df924b29c687f7ad3c5c7293f92
parentd7150a58d53786580fed00a3d4a0e9d2406fe9bb (diff)
downloadfparkan-6e5ee47d5192ddeb9e19cfb1dc072fe2e25123a5.tar.xz
fparkan-6e5ee47d5192ddeb9e19cfb1dc072fe2e25123a5.zip
feat(prototype): trace unit component edges
-rw-r--r--crates/fparkan-assets/src/lib.rs2
-rw-r--r--crates/fparkan-prototype/src/lib.rs66
-rw-r--r--docs/tomes/03-resources.md5
3 files changed, 67 insertions, 6 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 99de160..35380ef 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1627,6 +1627,7 @@ fn provenance_for_resource(
PrototypeGraphProvenance {
root_index,
parent_edge,
+ unit_component_index: None,
archive: Some(resource.archive.display_lossy().to_string()),
resource: Some(resource.name.0.clone()),
span: None,
@@ -2020,6 +2021,7 @@ fn push_visual_failure(
provenance: Some(PrototypeGraphProvenance {
root_index,
parent_edge,
+ unit_component_index: None,
archive: dependency.map(|resource| resource.archive.as_str().to_string()),
resource: Some(resource_raw),
span: None,
diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs
index 4010e6f..f4540d4 100644
--- a/crates/fparkan-prototype/src/lib.rs
+++ b/crates/fparkan-prototype/src/lib.rs
@@ -169,6 +169,12 @@ pub struct PrototypeGraphProvenance {
pub root_index: usize,
/// Immediate parent edge that discovered this edge.
pub parent_edge: Option<PrototypeGraphEdgeId>,
+ /// 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<usize>,
/// Source archive when available.
pub archive: Option<String>,
/// Source resource key when available.
@@ -755,8 +761,12 @@ pub fn build_prototype_graph(
graph
.root_unit_components
.push(expansion.unit_components.clone());
- let root_provenance = provenance_for_root(root_index, root);
- for prototype in expansion.prototypes {
+ 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),
@@ -795,6 +805,7 @@ pub fn build_prototype_graph(
provenance: Some(provenance_for_mesh(
root_index,
root_to_prototype_edge_id,
+ unit_component_index,
dependency,
)),
});
@@ -850,8 +861,6 @@ pub fn build_prototype_graph_report(
root_node,
));
let start = graph.prototype_requests.len();
- let root_provenance = provenance_for_root(root_index, root);
-
match resolve_prototype_requests(repository, vfs, root) {
Ok(expansion) => {
let expected = expansion.expected_count;
@@ -862,7 +871,13 @@ pub fn build_prototype_graph_report(
report.unit_component_count += expected;
}
let actual = expansion.prototypes.len();
- for prototype in expansion.prototypes {
+ 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),
@@ -903,6 +918,7 @@ pub fn build_prototype_graph_report(
provenance: Some(provenance_for_mesh(
root_index,
root_to_prototype_edge_id,
+ unit_component_index,
dependency,
)),
});
@@ -924,6 +940,7 @@ pub fn build_prototype_graph_report(
provenance: Some(PrototypeGraphProvenance {
root_index,
parent_edge: None,
+ unit_component_index: None,
archive: None,
resource: Some(root.0.clone()),
span: None,
@@ -940,6 +957,7 @@ pub fn build_prototype_graph_report(
provenance: Some(PrototypeGraphProvenance {
root_index,
parent_edge: None,
+ unit_component_index: None,
archive: None,
resource: Some(root.0.clone()),
span: None,
@@ -984,10 +1002,15 @@ fn graph_error_edge(edge: PrototypeGraphEdge, err: &PrototypeError) -> Prototype
}
}
-fn provenance_for_root(root_index: usize, root: &ResourceName) -> PrototypeGraphProvenance {
+fn provenance_for_root(
+ root_index: usize,
+ root: &ResourceName,
+ unit_component_index: Option<usize>,
+) -> PrototypeGraphProvenance {
PrototypeGraphProvenance {
root_index,
parent_edge: None,
+ unit_component_index,
archive: None,
resource: Some(root.0.clone()),
span: None,
@@ -997,11 +1020,13 @@ fn provenance_for_root(root_index: usize, root: &ResourceName) -> PrototypeGraph
fn provenance_for_mesh(
root_index: usize,
parent_edge: PrototypeGraphEdgeId,
+ unit_component_index: Option<usize>,
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,
@@ -1800,6 +1825,35 @@ mod tests {
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);
diff --git a/docs/tomes/03-resources.md b/docs/tomes/03-resources.md
index 94c22c2..a2386ba 100644
--- a/docs/tomes/03-resources.md
+++ b/docs/tomes/03-resources.md
@@ -475,6 +475,11 @@ string длиной 32 байта. Требование обязательног
составного unit для будущих Control/physics/AniMesh consumers, но не выдаёт
структурное сходство за доказанную семантику.
+Каждый graph edge от `UnitDatRoot` к effective component и далее к его MSH
+dependency также несёт `unit_component_index`. Это индекс исходной записи в
+порядке файла, а не тип компонента; по нему diagnostics и будущие consumers
+могут однозначно вернуться к raw 112-byte record.
+
## Вспомогательные форматы
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип