feat(assets): load native BAS geometry for placements
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
This commit is contained in:
3 files changed
+717
-21
No files matched your search
@@ -16,9 +16,9 @@ use fparkan_nres::{decode as decode_nres, ReadProfile};
|
|||||||
pub use fparkan_nres::{NresDocument, NresError};
|
pub use fparkan_nres::{NresDocument, NresError};
|
||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy, ResourceName};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy, ResourceName};
|
||||||
use fparkan_prototype::{
|
use fparkan_prototype::{
|
||||||
EffectivePrototype, PrototypeGeometry, PrototypeGraph, PrototypeGraphEdge,
|
decode_bas_geometry, BasGeometry, EffectivePrototype, PrototypeGeometry, PrototypeGraph,
|
||||||
PrototypeGraphFailure, PrototypeGraphNodeKind, PrototypeGraphProvenance, PrototypeGraphReport,
|
PrototypeGraphEdge, PrototypeGraphFailure, PrototypeGraphNodeKind, PrototypeGraphProvenance,
|
||||||
PrototypeGraphRequiredness,
|
PrototypeGraphReport, PrototypeGraphRequiredness,
|
||||||
};
|
};
|
||||||
use fparkan_resource::{CachedResourceRepository, ResourceError, ResourceKey, ResourceRepository};
|
use fparkan_resource::{CachedResourceRepository, ResourceError, ResourceKey, ResourceRepository};
|
||||||
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
||||||
@@ -918,6 +918,17 @@ pub struct PreparedComponentControl {
|
|||||||
pub point_list: Option<PreparedControlPointList>,
|
pub point_list: Option<PreparedControlPointList>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Component-local BAS geometry loaded from an explicitly referenced BAS
|
||||||
|
/// resource. This remains aligned with each component instance, not the shared
|
||||||
|
/// model cache.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PreparedComponentBas {
|
||||||
|
/// BAS resource that supplied this geometry.
|
||||||
|
pub source: ResourceKey,
|
||||||
|
/// Decoded BAS point sections.
|
||||||
|
pub geometry: BasGeometry,
|
||||||
|
}
|
||||||
|
|
||||||
/// Raw and decoded fields from one 36-byte CTLD animation row.
|
/// Raw and decoded fields from one 36-byte CTLD animation row.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct PreparedControlTimelineRow {
|
pub struct PreparedControlTimelineRow {
|
||||||
@@ -1570,6 +1581,10 @@ pub struct MissionAssets {
|
|||||||
/// component list. `None` means no CTLD pose override is available because
|
/// component list. `None` means no CTLD pose override is available because
|
||||||
/// there is no explicit reference or its referenced archive/entry is absent.
|
/// there is no explicit reference or its referenced archive/entry is absent.
|
||||||
pub object_component_controls: Vec<Vec<Option<PreparedComponentControl>>>,
|
pub object_component_controls: Vec<Vec<Option<PreparedComponentControl>>>,
|
||||||
|
/// Optional component-local BAS geometry aligned with each object's visual
|
||||||
|
/// component list. `None` means there is no explicit BAS reference or its
|
||||||
|
/// archive/entry is absent.
|
||||||
|
pub object_component_bas: Vec<Vec<Option<PreparedComponentBas>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MissionAssets {
|
impl MissionAssets {
|
||||||
@@ -2007,7 +2022,15 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut prototype_component_controls = Vec::with_capacity(prototypes.len());
|
let mut prototype_component_controls = Vec::with_capacity(prototypes.len());
|
||||||
|
let mut prototype_component_bas = Vec::with_capacity(prototypes.len());
|
||||||
for (prototype_index, prototype) in prototypes.iter().enumerate() {
|
for (prototype_index, prototype) in prototypes.iter().enumerate() {
|
||||||
|
let bas = match prototype.bas_resource.clone() {
|
||||||
|
Some(source) => {
|
||||||
|
prepare_component_bas_from_resource(repository, source, prototype_index)?
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
prototype_component_bas.push(bas);
|
||||||
let Some(source) = prototype.control_resource.clone() else {
|
let Some(source) = prototype.control_resource.clone() else {
|
||||||
prototype_component_controls.push(None);
|
prototype_component_controls.push(None);
|
||||||
continue;
|
continue;
|
||||||
@@ -2040,6 +2063,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
|
|
||||||
let mut object_visuals = Vec::with_capacity(root_prototype_spans.len());
|
let mut object_visuals = Vec::with_capacity(root_prototype_spans.len());
|
||||||
let mut object_component_controls = Vec::with_capacity(root_prototype_spans.len());
|
let mut object_component_controls = Vec::with_capacity(root_prototype_spans.len());
|
||||||
|
let mut object_component_bas = Vec::with_capacity(root_prototype_spans.len());
|
||||||
for (root_index, span) in root_prototype_spans.iter().enumerate() {
|
for (root_index, span) in root_prototype_spans.iter().enumerate() {
|
||||||
if span.start > span.end || span.end > prototype_visual_ids.len() {
|
if span.start > span.end || span.end > prototype_visual_ids.len() {
|
||||||
return Err(AssetError::InvalidPrototype(format!(
|
return Err(AssetError::InvalidPrototype(format!(
|
||||||
@@ -2048,6 +2072,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
let mut ids = Vec::new();
|
let mut ids = Vec::new();
|
||||||
let mut controls = Vec::new();
|
let mut controls = Vec::new();
|
||||||
|
let mut bases = Vec::new();
|
||||||
for index in span.clone() {
|
for index in span.clone() {
|
||||||
let visual_id = prototype_visual_ids[index];
|
let visual_id = prototype_visual_ids[index];
|
||||||
// A unit DAT is an instance tree. Reusing one prepared visual
|
// A unit DAT is an instance tree. Reusing one prepared visual
|
||||||
@@ -2055,9 +2080,11 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
// identical cannons on w_m_wlk2).
|
// identical cannons on w_m_wlk2).
|
||||||
ids.push(visual_id);
|
ids.push(visual_id);
|
||||||
controls.push(prototype_component_controls[index].clone());
|
controls.push(prototype_component_controls[index].clone());
|
||||||
|
bases.push(prototype_component_bas[index].clone());
|
||||||
}
|
}
|
||||||
object_visuals.push(ids);
|
object_visuals.push(ids);
|
||||||
object_component_controls.push(controls);
|
object_component_controls.push(controls);
|
||||||
|
object_component_bas.push(bases);
|
||||||
}
|
}
|
||||||
|
|
||||||
let assets = MissionAssets {
|
let assets = MissionAssets {
|
||||||
@@ -2068,6 +2095,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
visuals,
|
visuals,
|
||||||
object_visuals,
|
object_visuals,
|
||||||
object_component_controls,
|
object_component_controls,
|
||||||
|
object_component_bas,
|
||||||
};
|
};
|
||||||
let report = AssetPreparationReport {
|
let report = AssetPreparationReport {
|
||||||
model_count: assets.models.len(),
|
model_count: assets.models.len(),
|
||||||
@@ -3372,6 +3400,25 @@ fn prepare_component_control_from_resource<R: ResourceRepository>(
|
|||||||
Ok(Some(control))
|
Ok(Some(control))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_component_bas_from_resource<R: ResourceRepository>(
|
||||||
|
repository: &R,
|
||||||
|
source: ResourceKey,
|
||||||
|
prototype_index: usize,
|
||||||
|
) -> Result<Option<PreparedComponentBas>, AssetError> {
|
||||||
|
let Some(payload) = read_optional_key(repository, &source, Some("component BAS geometry"))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let geometry = decode_bas_geometry(&payload).map_err(|error| {
|
||||||
|
AssetError::InvalidPrototype(format!(
|
||||||
|
"prototype {prototype_index} BAS geometry {}:{}: {error}",
|
||||||
|
source.archive.as_str(),
|
||||||
|
String::from_utf8_lossy(&source.name.0),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(Some(PreparedComponentBas { source, geometry }))
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_component_node_data_table(
|
fn decode_component_node_data_table(
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
source: ResourceKey,
|
source: ResourceKey,
|
||||||
@@ -4617,6 +4664,89 @@ mod tests {
|
|||||||
assert!(decode_component_point_list(&non_finite, source, 0).is_err());
|
assert!(decode_component_point_list(&non_finite, source, 0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mission_assets_keep_explicit_bas_geometry_at_component_index() {
|
||||||
|
let mut bas = Vec::new();
|
||||||
|
bas.extend_from_slice(&1_u32.to_le_bytes()); // first-section records
|
||||||
|
bas.extend_from_slice(&1_u32.to_le_bytes()); // declared count
|
||||||
|
for point in [[1.0_f32, 2.0, 3.0], [4.0, 5.0, 6.0]] {
|
||||||
|
for value in point {
|
||||||
|
bas.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bas.extend_from_slice(&0x1122_3344_u32.to_le_bytes());
|
||||||
|
bas.extend_from_slice(&0x5566_7788_u32.to_le_bytes());
|
||||||
|
bas.extend_from_slice(&1_u32.to_le_bytes()); // second-section records
|
||||||
|
bas.extend_from_slice(&1_u32.to_le_bytes());
|
||||||
|
for point in [[7.0_f32, 8.0, 9.0], [10.0, 11.0, 12.0]] {
|
||||||
|
for value in point {
|
||||||
|
bas.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let repository = repository_with_archives(&[("fortif.rlb", &[(b"fr_b_plant.bas", &bas)])]);
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("fortif.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"fr_b_plant.bas"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let without_bas = EffectivePrototype {
|
||||||
|
key: fparkan_prototype::PrototypeKey(resource_name(b"unrelated")),
|
||||||
|
geometry: PrototypeGeometry::NonGeometric,
|
||||||
|
source: fparkan_prototype::PrototypeSource::ObjectsRegistry,
|
||||||
|
source_type: None,
|
||||||
|
dependencies: Vec::new(),
|
||||||
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
|
};
|
||||||
|
let prototype = EffectivePrototype {
|
||||||
|
key: fparkan_prototype::PrototypeKey(resource_name(b"fr_b_plant")),
|
||||||
|
geometry: PrototypeGeometry::NonGeometric,
|
||||||
|
source: fparkan_prototype::PrototypeSource::ObjectsRegistry,
|
||||||
|
source_type: None,
|
||||||
|
dependencies: Vec::new(),
|
||||||
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
|
node_data_resource: None,
|
||||||
|
bas_resource: Some(source.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let assets =
|
||||||
|
prepare_mission_assets_with_repository(&repository, &[0..2], &[without_bas, prototype])
|
||||||
|
.expect("mission BAS sidecar");
|
||||||
|
assert_eq!(assets.object_component_bas[0].len(), 2);
|
||||||
|
assert!(assets.object_component_bas[0][0].is_none());
|
||||||
|
let prepared = assets.object_component_bas[0][1]
|
||||||
|
.as_ref()
|
||||||
|
.expect("BAS aligned with component one");
|
||||||
|
assert_eq!(prepared.source, source);
|
||||||
|
assert_eq!(prepared.geometry.first[0].points[1], [4.0, 5.0, 6.0]);
|
||||||
|
assert_eq!(prepared.geometry.first[0].opaque_words_a, [0x1122_3344]);
|
||||||
|
assert_eq!(prepared.geometry.second[0].points[1], [10.0, 11.0, 12.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_explicit_bas_geometry_is_reported_at_preparation_boundary() {
|
||||||
|
let bad_payload = u32::MAX.to_le_bytes();
|
||||||
|
let repository =
|
||||||
|
repository_with_archives(&[("fortif.rlb", &[(b"broken.bas", &bad_payload)])]);
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("fortif.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"broken.bas"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = prepare_component_bas_from_resource(&repository, source, 3)
|
||||||
|
.expect_err("malformed BAS counts are rejected");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
AssetError::InvalidPrototype(message)
|
||||||
|
if message.contains("prototype 3 BAS geometry fortif.rlb:broken.bas")
|
||||||
|
&& message.contains("limit")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_control_preserves_unresolved_point_offset_sentinels() {
|
fn component_control_preserves_unresolved_point_offset_sentinels() {
|
||||||
let mut control = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
let mut control = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
||||||
@@ -5007,6 +5137,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
let second = EffectivePrototype {
|
let second = EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")),
|
||||||
@@ -5022,6 +5153,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_ne!(stable_visual_id(&first), stable_visual_id(&second));
|
assert_ne!(stable_visual_id(&first), stable_visual_id(&second));
|
||||||
@@ -5088,6 +5220,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -5205,6 +5338,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
graph
|
graph
|
||||||
@@ -5360,6 +5494,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -5520,6 +5655,7 @@ mod tests {
|
|||||||
type_id: None,
|
type_id: None,
|
||||||
}),
|
}),
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-b")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-b")),
|
||||||
@@ -5534,6 +5670,7 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-c")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-c")),
|
||||||
@@ -5548,6 +5685,7 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -5643,6 +5781,7 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let assets = prepare_mission_assets_with_repository(&repository, &[0..1], &[prototype])
|
let assets = prepare_mission_assets_with_repository(&repository, &[0..1], &[prototype])
|
||||||
@@ -5840,6 +5979,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -6051,6 +6191,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree_b")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree_b")),
|
||||||
@@ -6065,6 +6206,7 @@ mod tests {
|
|||||||
control_resource: None,
|
control_resource: None,
|
||||||
point_resource: None,
|
point_resource: None,
|
||||||
node_data_resource: None,
|
node_data_resource: None,
|
||||||
|
bas_resource: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
(repo, prototypes)
|
(repo, prototypes)
|
||||||
@@ -6227,6 +6369,7 @@ mod tests {
|
|||||||
prototype_requests: vec![prototype.key.clone()],
|
prototype_requests: vec![prototype.key.clone()],
|
||||||
root_prototype_request_spans: std::iter::once(0..1).collect(),
|
root_prototype_request_spans: std::iter::once(0..1).collect(),
|
||||||
root_unit_components: vec![Vec::new()],
|
root_unit_components: vec![Vec::new()],
|
||||||
|
root_unit_dat_header_flags: vec![None],
|
||||||
visual_dependencies_expanded: false,
|
visual_dependencies_expanded: false,
|
||||||
nodes: vec![root_node, prototype_node, mesh_node],
|
nodes: vec![root_node, prototype_node, mesh_node],
|
||||||
edges: vec![
|
edges: vec![
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//! Prototype registry and unit DAT primitives.
|
//! Prototype registry and unit DAT primitives.
|
||||||
|
|
||||||
use encoding_rs::WINDOWS_1251;
|
use encoding_rs::WINDOWS_1251;
|
||||||
use fparkan_binary::{checked_count_bytes, Cursor, DecodeError};
|
use fparkan_binary::{checked_allocation_len, checked_count_bytes, Cursor, DecodeError, Limits};
|
||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathPolicy, ResourceName};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathPolicy, ResourceName};
|
||||||
use fparkan_resource::{
|
use fparkan_resource::{
|
||||||
archive_path, resource_name, ResourceError, ResourceKey, ResourceRepository,
|
archive_path, resource_name, ResourceError, ResourceKey, ResourceRepository,
|
||||||
@@ -18,6 +18,9 @@ const PROTOTYPE_INHERITANCE_DEPTH_LIMIT: usize = 32;
|
|||||||
/// `objects.rlb` external geometry entry type (`EXTO`).
|
/// `objects.rlb` external geometry entry type (`EXTO`).
|
||||||
pub const PROTOTYPE_TYPE_EXTO: u32 = u32::from_le_bytes(*b"EXTO");
|
pub const PROTOTYPE_TYPE_EXTO: u32 = u32::from_le_bytes(*b"EXTO");
|
||||||
|
|
||||||
|
/// Unit DAT second-header-word bit that enables native BAS placement handling.
|
||||||
|
pub const UNIT_DAT_BAS_PLACEMENT_FLAG: u32 = 0x8000_0000;
|
||||||
|
|
||||||
/// Prototype key.
|
/// Prototype key.
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct PrototypeKey(pub ResourceName);
|
pub struct PrototypeKey(pub ResourceName);
|
||||||
@@ -40,6 +43,48 @@ pub struct UnitDat {
|
|||||||
pub records: Vec<UnitComponentRecord>,
|
pub records: Vec<UnitComponentRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// BAS payload with two ordered point-record sections.
|
||||||
|
///
|
||||||
|
/// The `first` and `second` fields describe stream order only. The native
|
||||||
|
/// loader reads one record shape in the first section and a point-only shape
|
||||||
|
/// in the second, but this type does not assign gameplay or geometry meaning
|
||||||
|
/// to either section.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BasGeometry {
|
||||||
|
/// First ordered section, whose records also carry two opaque word arrays.
|
||||||
|
pub first: Vec<BasGeometryRecord>,
|
||||||
|
/// Second ordered section of point sequences.
|
||||||
|
pub second: Vec<BasPointSequence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BAS record from the first ordered section.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BasGeometryRecord {
|
||||||
|
/// Count value stored before the point sequence.
|
||||||
|
pub declared_count: u32,
|
||||||
|
/// `declared_count + 1` points copied by the native loader.
|
||||||
|
///
|
||||||
|
/// Point sequences are preserved as read; closure is not imposed by the
|
||||||
|
/// binary decoder.
|
||||||
|
pub points: Vec<[f32; 3]>,
|
||||||
|
/// First opaque word array, containing `declared_count` entries.
|
||||||
|
pub opaque_words_a: Vec<u32>,
|
||||||
|
/// Second opaque word array, containing `declared_count` entries.
|
||||||
|
pub opaque_words_b: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BAS point sequence from the second ordered section.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BasPointSequence {
|
||||||
|
/// Count value stored before the point sequence.
|
||||||
|
pub declared_count: u32,
|
||||||
|
/// `declared_count + 1` points copied by the native loader.
|
||||||
|
///
|
||||||
|
/// Point sequences are preserved as read; closure is not imposed by the
|
||||||
|
/// binary decoder.
|
||||||
|
pub points: Vec<[f32; 3]>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Unit DAT binding used by mission object references.
|
/// Unit DAT binding used by mission object references.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct UnitDatBinding {
|
pub struct UnitDatBinding {
|
||||||
@@ -117,6 +162,9 @@ pub struct EffectivePrototype {
|
|||||||
/// Explicit component NDP/provider resource referenced by this
|
/// Explicit component NDP/provider resource referenced by this
|
||||||
/// prototype's resolved `objects.rlb` registry inheritance chain.
|
/// prototype's resolved `objects.rlb` registry inheritance chain.
|
||||||
pub node_data_resource: Option<ResourceKey>,
|
pub node_data_resource: Option<ResourceKey>,
|
||||||
|
/// Explicit BAS geometry resource referenced by this prototype's resolved
|
||||||
|
/// `objects.rlb` registry inheritance chain.
|
||||||
|
pub bas_resource: Option<ResourceKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prototype resolution source.
|
/// Prototype resolution source.
|
||||||
@@ -146,6 +194,10 @@ pub struct PrototypeGraph {
|
|||||||
/// assigning runtime semantics to their `kind`, links, descriptions, or
|
/// assigning runtime semantics to their `kind`, links, descriptions, or
|
||||||
/// opaque tails.
|
/// opaque tails.
|
||||||
pub root_unit_components: Vec<Vec<UnitComponentRecord>>,
|
pub root_unit_components: Vec<Vec<UnitComponentRecord>>,
|
||||||
|
/// Second word of each standard component Unit DAT header. This vector is
|
||||||
|
/// aligned with [`Self::roots`]; direct prototype roots, legacy one-model
|
||||||
|
/// bindings, and failed decodes are `None`.
|
||||||
|
pub root_unit_dat_header_flags: Vec<Option<u32>>,
|
||||||
/// Whether visual dependency expansion has already been applied.
|
/// Whether visual dependency expansion has already been applied.
|
||||||
pub visual_dependencies_expanded: bool,
|
pub visual_dependencies_expanded: bool,
|
||||||
/// Materialized prototype dependency graph nodes.
|
/// Materialized prototype dependency graph nodes.
|
||||||
@@ -568,6 +620,190 @@ pub fn decode_unit_dat(payload: &[u8]) -> Result<UnitDat, PrototypeError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes a BAS payload as two ordered sections of point records.
|
||||||
|
///
|
||||||
|
/// Each section begins with a record count. Every record stores a declared
|
||||||
|
/// count followed by that count plus one little-endian `Vec3` values. Records
|
||||||
|
/// in the first section then carry two arrays of `declared_count` opaque
|
||||||
|
/// words. The decoder preserves points and words without assigning geometric
|
||||||
|
/// meaning or requiring the point sequence to be closed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`PrototypeError::Decode`] when a count exceeds the shared binary
|
||||||
|
/// limits, a record is truncated, a point is not finite, or trailing bytes
|
||||||
|
/// remain.
|
||||||
|
pub fn decode_bas_geometry(payload: &[u8]) -> Result<BasGeometry, PrototypeError> {
|
||||||
|
let limits = Limits::default();
|
||||||
|
let payload_len = u64::try_from(payload.len()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
if payload_len > limits.max_file_bytes {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: payload_len,
|
||||||
|
limit: limits.max_file_bytes,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(payload);
|
||||||
|
let mut budget = BasAllocationBudget::new(limits.max_decoded_bytes);
|
||||||
|
|
||||||
|
let first_count = read_bas_count(&mut cursor, limits.max_entries)?;
|
||||||
|
check_bas_section_minimum(&cursor, first_count)?;
|
||||||
|
let first_capacity =
|
||||||
|
budget.reserve::<BasGeometryRecord>(u64::from(first_count), limits.max_entries)?;
|
||||||
|
let mut first = Vec::with_capacity(first_capacity);
|
||||||
|
for _ in 0..first_count {
|
||||||
|
let declared_count = read_bas_count(&mut cursor, limits.max_array_items)?;
|
||||||
|
let point_count = bas_point_count(declared_count, limits.max_array_items)?;
|
||||||
|
check_bas_first_record(&cursor, declared_count, point_count)?;
|
||||||
|
|
||||||
|
let points = read_bas_points(&mut cursor, point_count, &mut budget, limits)?;
|
||||||
|
let opaque_words_a = read_bas_words(&mut cursor, declared_count, &mut budget, limits)?;
|
||||||
|
let opaque_words_b = read_bas_words(&mut cursor, declared_count, &mut budget, limits)?;
|
||||||
|
first.push(BasGeometryRecord {
|
||||||
|
declared_count,
|
||||||
|
points,
|
||||||
|
opaque_words_a,
|
||||||
|
opaque_words_b,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let second_count = read_bas_count(&mut cursor, limits.max_entries)?;
|
||||||
|
check_bas_section_minimum(&cursor, second_count)?;
|
||||||
|
let second_capacity =
|
||||||
|
budget.reserve::<BasPointSequence>(u64::from(second_count), limits.max_entries)?;
|
||||||
|
let mut second = Vec::with_capacity(second_capacity);
|
||||||
|
for _ in 0..second_count {
|
||||||
|
let declared_count = read_bas_count(&mut cursor, limits.max_array_items)?;
|
||||||
|
let point_count = bas_point_count(declared_count, limits.max_array_items)?;
|
||||||
|
let remaining =
|
||||||
|
u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
checked_count_bytes(point_count, 12, remaining)?;
|
||||||
|
let points = read_bas_points(&mut cursor, point_count, &mut budget, limits)?;
|
||||||
|
second.push(BasPointSequence {
|
||||||
|
declared_count,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.require_eof()?;
|
||||||
|
Ok(BasGeometry { first, second })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bas_count(cursor: &mut Cursor<'_>, limit: u32) -> Result<u32, DecodeError> {
|
||||||
|
let count = cursor.read_u32_le()?;
|
||||||
|
if count > limit {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: u64::from(count),
|
||||||
|
limit: u64::from(limit),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bas_point_count(declared_count: u32, limit: u32) -> Result<u64, DecodeError> {
|
||||||
|
let point_count = u64::from(declared_count)
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
if point_count > u64::from(limit) {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: point_count,
|
||||||
|
limit: u64::from(limit),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(point_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_bas_section_minimum(cursor: &Cursor<'_>, record_count: u32) -> Result<(), DecodeError> {
|
||||||
|
let remaining = u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
// Each record has a count word and at least one point (count zero).
|
||||||
|
checked_count_bytes(u64::from(record_count), 16, remaining)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_bas_first_record(
|
||||||
|
cursor: &Cursor<'_>,
|
||||||
|
declared_count: u32,
|
||||||
|
point_count: u64,
|
||||||
|
) -> Result<(), DecodeError> {
|
||||||
|
let remaining = u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
let point_bytes = checked_count_bytes(point_count, 12, remaining)?;
|
||||||
|
let after_points = remaining
|
||||||
|
.checked_sub(u64::try_from(point_bytes).map_err(|_| DecodeError::IntegerOverflow)?)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
// The two unnamed word arrays each contain `declared_count` u32 entries.
|
||||||
|
checked_count_bytes(u64::from(declared_count), 8, after_points)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bas_points(
|
||||||
|
cursor: &mut Cursor<'_>,
|
||||||
|
point_count: u64,
|
||||||
|
budget: &mut BasAllocationBudget,
|
||||||
|
limits: Limits,
|
||||||
|
) -> Result<Vec<[f32; 3]>, DecodeError> {
|
||||||
|
let capacity = budget.reserve::<[f32; 3]>(point_count, limits.max_array_items)?;
|
||||||
|
let mut points = Vec::with_capacity(capacity);
|
||||||
|
for _ in 0..point_count {
|
||||||
|
let point = [
|
||||||
|
cursor.read_f32_le()?,
|
||||||
|
cursor.read_f32_le()?,
|
||||||
|
cursor.read_f32_le()?,
|
||||||
|
];
|
||||||
|
if !point.iter().all(|value| value.is_finite()) {
|
||||||
|
return Err(DecodeError::Invalid("BAS point is not finite"));
|
||||||
|
}
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
Ok(points)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bas_words(
|
||||||
|
cursor: &mut Cursor<'_>,
|
||||||
|
word_count: u32,
|
||||||
|
budget: &mut BasAllocationBudget,
|
||||||
|
limits: Limits,
|
||||||
|
) -> Result<Vec<u32>, DecodeError> {
|
||||||
|
let capacity = budget.reserve::<u32>(u64::from(word_count), limits.max_array_items)?;
|
||||||
|
let mut words = Vec::with_capacity(capacity);
|
||||||
|
for _ in 0..word_count {
|
||||||
|
words.push(cursor.read_u32_le()?);
|
||||||
|
}
|
||||||
|
Ok(words)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BasAllocationBudget {
|
||||||
|
used: u64,
|
||||||
|
limit: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasAllocationBudget {
|
||||||
|
const fn new(limit: u64) -> Self {
|
||||||
|
Self { used: 0, limit }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve<T>(&mut self, count: u64, count_limit: u32) -> Result<usize, DecodeError> {
|
||||||
|
let capacity = checked_allocation_len(count, u64::from(count_limit))?;
|
||||||
|
let element_size =
|
||||||
|
u64::try_from(std::mem::size_of::<T>()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
let allocation = count
|
||||||
|
.checked_mul(element_size)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
let total = self
|
||||||
|
.used
|
||||||
|
.checked_add(allocation)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
if total > self.limit {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: total,
|
||||||
|
limit: self.limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.used = total;
|
||||||
|
Ok(capacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds the immediate-child tree encoded by Unit DAT record counts.
|
/// Builds the immediate-child tree encoded by Unit DAT record counts.
|
||||||
///
|
///
|
||||||
/// Records are stored in preorder. The final word of each 112-byte record is
|
/// Records are stored in preorder. The final word of each 112-byte record is
|
||||||
@@ -750,6 +986,7 @@ struct ResolvedPrototypeRequests {
|
|||||||
expected_count: usize,
|
expected_count: usize,
|
||||||
prototypes: Vec<EffectivePrototype>,
|
prototypes: Vec<EffectivePrototype>,
|
||||||
unit_components: Vec<UnitComponentRecord>,
|
unit_components: Vec<UnitComponentRecord>,
|
||||||
|
unit_dat_header_flags: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_prototype_requests(
|
fn resolve_prototype_requests(
|
||||||
@@ -766,6 +1003,7 @@ fn resolve_prototype_requests(
|
|||||||
expected_count: 1,
|
expected_count: 1,
|
||||||
prototypes: prototype.into_iter().collect(),
|
prototypes: prototype.into_iter().collect(),
|
||||||
unit_components: Vec::new(),
|
unit_components: Vec::new(),
|
||||||
|
unit_dat_header_flags: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,6 +1039,11 @@ fn resolve_unit_dat_prototype_requests(
|
|||||||
expected_count: unit.records.len(),
|
expected_count: unit.records.len(),
|
||||||
prototypes,
|
prototypes,
|
||||||
unit_components: unit.records,
|
unit_components: unit.records,
|
||||||
|
unit_dat_header_flags: Some(u32::from_le_bytes(
|
||||||
|
unit.header_opaque[4..8]
|
||||||
|
.try_into()
|
||||||
|
.expect("fixed Unit DAT header slice"),
|
||||||
|
)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,6 +1056,7 @@ fn resolve_unit_dat_prototype_requests(
|
|||||||
expected_count: 1,
|
expected_count: 1,
|
||||||
prototypes: prototype.into_iter().collect(),
|
prototypes: prototype.into_iter().collect(),
|
||||||
unit_components: Vec::new(),
|
unit_components: Vec::new(),
|
||||||
|
unit_dat_header_flags: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,6 +1104,9 @@ pub fn build_prototype_graph(
|
|||||||
graph
|
graph
|
||||||
.root_unit_components
|
.root_unit_components
|
||||||
.push(expansion.unit_components.clone());
|
.push(expansion.unit_components.clone());
|
||||||
|
graph
|
||||||
|
.root_unit_dat_header_flags
|
||||||
|
.push(expansion.unit_dat_header_flags);
|
||||||
let unit_component_count = expansion.unit_components.len();
|
let unit_component_count = expansion.unit_components.len();
|
||||||
for (prototype_index, prototype) in expansion.prototypes.into_iter().enumerate() {
|
for (prototype_index, prototype) in expansion.prototypes.into_iter().enumerate() {
|
||||||
let unit_component_index = is_unit_dat_root
|
let unit_component_index = is_unit_dat_root
|
||||||
@@ -966,6 +1213,9 @@ pub fn build_prototype_graph_report(
|
|||||||
graph
|
graph
|
||||||
.root_unit_components
|
.root_unit_components
|
||||||
.push(expansion.unit_components.clone());
|
.push(expansion.unit_components.clone());
|
||||||
|
graph
|
||||||
|
.root_unit_dat_header_flags
|
||||||
|
.push(expansion.unit_dat_header_flags);
|
||||||
if edge == PrototypeGraphEdge::MissionToUnitDat {
|
if edge == PrototypeGraphEdge::MissionToUnitDat {
|
||||||
report.unit_component_count += expected;
|
report.unit_component_count += expected;
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1316,9 @@ pub fn build_prototype_graph_report(
|
|||||||
if graph.root_unit_components.len() <= root_index {
|
if graph.root_unit_components.len() <= root_index {
|
||||||
graph.root_unit_components.push(Vec::new());
|
graph.root_unit_components.push(Vec::new());
|
||||||
}
|
}
|
||||||
|
if graph.root_unit_dat_header_flags.len() <= root_index {
|
||||||
|
graph.root_unit_dat_header_flags.push(None);
|
||||||
|
}
|
||||||
let end = graph.prototype_requests.len();
|
let end = graph.prototype_requests.len();
|
||||||
graph.root_prototype_request_spans.push(start..end);
|
graph.root_prototype_request_spans.push(start..end);
|
||||||
}
|
}
|
||||||
@@ -1155,6 +1408,7 @@ fn resolve_archive_model(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,6 +1435,7 @@ fn resolve_objects_registry_model(
|
|||||||
let control_resource = resolve_explicit_control_resource(object_key, &refs)?;
|
let control_resource = resolve_explicit_control_resource(object_key, &refs)?;
|
||||||
let point_resource = resolve_explicit_point_resource(object_key, &refs)?;
|
let point_resource = resolve_explicit_point_resource(object_key, &refs)?;
|
||||||
let node_data_resource = resolve_explicit_node_data_resource(object_key, &refs)?;
|
let node_data_resource = resolve_explicit_node_data_resource(object_key, &refs)?;
|
||||||
|
let bas_resource = resolve_explicit_bas_resource(object_key, &refs)?;
|
||||||
|
|
||||||
let mut missing_mesh_refs = Vec::new();
|
let mut missing_mesh_refs = Vec::new();
|
||||||
for item in refs.iter().filter(|item| is_explicit_mesh_ref(item)) {
|
for item in refs.iter().filter(|item| is_explicit_mesh_ref(item)) {
|
||||||
@@ -1193,6 +1448,7 @@ fn resolve_objects_registry_model(
|
|||||||
control_resource.clone(),
|
control_resource.clone(),
|
||||||
point_resource.clone(),
|
point_resource.clone(),
|
||||||
node_data_resource.clone(),
|
node_data_resource.clone(),
|
||||||
|
bas_resource.clone(),
|
||||||
)? {
|
)? {
|
||||||
return Ok(Some(prototype));
|
return Ok(Some(prototype));
|
||||||
}
|
}
|
||||||
@@ -1215,6 +1471,7 @@ fn resolve_objects_registry_model(
|
|||||||
control_resource,
|
control_resource,
|
||||||
point_resource,
|
point_resource,
|
||||||
node_data_resource,
|
node_data_resource,
|
||||||
|
bas_resource,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1284,6 +1541,7 @@ fn resolve_object_ref_model(
|
|||||||
control_resource: Option<ResourceKey>,
|
control_resource: Option<ResourceKey>,
|
||||||
point_resource: Option<ResourceKey>,
|
point_resource: Option<ResourceKey>,
|
||||||
node_data_resource: Option<ResourceKey>,
|
node_data_resource: Option<ResourceKey>,
|
||||||
|
bas_resource: Option<ResourceKey>,
|
||||||
) -> Result<Option<EffectivePrototype>, PrototypeError> {
|
) -> Result<Option<EffectivePrototype>, PrototypeError> {
|
||||||
let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
||||||
let Some(mesh) = find_mesh_resource(repository, &archive, &ResourceName(model_name.to_vec()))?
|
let Some(mesh) = find_mesh_resource(repository, &archive, &ResourceName(model_name.to_vec()))?
|
||||||
@@ -1298,6 +1556,7 @@ fn resolve_object_ref_model(
|
|||||||
control_resource,
|
control_resource,
|
||||||
point_resource,
|
point_resource,
|
||||||
node_data_resource,
|
node_data_resource,
|
||||||
|
bas_resource,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1317,6 +1576,10 @@ fn is_explicit_node_data_ref(item: &ObjectRefRecord) -> bool {
|
|||||||
has_extension_bytes(cstr_bytes(&item.resource_raw), b"ndp")
|
has_extension_bytes(cstr_bytes(&item.resource_raw), b"ndp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_explicit_bas_ref(item: &ObjectRefRecord) -> bool {
|
||||||
|
has_extension_bytes(cstr_bytes(&item.resource_raw), b"bas")
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_explicit_control_resource(
|
fn resolve_explicit_control_resource(
|
||||||
requested: &ResourceName,
|
requested: &ResourceName,
|
||||||
refs: &[ObjectRefRecord],
|
refs: &[ObjectRefRecord],
|
||||||
@@ -1338,6 +1601,13 @@ fn resolve_explicit_node_data_resource(
|
|||||||
resolve_explicit_resource(requested, refs, is_explicit_node_data_ref, "node data")
|
resolve_explicit_resource(requested, refs, is_explicit_node_data_ref, "node data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_explicit_bas_resource(
|
||||||
|
requested: &ResourceName,
|
||||||
|
refs: &[ObjectRefRecord],
|
||||||
|
) -> Result<Option<ResourceKey>, PrototypeError> {
|
||||||
|
resolve_explicit_resource(requested, refs, is_explicit_bas_ref, "BAS geometry")
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_explicit_resource(
|
fn resolve_explicit_resource(
|
||||||
requested: &ResourceName,
|
requested: &ResourceName,
|
||||||
refs: &[ObjectRefRecord],
|
refs: &[ObjectRefRecord],
|
||||||
@@ -1442,6 +1712,7 @@ fn effective(
|
|||||||
control_resource: Option<ResourceKey>,
|
control_resource: Option<ResourceKey>,
|
||||||
point_resource: Option<ResourceKey>,
|
point_resource: Option<ResourceKey>,
|
||||||
node_data_resource: Option<ResourceKey>,
|
node_data_resource: Option<ResourceKey>,
|
||||||
|
bas_resource: Option<ResourceKey>,
|
||||||
) -> EffectivePrototype {
|
) -> EffectivePrototype {
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: PrototypeKey(requested),
|
key: PrototypeKey(requested),
|
||||||
@@ -1452,6 +1723,7 @@ fn effective(
|
|||||||
control_resource,
|
control_resource,
|
||||||
point_resource,
|
point_resource,
|
||||||
node_data_resource,
|
node_data_resource,
|
||||||
|
bas_resource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,6 +1858,70 @@ mod tests {
|
|||||||
assert_eq!(cstr_bytes(&records[1].resource_raw), b"second.msh");
|
assert_eq!(cstr_bytes(&records[1].resource_raw), b"second.msh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bas_geometry_preserves_both_sections_and_open_point_sequences() {
|
||||||
|
let bytes = build_bas_geometry_payload();
|
||||||
|
|
||||||
|
let geometry = decode_bas_geometry(&bytes).expect("BAS geometry");
|
||||||
|
|
||||||
|
assert_eq!(geometry.first.len(), 2);
|
||||||
|
assert_eq!(geometry.first[0].declared_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
geometry.first[0].points,
|
||||||
|
vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
|
||||||
|
);
|
||||||
|
assert_eq!(geometry.first[0].opaque_words_a, vec![0x1122_3344]);
|
||||||
|
assert_eq!(geometry.first[0].opaque_words_b, vec![0x5566_7788]);
|
||||||
|
assert_eq!(geometry.first[1].declared_count, 2);
|
||||||
|
assert_eq!(
|
||||||
|
geometry.first[1].points,
|
||||||
|
vec![[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]]
|
||||||
|
);
|
||||||
|
assert_eq!(geometry.first[1].opaque_words_a, vec![7, 8]);
|
||||||
|
assert_eq!(geometry.first[1].opaque_words_b, vec![9, 10]);
|
||||||
|
assert_eq!(geometry.second.len(), 1);
|
||||||
|
assert_eq!(geometry.second[0].declared_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
geometry.second[0].points,
|
||||||
|
vec![[20.0, 21.0, 22.0], [23.0, 24.0, 25.0]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bas_geometry_rejects_truncated_records_and_extreme_counts() {
|
||||||
|
let mut truncated = build_bas_geometry_payload();
|
||||||
|
truncated.pop();
|
||||||
|
assert!(matches!(
|
||||||
|
decode_bas_geometry(&truncated),
|
||||||
|
Err(PrototypeError::Decode(DecodeError::UnexpectedEof { .. }))
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_bas_geometry(&u32::MAX.to_le_bytes()),
|
||||||
|
Err(PrototypeError::Decode(DecodeError::LimitExceeded { .. }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bas_geometry_rejects_non_finite_points_and_trailing_bytes() {
|
||||||
|
let mut non_finite = build_bas_geometry_payload();
|
||||||
|
// The first point follows the first section and first record counts.
|
||||||
|
non_finite[8..12].copy_from_slice(&f32::NAN.to_bits().to_le_bytes());
|
||||||
|
assert!(matches!(
|
||||||
|
decode_bas_geometry(&non_finite),
|
||||||
|
Err(PrototypeError::Decode(DecodeError::Invalid(
|
||||||
|
"BAS point is not finite"
|
||||||
|
)))
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut trailing = build_bas_geometry_payload();
|
||||||
|
trailing.push(0);
|
||||||
|
assert!(matches!(
|
||||||
|
decode_bas_geometry(&trailing),
|
||||||
|
Err(PrototypeError::Decode(DecodeError::TrailingBytes { .. }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unit_zero_records_uses_exact_size() {
|
fn unit_zero_records_uses_exact_size() {
|
||||||
let bytes = [0_u8; 8];
|
let bytes = [0_u8; 8];
|
||||||
@@ -1628,6 +1964,10 @@ mod tests {
|
|||||||
let unit = decode_unit_dat(&bytes).expect("unit");
|
let unit = decode_unit_dat(&bytes).expect("unit");
|
||||||
let record = &unit.records[0];
|
let record = &unit.records[0];
|
||||||
assert_eq!(unit.header_opaque, [0xF1, 0xF0, 1, 2, 3, 4, 5, 6]);
|
assert_eq!(unit.header_opaque, [0xF1, 0xF0, 1, 2, 3, 4, 5, 6]);
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_le_bytes(unit.header_opaque[4..8].try_into().unwrap()),
|
||||||
|
0x0605_0403
|
||||||
|
);
|
||||||
assert_eq!(record.parent_or_link, -7);
|
assert_eq!(record.parent_or_link, -7);
|
||||||
assert_eq!(&record.description_raw[..description.len()], description);
|
assert_eq!(&record.description_raw[..description.len()], description);
|
||||||
assert_eq!(record.tail0, 0x1122_3344);
|
assert_eq!(record.tail0, 0x1122_3344);
|
||||||
@@ -1806,6 +2146,7 @@ mod tests {
|
|||||||
let (graph, resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &roots);
|
let (graph, resolved, report) = build_prototype_graph_report(&repo, vfs.as_ref(), &roots);
|
||||||
|
|
||||||
assert_eq!(graph.roots.len(), 2);
|
assert_eq!(graph.roots.len(), 2);
|
||||||
|
assert_eq!(graph.root_unit_dat_header_flags, vec![None, None]);
|
||||||
assert_eq!(resolved.len(), 1);
|
assert_eq!(resolved.len(), 1);
|
||||||
assert_eq!(report.root_count, 2);
|
assert_eq!(report.root_count, 2);
|
||||||
assert_eq!(report.direct_reference_count, 2);
|
assert_eq!(report.direct_reference_count, 2);
|
||||||
@@ -1838,6 +2179,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let vfs = Arc::new(vfs);
|
let vfs = Arc::new(vfs);
|
||||||
let repo = CachedResourceRepository::new(vfs.clone());
|
let repo = CachedResourceRepository::new(vfs.clone());
|
||||||
|
let (graph, _, report) = build_prototype_graph_report(
|
||||||
|
&repo,
|
||||||
|
vfs.as_ref(),
|
||||||
|
&[resource_name(b"UNITS/AUTO/unit.dat")],
|
||||||
|
);
|
||||||
|
assert!(report.is_success());
|
||||||
|
assert_eq!(graph.root_unit_dat_header_flags, vec![None]);
|
||||||
let resolved =
|
let resolved =
|
||||||
resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"UNITS/AUTO/unit.dat"))
|
resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"UNITS/AUTO/unit.dat"))
|
||||||
.expect("resolve")
|
.expect("resolve")
|
||||||
@@ -1923,16 +2271,12 @@ mod tests {
|
|||||||
let objects_path = resource_archive_path(b"objects.rlb").expect("objects path");
|
let objects_path = resource_archive_path(b"objects.rlb").expect("objects path");
|
||||||
let static_path = resource_archive_path(b"static.rlb").expect("static path");
|
let static_path = resource_archive_path(b"static.rlb").expect("static path");
|
||||||
let mesh = minimal_msh_payload();
|
let mesh = minimal_msh_payload();
|
||||||
vfs.insert(
|
let mut unit_dat = build_unit_dat(&[
|
||||||
dat_path,
|
|
||||||
Arc::from(
|
|
||||||
build_unit_dat(&[
|
|
||||||
(b"objects.rlb".as_slice(), b"component_a".as_slice()),
|
(b"objects.rlb".as_slice(), b"component_a".as_slice()),
|
||||||
(b"objects.rlb".as_slice(), b"component_b".as_slice()),
|
(b"objects.rlb".as_slice(), b"component_b".as_slice()),
|
||||||
])
|
]);
|
||||||
.into_boxed_slice(),
|
unit_dat[4..8].copy_from_slice(&UNIT_DAT_BAS_PLACEMENT_FLAG.to_le_bytes());
|
||||||
),
|
vfs.insert(dat_path, Arc::from(unit_dat.into_boxed_slice()));
|
||||||
);
|
|
||||||
vfs.insert(
|
vfs.insert(
|
||||||
objects_path,
|
objects_path,
|
||||||
Arc::from(
|
Arc::from(
|
||||||
@@ -2077,16 +2421,12 @@ mod tests {
|
|||||||
let objects_path = resource_archive_path(b"objects.rlb").expect("objects path");
|
let objects_path = resource_archive_path(b"objects.rlb").expect("objects path");
|
||||||
let static_path = resource_archive_path(b"static.rlb").expect("static path");
|
let static_path = resource_archive_path(b"static.rlb").expect("static path");
|
||||||
let mesh = minimal_msh_payload();
|
let mesh = minimal_msh_payload();
|
||||||
vfs.insert(
|
let mut unit_dat = build_unit_dat(&[
|
||||||
dat_path,
|
|
||||||
Arc::from(
|
|
||||||
build_unit_dat(&[
|
|
||||||
(b"objects.rlb".as_slice(), b"component_a".as_slice()),
|
(b"objects.rlb".as_slice(), b"component_a".as_slice()),
|
||||||
(b"objects.rlb".as_slice(), b"component_b".as_slice()),
|
(b"objects.rlb".as_slice(), b"component_b".as_slice()),
|
||||||
])
|
]);
|
||||||
.into_boxed_slice(),
|
unit_dat[4..8].copy_from_slice(&UNIT_DAT_BAS_PLACEMENT_FLAG.to_le_bytes());
|
||||||
),
|
vfs.insert(dat_path, Arc::from(unit_dat.into_boxed_slice()));
|
||||||
);
|
|
||||||
vfs.insert(
|
vfs.insert(
|
||||||
objects_path,
|
objects_path,
|
||||||
Arc::from(
|
Arc::from(
|
||||||
@@ -2132,6 +2472,10 @@ mod tests {
|
|||||||
assert_eq!(graph.prototype_requests[1].0 .0, b"component_b");
|
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.len(), 1);
|
||||||
assert_eq!(graph.root_unit_components[0].len(), 2);
|
assert_eq!(graph.root_unit_components[0].len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
graph.root_unit_dat_header_flags,
|
||||||
|
vec![Some(UNIT_DAT_BAS_PLACEMENT_FLAG)]
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cstr_bytes(&graph.root_unit_components[0][0].resource_raw),
|
cstr_bytes(&graph.root_unit_components[0][0].resource_raw),
|
||||||
b"component_a"
|
b"component_a"
|
||||||
@@ -2204,6 +2548,7 @@ mod tests {
|
|||||||
(b"fortif.rlb".as_slice(), b"child_proto.bas".as_slice()),
|
(b"fortif.rlb".as_slice(), b"child_proto.bas".as_slice()),
|
||||||
(b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()),
|
(b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()),
|
||||||
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
||||||
|
(b"fortif.rlb".as_slice(), b"parent_proto.ndp".as_slice()),
|
||||||
])
|
])
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
),
|
),
|
||||||
@@ -2252,6 +2597,11 @@ mod tests {
|
|||||||
.expect("inherited explicit NDP provider table");
|
.expect("inherited explicit NDP provider table");
|
||||||
assert_eq!(node_data.archive.as_str(), "fortif.rlb");
|
assert_eq!(node_data.archive.as_str(), "fortif.rlb");
|
||||||
assert!(node_data.name.0.eq_ignore_ascii_case(b"parent_proto.ndp"));
|
assert!(node_data.name.0.eq_ignore_ascii_case(b"parent_proto.ndp"));
|
||||||
|
let bas = resolved
|
||||||
|
.bas_resource
|
||||||
|
.expect("child-local explicit BAS geometry");
|
||||||
|
assert_eq!(bas.archive.as_str(), "fortif.rlb");
|
||||||
|
assert!(bas.name.0.eq_ignore_ascii_case(b"child_proto.bas"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2389,6 +2739,33 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn objects_registry_rejects_distinct_explicit_bas_resources() {
|
||||||
|
let raw_name = |value: &[u8]| {
|
||||||
|
let mut raw = [0; 32];
|
||||||
|
raw[..value.len()].copy_from_slice(value);
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
let refs = [
|
||||||
|
ObjectRefRecord {
|
||||||
|
archive_raw: raw_name(b"first.rlb"),
|
||||||
|
resource_raw: raw_name(b"first.bas"),
|
||||||
|
},
|
||||||
|
ObjectRefRecord {
|
||||||
|
archive_raw: raw_name(b"second.rlb"),
|
||||||
|
resource_raw: raw_name(b"second.bas"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let error = resolve_explicit_bas_resource(&resource_name(b"prototype"), &refs)
|
||||||
|
.expect_err("distinct explicit BAS resources are ambiguous");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
PrototypeError::Resource(ResourceError::Format(message))
|
||||||
|
if message.contains("ambiguous explicit BAS geometry resources")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn objects_registry_inheritance_resolves_multiple_levels() {
|
fn objects_registry_inheritance_resolves_multiple_levels() {
|
||||||
let mut vfs = MemoryVfs::default();
|
let mut vfs = MemoryVfs::default();
|
||||||
@@ -2782,6 +3159,27 @@ mod tests {
|
|||||||
assert_eq!(mesh.archive.as_str().to_ascii_lowercase(), archive);
|
assert_eq!(mesh.archive.as_str().to_ascii_lowercase(), archive);
|
||||||
assert!(mesh.name.0.eq_ignore_ascii_case(model));
|
assert!(mesh.name.0.eq_ignore_ascii_case(model));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let plant = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"fr_b_plant"))
|
||||||
|
.expect("resolve factory prototype")
|
||||||
|
.expect("factory prototype");
|
||||||
|
let bas = plant.bas_resource.expect("explicit BAS registry reference");
|
||||||
|
assert_eq!(bas.archive.as_str().to_ascii_lowercase(), "fortif.rlb");
|
||||||
|
assert!(bas.name.0.eq_ignore_ascii_case(b"fr_b_plant.bas"));
|
||||||
|
let bas_archive = repo
|
||||||
|
.open_archive(&bas.archive)
|
||||||
|
.expect("open explicit BAS archive");
|
||||||
|
let bas_entry = repo
|
||||||
|
.find(bas_archive, &bas.name)
|
||||||
|
.expect("find explicit BAS entry")
|
||||||
|
.expect("BAS entry exists");
|
||||||
|
let bas_payload = repo.read(bas_entry).expect("read explicit BAS entry");
|
||||||
|
let geometry =
|
||||||
|
decode_bas_geometry(bas_payload.as_slice()).expect("decode licensed BAS geometry");
|
||||||
|
assert_eq!(geometry.first.len(), 1);
|
||||||
|
assert_eq!(geometry.second.len(), 1);
|
||||||
|
assert_eq!(geometry.first[0].points.len(), 21);
|
||||||
|
assert_eq!(geometry.second[0].points.len(), 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3001,6 +3399,53 @@ mod tests {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_bas_geometry_payload() -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&2_u32.to_le_bytes());
|
||||||
|
|
||||||
|
push_bas_record_with_words(
|
||||||
|
&mut out,
|
||||||
|
1,
|
||||||
|
&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
|
||||||
|
&[0x1122_3344],
|
||||||
|
&[0x5566_7788],
|
||||||
|
);
|
||||||
|
push_bas_record_with_words(
|
||||||
|
&mut out,
|
||||||
|
2,
|
||||||
|
&[[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]],
|
||||||
|
&[7, 8],
|
||||||
|
&[9, 10],
|
||||||
|
);
|
||||||
|
|
||||||
|
out.extend_from_slice(&1_u32.to_le_bytes());
|
||||||
|
out.extend_from_slice(&1_u32.to_le_bytes());
|
||||||
|
push_bas_points(&mut out, &[[20.0, 21.0, 22.0], [23.0, 24.0, 25.0]]);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_bas_record_with_words(
|
||||||
|
out: &mut Vec<u8>,
|
||||||
|
declared_count: u32,
|
||||||
|
points: &[[f32; 3]],
|
||||||
|
words_a: &[u32],
|
||||||
|
words_b: &[u32],
|
||||||
|
) {
|
||||||
|
out.extend_from_slice(&declared_count.to_le_bytes());
|
||||||
|
push_bas_points(out, points);
|
||||||
|
for word in words_a.iter().chain(words_b) {
|
||||||
|
out.extend_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_bas_points(out: &mut Vec<u8>, points: &[[f32; 3]]) {
|
||||||
|
for point in points {
|
||||||
|
for value in point {
|
||||||
|
out.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_object_refs(items: &[(&[u8], &[u8])]) -> Vec<u8> {
|
fn build_object_refs(items: &[(&[u8], &[u8])]) -> Vec<u8> {
|
||||||
let mut out = Vec::with_capacity(items.len() * 64);
|
let mut out = Vec::with_capacity(items.len() * 64);
|
||||||
for (archive, resource) in items {
|
for (archive, resource) in items {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use fparkan_assets::{
|
|||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
||||||
use fparkan_prototype::{
|
use fparkan_prototype::{
|
||||||
build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport,
|
build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport,
|
||||||
UnitComponentRecord,
|
UnitComponentRecord, UNIT_DAT_BAS_PLACEMENT_FLAG,
|
||||||
};
|
};
|
||||||
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
||||||
use fparkan_script::{
|
use fparkan_script::{
|
||||||
@@ -74,6 +74,13 @@ pub struct MissionObjectDraft {
|
|||||||
pub resource_name_raw: Vec<u8>,
|
pub resource_name_raw: Vec<u8>,
|
||||||
/// Raw identity/clan word from the mission.
|
/// Raw identity/clan word from the mission.
|
||||||
pub identity_or_clan_raw: u32,
|
pub identity_or_clan_raw: u32,
|
||||||
|
/// Raw word after the mission object's instance name. Native BAS
|
||||||
|
/// placement uses its zero/nonzero state only when the Unit DAT header
|
||||||
|
/// gate is set.
|
||||||
|
pub raw_after_name: u32,
|
||||||
|
/// Second word of the root Unit DAT header, when it was decoded. A direct
|
||||||
|
/// prototype root has no Unit DAT header.
|
||||||
|
pub unit_dat_header_flags: Option<u32>,
|
||||||
/// Raw position vector.
|
/// Raw position vector.
|
||||||
pub position: [f32; 3],
|
pub position: [f32; 3],
|
||||||
/// Raw orientation vector.
|
/// Raw orientation vector.
|
||||||
@@ -91,6 +98,49 @@ pub struct MissionObjectDraft {
|
|||||||
pub unit_components: Vec<UnitComponentRecord>,
|
pub unit_components: Vec<UnitComponentRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Native terrain adjustment selected for a mission object's BAS geometry.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BasementPlacementMode {
|
||||||
|
/// Flatten the BAS inner boundary to mean outer terrain height and adjust
|
||||||
|
/// the object root transform by the native placement delta.
|
||||||
|
FlattenInnerAndAdjustRoot,
|
||||||
|
/// Keep authored BAS inner heights and leave the object root transform as
|
||||||
|
/// supplied by the mission.
|
||||||
|
PreserveAuthoredInnerHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MissionObjectDraft {
|
||||||
|
/// Returns the BAS mode selected by the native Unit DAT gate and mission
|
||||||
|
/// word, independent of resource path naming.
|
||||||
|
#[must_use]
|
||||||
|
pub fn basement_placement_mode(&self) -> Option<BasementPlacementMode> {
|
||||||
|
let header_flags = self.unit_dat_header_flags?;
|
||||||
|
if header_flags & UNIT_DAT_BAS_PLACEMENT_FLAG == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(if self.raw_after_name == 0 {
|
||||||
|
BasementPlacementMode::FlattenInnerAndAdjustRoot
|
||||||
|
} else {
|
||||||
|
BasementPlacementMode::PreserveAuthoredInnerHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this mission root's position after applying the native BAS
|
||||||
|
/// transform adjustment once. Native code subtracts
|
||||||
|
/// `authored_inner_z - mean_outer_z` from matrix element 11. Component
|
||||||
|
/// local transforms should continue to be composed beneath this adjusted
|
||||||
|
/// root position without applying the delta again.
|
||||||
|
#[must_use]
|
||||||
|
pub fn root_position_with_basement_delta(&self, placement_delta: f32) -> [f32; 3] {
|
||||||
|
let mut position = self.position;
|
||||||
|
if self.basement_placement_mode() == Some(BasementPlacementMode::FlattenInnerAndAdjustRoot)
|
||||||
|
{
|
||||||
|
position[2] -= placement_delta;
|
||||||
|
}
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A compiled script bundle selected by one mission clan.
|
/// A compiled script bundle selected by one mission clan.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MissionScriptBundle {
|
pub struct MissionScriptBundle {
|
||||||
@@ -731,6 +781,12 @@ fn load_mission_with_options(
|
|||||||
original_id: u32::try_from(index).ok().map(OriginalObjectId),
|
original_id: u32::try_from(index).ok().map(OriginalObjectId),
|
||||||
resource_name_raw: object.resource_raw.clone(),
|
resource_name_raw: object.resource_raw.clone(),
|
||||||
identity_or_clan_raw: object.identity_or_clan_raw,
|
identity_or_clan_raw: object.identity_or_clan_raw,
|
||||||
|
raw_after_name: object.raw_after_name,
|
||||||
|
unit_dat_header_flags: prototype_graph
|
||||||
|
.root_unit_dat_header_flags
|
||||||
|
.get(index)
|
||||||
|
.copied()
|
||||||
|
.flatten(),
|
||||||
position: object.position,
|
position: object.position,
|
||||||
orientation_raw: object.orientation,
|
orientation_raw: object.orientation,
|
||||||
scale: object.scale,
|
scale: object.scale,
|
||||||
@@ -1329,6 +1385,58 @@ mod tests {
|
|||||||
use fparkan_vfs::{DirectoryVfs, MemoryVfs, VfsEntry, VfsMetadata};
|
use fparkan_vfs::{DirectoryVfs, MemoryVfs, VfsEntry, VfsMetadata};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn basement_draft(header_flags: Option<u32>, raw_after_name: u32) -> MissionObjectDraft {
|
||||||
|
MissionObjectDraft {
|
||||||
|
original_id: Some(OriginalObjectId(7)),
|
||||||
|
resource_name_raw: b"BUILDS/example.dat".to_vec(),
|
||||||
|
identity_or_clan_raw: 0,
|
||||||
|
raw_after_name,
|
||||||
|
unit_dat_header_flags: header_flags,
|
||||||
|
position: [12.0, 34.0, 100.0],
|
||||||
|
orientation_raw: [0.0; 3],
|
||||||
|
scale: [1.0; 3],
|
||||||
|
visual_ids: Vec::new(),
|
||||||
|
properties: Vec::new(),
|
||||||
|
unit_components: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_basement_mode_uses_unit_header_gate_not_builds_path() {
|
||||||
|
assert_eq!(
|
||||||
|
basement_draft(Some(UNIT_DAT_BAS_PLACEMENT_FLAG), 0).basement_placement_mode(),
|
||||||
|
Some(BasementPlacementMode::FlattenInnerAndAdjustRoot)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
basement_draft(Some(UNIT_DAT_BAS_PLACEMENT_FLAG), 1).basement_placement_mode(),
|
||||||
|
Some(BasementPlacementMode::PreserveAuthoredInnerHeight)
|
||||||
|
);
|
||||||
|
assert_eq!(basement_draft(Some(0), 0).basement_placement_mode(), None);
|
||||||
|
assert_eq!(basement_draft(None, 1).basement_placement_mode(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_basement_delta_adjusts_only_flattened_root_once() {
|
||||||
|
let flatten = basement_draft(Some(UNIT_DAT_BAS_PLACEMENT_FLAG), 0);
|
||||||
|
let root = flatten.root_position_with_basement_delta(3.25);
|
||||||
|
assert_eq!(root, [12.0, 34.0, 96.75]);
|
||||||
|
// A component remains local to the root; it receives the root shift
|
||||||
|
// through composition instead of applying the delta itself.
|
||||||
|
let child_local_z = 8.0;
|
||||||
|
assert_eq!(root[2] + child_local_z, 104.75);
|
||||||
|
|
||||||
|
let preserve = basement_draft(Some(UNIT_DAT_BAS_PLACEMENT_FLAG), 0x40);
|
||||||
|
assert_eq!(
|
||||||
|
preserve.root_position_with_basement_delta(3.25),
|
||||||
|
preserve.position
|
||||||
|
);
|
||||||
|
let no_gate = basement_draft(Some(0), 0);
|
||||||
|
assert_eq!(
|
||||||
|
no_gate.root_position_with_basement_delta(3.25),
|
||||||
|
no_gate.position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
||||||
let mut engine = create(EngineServices::default()).expect("engine");
|
let mut engine = create(EngineServices::default()).expect("engine");
|
||||||
|
|||||||
Reference in new issue
Block a user