From d7150a58d53786580fed00a3d4a0e9d2406fe9bb Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 20:15:26 +0400 Subject: [PATCH] feat(prototype): retain unit component provenance --- apps/fparkan-game/src/main.rs | 3 +++ crates/fparkan-assets/src/lib.rs | 1 + crates/fparkan-prototype/src/lib.rs | 30 +++++++++++++++++++++++++++++ crates/fparkan-runtime/src/lib.rs | 13 +++++++++++++ docs/tomes/03-resources.md | 7 +++++++ docs/tomes/06-behavior.md | 6 ++++++ 6 files changed, 60 insertions(+) diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 90effa7..7f059af 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -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))?; diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 5bb3edf..99de160 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -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![ diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs index 6570a95..4010e6f 100644 --- a/crates/fparkan-prototype/src/lib.rs +++ b/crates/fparkan-prototype/src/lib.rs @@ -128,6 +128,13 @@ pub struct PrototypeGraph { pub prototype_requests: Vec, /// Mission object-local spans of effective prototype requests. pub root_prototype_request_spans: Vec>, + /// 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>, /// 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, + unit_components: Vec, } 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); diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 740ad34..696bb33 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -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>, /// Ordered mission properties. pub properties: Vec, + /// 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, } #[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 diff --git a/docs/tomes/03-resources.md b/docs/tomes/03-resources.md index 7bce1a1..94c22c2 100644 --- a/docs/tomes/03-resources.md +++ b/docs/tomes/03-resources.md @@ -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, материал и текстура отвечают за видимую форму. Полноценный прототип diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index a201c4b..5764bb0 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -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-а: ограничения скорости,