diff options
Diffstat (limited to 'crates/fparkan-runtime/src/lib.rs')
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 110 |
1 files changed, 109 insertions, 1 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index e0088a3..57bee81 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -12,7 +12,7 @@ use fparkan_assets::{ use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy}; use fparkan_prototype::{ build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport, - UnitComponentRecord, + UnitComponentRecord, UNIT_DAT_BAS_PLACEMENT_FLAG, }; use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository}; use fparkan_script::{ @@ -74,6 +74,13 @@ pub struct MissionObjectDraft { pub resource_name_raw: Vec<u8>, /// Raw identity/clan word from the mission. 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. pub position: [f32; 3], /// Raw orientation vector. @@ -91,6 +98,49 @@ pub struct MissionObjectDraft { 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. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MissionScriptBundle { @@ -731,6 +781,12 @@ fn load_mission_with_options( original_id: u32::try_from(index).ok().map(OriginalObjectId), resource_name_raw: object.resource_raw.clone(), 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, orientation_raw: object.orientation, scale: object.scale, @@ -1329,6 +1385,58 @@ mod tests { use fparkan_vfs::{DirectoryVfs, MemoryVfs, VfsEntry, VfsMetadata}; 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] fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() { let mut engine = create(EngineServices::default()).expect("engine"); |
