feat(prototype): retain unit component provenance

This commit is contained in:
2026-07-18 20:15:26 +04:00
parent b7a102caa7
commit d7150a58d5
6 changed files with 60 additions and 0 deletions
+3
View File
@@ -1810,6 +1810,7 @@ mod tests {
scale: [2.0, 3.0, 4.0],
visual_ids: Vec::new(),
properties: Vec::new(),
unit_components: Vec::new(),
};
assert_eq!(
mission_position_scale_transform(&draft),
@@ -1850,6 +1851,7 @@ mod tests {
scale: [1.0; 3],
visual_ids: Vec::new(),
properties: Vec::new(),
unit_components: Vec::new(),
}];
let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?;
@@ -1887,6 +1889,7 @@ mod tests {
scale: [1.0; 3],
visual_ids: Vec::new(),
properties: Vec::new(),
unit_components: Vec::new(),
}];
let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?;
+1
View File
@@ -3528,6 +3528,7 @@ mod tests {
roots: vec![prototype.key.clone()],
prototype_requests: vec![prototype.key.clone()],
root_prototype_request_spans: std::iter::once(0..1).collect(),
root_unit_components: vec![Vec::new()],
visual_dependencies_expanded: false,
nodes: vec![root_node, prototype_node, mesh_node],
edges: vec![
+30
View File
@@ -128,6 +128,13 @@ pub struct PrototypeGraph {
pub prototype_requests: Vec<PrototypeKey>,
/// Mission object-local spans of effective prototype requests.
pub root_prototype_request_spans: Vec<std::ops::Range<usize>>,
/// Ordered raw unit DAT component records for each mission root.
///
/// This vector is aligned with [`Self::roots`]. Direct roots and legacy
/// unit bindings have an empty record list. Records are preserved without
/// assigning runtime semantics to their `kind`, links, descriptions, or
/// opaque tails.
pub root_unit_components: Vec<Vec<UnitComponentRecord>>,
/// Whether visual dependency expansion has already been applied.
pub visual_dependencies_expanded: bool,
/// Materialized prototype dependency graph nodes.
@@ -637,6 +644,7 @@ fn resolve_direct_prototype(
struct ResolvedPrototypeRequests {
expected_count: usize,
prototypes: Vec<EffectivePrototype>,
unit_components: Vec<UnitComponentRecord>,
}
fn resolve_prototype_requests(
@@ -652,6 +660,7 @@ fn resolve_prototype_requests(
Ok(ResolvedPrototypeRequests {
expected_count: 1,
prototypes: prototype.into_iter().collect(),
unit_components: Vec::new(),
})
}
@@ -686,6 +695,7 @@ fn resolve_unit_dat_prototype_requests(
return Ok(ResolvedPrototypeRequests {
expected_count: unit.records.len(),
prototypes,
unit_components: unit.records,
});
}
@@ -697,6 +707,7 @@ fn resolve_unit_dat_prototype_requests(
Ok(ResolvedPrototypeRequests {
expected_count: 1,
prototypes: prototype.into_iter().collect(),
unit_components: Vec::new(),
})
}
@@ -741,6 +752,9 @@ pub fn build_prototype_graph(
graph.roots.push(key);
let start = graph.prototype_requests.len();
let expansion = resolve_prototype_requests(repository, vfs, root)?;
graph
.root_unit_components
.push(expansion.unit_components.clone());
let root_provenance = provenance_for_root(root_index, root);
for prototype in expansion.prototypes {
let prototype_node = PrototypeGraphNode::prototype(
@@ -841,6 +855,9 @@ pub fn build_prototype_graph_report(
match resolve_prototype_requests(repository, vfs, root) {
Ok(expansion) => {
let expected = expansion.expected_count;
graph
.root_unit_components
.push(expansion.unit_components.clone());
if edge == PrototypeGraphEdge::MissionToUnitDat {
report.unit_component_count += expected;
}
@@ -929,6 +946,9 @@ pub fn build_prototype_graph_report(
}),
}),
}
if graph.root_unit_components.len() <= root_index {
graph.root_unit_components.push(Vec::new());
}
let end = graph.prototype_requests.len();
graph.root_prototype_request_spans.push(start..end);
}
@@ -1770,6 +1790,16 @@ mod tests {
assert_eq!(graph.prototype_requests.len(), 2);
assert_eq!(graph.prototype_requests[0].0 .0, b"component_a");
assert_eq!(graph.prototype_requests[1].0 .0, b"component_b");
assert_eq!(graph.root_unit_components.len(), 1);
assert_eq!(graph.root_unit_components[0].len(), 2);
assert_eq!(
cstr_bytes(&graph.root_unit_components[0][0].resource_raw),
b"component_a"
);
assert_eq!(
cstr_bytes(&graph.root_unit_components[0][1].resource_raw),
b"component_b"
);
assert_eq!(resolved.len(), 2);
assert_eq!(report.unit_reference_count, 1);
assert_eq!(report.unit_component_count, 2);
+13
View File
@@ -31,6 +31,7 @@ use fparkan_assets::{
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
use fparkan_prototype::{
build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport,
UnitComponentRecord,
};
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_terrain::SurfaceQuery;
@@ -229,6 +230,11 @@ pub struct MissionObjectDraft {
pub visual_ids: Vec<AssetId<PreparedVisual>>,
/// Ordered mission properties.
pub properties: Vec<MissionObjectProperty>,
/// Ordered raw Unit DAT components that selected this mission root.
///
/// The records preserve source provenance for future controller loading;
/// their fields intentionally have no inferred Control/physics semantics.
pub unit_components: Vec<UnitComponentRecord>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -912,6 +918,11 @@ fn load_mission_with_options_and_progress(
name_raw: property.name_raw.clone(),
})
.collect(),
unit_components: prototype_graph
.root_unit_components
.get(index)
.cloned()
.unwrap_or_default(),
})
.collect();
let mut new_runtime_world = new_world(WorldConfig);
@@ -1608,6 +1619,7 @@ mod tests {
.expect("load mission");
let drafts = loaded_mission_object_drafts(&engine).expect("object drafts");
let assets = loaded_mission_assets(&engine).expect("mission assets");
let graph = loaded_prototype_graph(&engine).expect("prototype graph");
assert_eq!(drafts.len(), loaded.object_count);
assert!(drafts.iter().any(|draft| !draft.visual_ids.is_empty()));
@@ -1617,6 +1629,7 @@ mod tests {
u32::try_from(index).ok().map(OriginalObjectId)
);
assert_eq!(draft.visual_ids, assets.visuals_for_object(index));
assert_eq!(draft.unit_components, graph.root_unit_components[index]);
assert!(draft.position.iter().all(|component| component.is_finite()));
assert!(draft
.orientation_raw
+7
View File
@@ -468,6 +468,13 @@ string длиной 32 байта. Требование обязательног
Части 2. Все соответствуют формуле размера, `kind == 1` и
`archive_name == "objects.rlb"`.
При построении mission prototype graph FParkan сохраняет для каждого root
упорядоченный список этих raw records. Список передаётся в
`MissionObjectDraft` без преобразования `kind`, `parent_or_link`, description
или tails в предполагаемые роли. Так runtime уже располагает исходными данными
составного unit для будущих Control/physics/AniMesh consumers, но не выдаёт
структурное сходство за доказанную семантику.
## Вспомогательные форматы
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
+6
View File
@@ -276,6 +276,12 @@ component как ordered raw-string/resource provenance, но не для при
этим строкам смысловых имён до трассировки private update methods. Extractor:
`tools/ghidra/ExportAniMeshControlCaller.java`.
Runtime сохраняет ordered raw Unit DAT records рядом с каждым mission object
draft. Это создаёт проверяемую границу передачи данных от loader-а к будущему
Control consumer-у: никакой компонент пока не получает имя `Control` только
по `kind`, `parent_or_link` или description; semantic binding появится лишь
после trace private update/load methods.
### Control system и physical model
`LoadControlSystem` загружает настройки controller-а: ограничения скорости,