feat(runtime): load Control node providers for resource queries
Docs Deploy / Build and Deploy MkDocs (push) Failing after 3h0m1s

This commit is contained in:
Valentin Popov committed 2026-10-22 14:50:54 +04:00
1 parent b9a4d1b478
commit 29b81b8723
4 files changed
+977 -73

No files matched your search

+781 -48
View File
@@ -1,14 +1,16 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! Runtime orchestration for mission loading and world stepping. //! Runtime orchestration for mission loading and world stepping.
#[cfg(test)]
use fparkan_assets::PreparedControlResourcePair;
use fparkan_assets::{ use fparkan_assets::{
decode_mission_land_path, decode_mission_payload, decode_nres_payload, decode_mission_land_path, decode_mission_payload, decode_nres_payload,
derive_mission_land_paths, extend_graph_report_with_visual_dependencies, derive_mission_land_paths, extend_graph_report_with_visual_dependencies,
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError, mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError, AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
MissionTerrainPaths, NresError, PreparedComponentControl, PreparedControlEffectSection, MissionTerrainPaths, NresError, PreparedComponentControl, PreparedControlEffectSection,
PreparedControlTypedRecord, PreparedVisual, TerrainFormatError, TerrainPreparationError, PreparedControlNodeDataRow, PreparedControlNodeDataTable, PreparedControlTypedRecord,
TerrainWorld, TmaProfile, PreparedVisual, TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile,
}; };
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy}; use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
use fparkan_prototype::{ use fparkan_prototype::{
@@ -190,6 +192,49 @@ pub struct EffectiveControlDevice {
pub descriptor_provenance: ControlDeviceRecordProvenance, pub descriptor_provenance: ControlDeviceRecordProvenance,
} }
/// Initial row in the loaded global Control node-data provider.
///
/// The native table is indexed independently of MSH model nodes. Its rows are
/// appended in root/EXTO resource order and begin with a type-1 value of 1.0.
#[derive(Clone, Debug, PartialEq)]
pub struct MissionControlNodeProviderRow {
/// NDP resource that contributed the row.
pub source: ResourceKey,
/// Unit DAT component that contributed the row.
pub unit_component_index: usize,
/// Index in that component's original NDP table.
pub source_row_index: usize,
/// Original NDP descriptor fields and provenance for this provider row.
pub source_row: PreparedControlNodeDataRow,
/// Initial native provider selector-1 value (`+0x14`) as raw bits.
pub initial_type1_value_bits: u32,
}
/// Availability of the initial global Control node-data provider projection.
#[derive(Clone, Debug, PartialEq)]
pub enum MissionControlNodeProviderTableStatus {
/// Rows in native append order, without padding to the MSH node count.
Available(Vec<MissionControlNodeProviderRow>),
/// Provider lookup is not modeled for this object's available inputs.
Unavailable(MissionControlNodeProviderTableUnavailableReason),
}
/// Why the global Control node-data provider could not be projected.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MissionControlNodeProviderTableUnavailableReason {
/// A root or mounted EXTO Control resource has no resolved NDP sidecar.
MissingNodeData {
/// Unit DAT component needing the provider rows.
unit_component_index: usize,
/// Explicitly requested sidecar when the reference was known.
source: Option<ResourceKey>,
},
/// A mounted EXTO needs a first NDP row for the native skip-first path.
MissingExToRootRow { unit_component_index: usize },
/// The Control component traversal itself was incomplete.
DeviceTableUnavailable(MissionControlDeviceTableUnavailableReason),
}
/// Why a loaded Control resource query or write could not be represented. /// Why a loaded Control resource query or write could not be represented.
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum ControlResourceAccessError { pub enum ControlResourceAccessError {
@@ -205,6 +250,24 @@ pub enum ControlResourceAccessError {
UnsupportedSelector(u32), UnsupportedSelector(u32),
/// A selected row has a factory class whose getter has not been checked. /// A selected row has a factory class whose getter has not been checked.
UnsupportedFactoryTag { factory_tag: u32 }, UnsupportedFactoryTag { factory_tag: u32 },
/// A device index does not exist in the object's native Control table.
DeviceIndexOutOfRange {
/// Requested global Control device index.
device_index: usize,
/// Projected device count.
device_count: usize,
},
/// The node provider table could not be projected.
NodeProviderTableUnavailable(MissionControlNodeProviderTableUnavailableReason),
/// A device's native provider key exceeds the actual appended NDP rows.
NodeProviderKeyOutOfRange {
/// Current native device key, used directly as a provider row index.
key: u32,
/// Actual global provider row count.
row_count: usize,
},
/// A native tag has an e00 callback that has not been implemented here.
UnsupportedCurrentTag { tag: u32 },
/// A selected row lacks a value needed by the native getter or setter. /// A selected row lacks a value needed by the native getter or setter.
UnsupportedField { UnsupportedField {
factory_tag: u32, factory_tag: u32,
@@ -283,6 +346,13 @@ pub struct MissionControlDeviceTableState {
pub handle: ObjectHandle, pub handle: ObjectHandle,
/// Complete base projection, or an explicit unsupported boundary. /// Complete base projection, or an explicit unsupported boundary.
pub status: MissionControlDeviceTableStatus, pub status: MissionControlDeviceTableStatus,
/// Initial global NDP provider rows used by indexed Control selectors.
pub node_provider_status: MissionControlNodeProviderTableStatus,
}
struct ControlDeviceProjection {
devices: Vec<EffectiveControlDevice>,
node_provider_status: MissionControlNodeProviderTableStatus,
} }
/// Resource distributor state associated with one TMA clan's constructed AI. /// Resource distributor state associated with one TMA clan's constructed AI.
@@ -1223,25 +1293,36 @@ fn load_mission_with_options(
.iter() .iter()
.zip(&handles) .zip(&handles)
.enumerate() .enumerate()
.map( .map(|(object_index, (draft, handle))| {
|(object_index, (draft, handle))| MissionControlDeviceTableState { let projection = build_control_device_projection(
original_id: draft.original_id, object_index,
handle: *handle, draft,
status: build_control_device_table( &mission_assets,
object_index, prototype_graph
draft, .root_prototype_request_spans
&mission_assets, .get(object_index),
prototype_graph &resolved_prototypes,
.root_prototype_request_spans );
.get(object_index), match projection {
&resolved_prototypes, Ok(projection) => MissionControlDeviceTableState {
) original_id: draft.original_id,
.map_or_else( handle: *handle,
MissionControlDeviceTableStatus::Unavailable, status: MissionControlDeviceTableStatus::Available(projection.devices),
MissionControlDeviceTableStatus::Available, node_provider_status: projection.node_provider_status,
), },
}, Err(reason) => MissionControlDeviceTableState {
) original_id: draft.original_id,
handle: *handle,
status: MissionControlDeviceTableStatus::Unavailable(reason.clone()),
node_provider_status:
MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::DeviceTableUnavailable(
reason,
),
),
},
}
})
.collect() .collect()
} else { } else {
Vec::new() Vec::new()
@@ -1652,6 +1733,35 @@ pub fn query_loaded_mission_control_resource(
query_control_device_table_resource(table, selector) query_control_device_table_resource(table, selector)
} }
/// Queries one loaded Control device through its original factory getter.
///
/// Selector `0xe00` is currently modeled for current descriptor tag 26 with
/// factory tag 26. The loaded NDP provider value is its initial native state;
/// later Control updates are not synchronized into this snapshot.
///
/// # Errors
/// Returns an error for an invalid handle, unavailable mission/table/provider,
/// an out-of-range device/provider key, or a factory/tag/float path that is not
/// supported by the current reconstruction.
pub fn query_loaded_mission_control_device_selector(
engine: &Engine,
handle: ObjectHandle,
device_index: usize,
selector: u32,
) -> Result<Option<f32>, ControlResourceAccessError> {
transform_state(&engine.world, handle).map_err(ControlResourceAccessError::World)?;
let loaded = engine
.loaded
.as_ref()
.ok_or(ControlResourceAccessError::MissionUnavailable)?;
let table = loaded
.control_device_tables
.iter()
.find(|table| table.handle == handle)
.ok_or(ControlResourceAccessError::ObjectUnavailable { handle })?;
query_control_device_table_selector(table, device_index, selector)
}
/// Sets native Control global-table selector 1 on rows whose selector-0 value /// Sets native Control global-table selector 1 on rows whose selector-0 value
/// is positive. Validation is completed before any row is changed. /// is positive. Validation is completed before any row is changed.
/// ///
@@ -1721,6 +1831,85 @@ fn query_control_device_table_resource(
checked_control_ratio("ratio", numerator, denominator).map(Some) checked_control_ratio("ratio", numerator, denominator).map(Some)
} }
fn query_control_device_table_selector(
table: &MissionControlDeviceTableState,
device_index: usize,
selector: u32,
) -> Result<Option<f32>, ControlResourceAccessError> {
if selector != 0xe00 {
return Err(ControlResourceAccessError::UnsupportedSelector(selector));
}
let devices = match &table.status {
MissionControlDeviceTableStatus::Available(devices) => devices,
MissionControlDeviceTableStatus::Unavailable(reason) => {
return Err(ControlResourceAccessError::TableUnavailable(reason.clone()));
}
};
let device =
devices
.get(device_index)
.ok_or(ControlResourceAccessError::DeviceIndexOutOfRange {
device_index,
device_count: devices.len(),
})?;
match device.descriptor.tag {
26 => {}
5 | 10 | 21 => {
return Err(ControlResourceAccessError::UnsupportedCurrentTag {
tag: device.descriptor.tag,
});
}
_ => return Ok(None),
}
if device.factory_tag != 26 {
return Err(ControlResourceAccessError::UnsupportedFactoryTag {
factory_tag: device.factory_tag,
});
}
let providers = match &table.node_provider_status {
MissionControlNodeProviderTableStatus::Available(providers) => providers,
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
reason.clone(),
));
}
};
let key = usize::try_from(device.key).map_err(|_| {
ControlResourceAccessError::NodeProviderKeyOutOfRange {
key: device.key,
row_count: providers.len(),
}
})?;
let provider =
providers
.get(key)
.ok_or(ControlResourceAccessError::NodeProviderKeyOutOfRange {
key: device.key,
row_count: providers.len(),
})?;
let provider_value = checked_control_float(
device.factory_tag,
"node provider value",
f32::from_bits(provider.initial_type1_value_bits),
)?;
let coefficient = control_device_coefficient(device)?;
let multiplier = control_device_float(device, device.multiplier_bits, "multiplier")?;
let provider_weighted = checked_control_product(
device.factory_tag,
"provider times coefficient",
provider_value,
coefficient,
)?;
checked_control_product(
device.factory_tag,
"then multiplier",
provider_weighted,
multiplier,
)
.map(Some)
}
fn set_control_device_table_multiplier( fn set_control_device_table_multiplier(
table: &mut MissionControlDeviceTableState, table: &mut MissionControlDeviceTableState,
value: f32, value: f32,
@@ -1941,6 +2130,30 @@ fn native_component_target_index(
} }
} }
fn native_component_mount_node(
tree: &[UnitComponentTreeNode],
component_ids: &[Option<u32>],
first_global_nodes: &[Option<u32>],
component_index: usize,
raw_local_index: i32,
) -> Option<u32> {
tree.get(component_index)
.and_then(|node| node.parent_index)
.and_then(|parent_index| {
let parent_id = component_ids.get(parent_index).copied().flatten()?;
let parent_first = first_global_nodes
.get(parent_index)
.copied()
.flatten()
.unwrap_or(u32::MAX);
Some(
parent_first
.wrapping_add(raw_local_index as u32)
.wrapping_sub(u32::from(parent_id != 0)),
)
})
}
fn native_into_table_target_index( fn native_into_table_target_index(
devices: &[EffectiveControlDevice], devices: &[EffectiveControlDevice],
parent_component_id: u32, parent_component_id: u32,
@@ -2091,6 +2304,7 @@ fn native_control_factory_defaults(factory_tag: u32) -> (Option<u32>, Option<u32
} }
} }
#[cfg(test)]
fn build_control_device_table( fn build_control_device_table(
object_index: usize, object_index: usize,
draft: &MissionObjectDraft, draft: &MissionObjectDraft,
@@ -2098,6 +2312,17 @@ fn build_control_device_table(
prototype_span: Option<&std::ops::Range<usize>>, prototype_span: Option<&std::ops::Range<usize>>,
prototypes: &[fparkan_prototype::EffectivePrototype], prototypes: &[fparkan_prototype::EffectivePrototype],
) -> Result<Vec<EffectiveControlDevice>, MissionControlDeviceTableUnavailableReason> { ) -> Result<Vec<EffectiveControlDevice>, MissionControlDeviceTableUnavailableReason> {
build_control_device_projection(object_index, draft, assets, prototype_span, prototypes)
.map(|projection| projection.devices)
}
fn build_control_device_projection(
object_index: usize,
draft: &MissionObjectDraft,
assets: &MissionAssets,
prototype_span: Option<&std::ops::Range<usize>>,
prototypes: &[fparkan_prototype::EffectivePrototype],
) -> Result<ControlDeviceProjection, MissionControlDeviceTableUnavailableReason> {
use MissionControlDeviceTableUnavailableReason as Unavailable; use MissionControlDeviceTableUnavailableReason as Unavailable;
draft draft
@@ -2250,6 +2475,7 @@ fn build_control_device_table(
} }
let mut devices = Vec::new(); let mut devices = Vec::new();
let mut node_provider_status = MissionControlNodeProviderTableStatus::Available(Vec::new());
let mut has_control_resource = false; let mut has_control_resource = false;
for component_index in active_components { for component_index in active_components {
let prototype = let prototype =
@@ -2286,6 +2512,40 @@ fn build_control_device_table(
component_ids[component_index].ok_or(Unavailable::ComponentAlignmentMismatch)?; component_ids[component_index].ok_or(Unavailable::ComponentAlignmentMismatch)?;
let is_root = component_index == 0; let is_root = component_index == 0;
let is_ex_to = prototype.source_type == Some(PROTOTYPE_TYPE_EXTO); let is_ex_to = prototype.source_type == Some(PROTOTYPE_TYPE_EXTO);
let raw_local_index = draft.unit_components[component_index].parent_or_link;
let parent_component_id = tree
.get(component_index)
.and_then(|node| node.parent_index)
.and_then(|parent_index| component_ids.get(parent_index).copied().flatten())
.unwrap_or(u32::MAX);
let mount_node = if is_ex_to {
native_component_mount_node(
&tree,
&component_ids,
&first_global_nodes,
component_index,
raw_local_index,
)
} else {
None
};
if is_root {
append_control_node_provider_rows(
&mut node_provider_status,
component_index,
prototype.node_data_resource.as_ref(),
control.node_data.as_ref(),
false,
);
} else if is_ex_to && mount_node.is_some_and(|node| node != u32::MAX) {
append_control_node_provider_rows(
&mut node_provider_status,
component_index,
prototype.node_data_resource.as_ref(),
control.node_data.as_ref(),
true,
);
}
if is_root { if is_root {
for (record_index, descriptor) in control.typed_records.iter().enumerate() { for (record_index, descriptor) in control.typed_records.iter().enumerate() {
append_effective_control_device( append_effective_control_device(
@@ -2301,12 +2561,6 @@ fn build_control_device_table(
continue; continue;
} }
let raw_local_index = draft.unit_components[component_index].parent_or_link;
let parent_component_id = tree
.get(component_index)
.and_then(|node| node.parent_index)
.and_then(|parent_index| component_ids.get(parent_index).copied().flatten())
.unwrap_or(u32::MAX);
let native_target = native_component_target_index( let native_target = native_component_target_index(
prototype.source_type, prototype.source_type,
&devices, &devices,
@@ -2315,25 +2569,6 @@ fn build_control_device_table(
); );
match native_control_target_action(native_target) { match native_control_target_action(native_target) {
ControlTableTarget::Append => { ControlTableTarget::Append => {
let mount_node = if is_ex_to {
tree.get(component_index)
.and_then(|node| node.parent_index)
.and_then(|parent_index| {
let parent_id = component_ids.get(parent_index).copied().flatten()?;
let parent_first = first_global_nodes
.get(parent_index)
.copied()
.flatten()
.unwrap_or(u32::MAX);
Some(
parent_first
.wrapping_add(raw_local_index as u32)
.wrapping_sub(u32::from(parent_id != 0)),
)
})
} else {
None
};
let first_global_node = first_global_nodes.get(component_index).copied().flatten(); let first_global_node = first_global_nodes.get(component_index).copied().flatten();
for (record_index, descriptor) in control.typed_records.iter().enumerate() { for (record_index, descriptor) in control.typed_records.iter().enumerate() {
let raw_key = descriptor.fixed_words[1]; let raw_key = descriptor.fixed_words[1];
@@ -2395,7 +2630,49 @@ fn build_control_device_table(
if !has_control_resource { if !has_control_resource {
return Err(Unavailable::NoControlResource); return Err(Unavailable::NoControlResource);
} }
Ok(devices) Ok(ControlDeviceProjection {
devices,
node_provider_status,
})
}
fn append_control_node_provider_rows(
status: &mut MissionControlNodeProviderTableStatus,
unit_component_index: usize,
requested_source: Option<&ResourceKey>,
node_data: Option<&PreparedControlNodeDataTable>,
skip_first_row: bool,
) {
let MissionControlNodeProviderTableStatus::Available(rows) = status else {
return;
};
let Some(node_data) = node_data else {
*status = MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::MissingNodeData {
unit_component_index,
source: requested_source.cloned(),
},
);
return;
};
if skip_first_row && node_data.rows.is_empty() {
*status = MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::MissingExToRootRow {
unit_component_index,
},
);
return;
}
let first_row = usize::from(skip_first_row);
for (source_row_index, source_row) in node_data.rows.iter().enumerate().skip(first_row) {
rows.push(MissionControlNodeProviderRow {
source: node_data.source.clone(),
unit_component_index,
source_row_index,
source_row: source_row.clone(),
initial_type1_value_bits: 0x3f80_0000,
});
}
} }
fn add_behavior_registered_object( fn add_behavior_registered_object(
@@ -2837,6 +3114,62 @@ mod tests {
} }
} }
fn synthetic_control_node_data_table(
index: usize,
row_count: usize,
) -> PreparedControlNodeDataTable {
PreparedControlNodeDataTable {
source: synthetic_resource_key(300 + index),
rows: (0..row_count)
.map(|row_index| PreparedControlNodeDataRow {
raw_flags: u32::try_from(row_index).expect("row index fits u32"),
max_volume: (100 + row_index) as f32,
mobility_per_volume: (200 + row_index) as f32,
effect: PreparedControlResourcePair {
archive: [0; 32],
name: [0; 32],
},
})
.collect(),
}
}
fn control_with_synthetic_node_data(
index: usize,
typed_records: Vec<PreparedControlTypedRecord>,
row_count: usize,
) -> PreparedComponentControl {
let mut control = synthetic_component_control(
index,
typed_records,
PreparedControlEffectSection::Parsed(Vec::new()),
);
control.node_data = Some(synthetic_control_node_data_table(index, row_count));
control
}
fn synthetic_node_provider_row(
component_index: usize,
source_row_index: usize,
initial_type1_value_bits: u32,
) -> MissionControlNodeProviderRow {
MissionControlNodeProviderRow {
source: synthetic_resource_key(300 + component_index),
unit_component_index: component_index,
source_row_index,
source_row: PreparedControlNodeDataRow {
raw_flags: u32::try_from(source_row_index).expect("row index fits u32"),
max_volume: 100.0,
mobility_per_volume: 200.0,
effect: PreparedControlResourcePair {
archive: [0; 32],
name: [0; 32],
},
},
initial_type1_value_bits,
}
}
fn synthetic_control_fixture( fn synthetic_control_fixture(
source_types: &[Option<u32>], source_types: &[Option<u32>],
kinds: &[u32], kinds: &[u32],
@@ -2902,8 +3235,11 @@ mod tests {
control_resource: controls[index] control_resource: controls[index]
.as_ref() .as_ref()
.map(|control| control.source.clone()), .map(|control| control.source.clone()),
node_data_resource: controls[index]
.as_ref()
.and_then(|control| control.node_data.as_ref())
.map(|node_data| node_data.source.clone()),
point_resource: None, point_resource: None,
node_data_resource: None,
bas_resource: None, bas_resource: None,
}); });
} }
@@ -2956,6 +3292,48 @@ mod tests {
) )
} }
fn synthetic_control_projection(
source_types: &[Option<u32>],
kinds: &[u32],
child_counts: &[u32],
parent_or_links: &[i32],
model_nodes: &[usize],
controls: Vec<Option<PreparedComponentControl>>,
) -> (
MissionControlDeviceTableStatus,
MissionControlNodeProviderTableStatus,
) {
let (draft, assets, prototypes) = synthetic_control_fixture(
source_types,
kinds,
child_counts,
parent_or_links,
model_nodes,
controls,
);
let (status, node_provider_status) = match build_control_device_projection(
0,
&draft,
&assets,
Some(&(0..prototypes.len())),
&prototypes,
) {
Ok(projection) => (
MissionControlDeviceTableStatus::Available(projection.devices),
projection.node_provider_status,
),
Err(reason) => (
MissionControlDeviceTableStatus::Unavailable(reason.clone()),
MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::DeviceTableUnavailable(
reason,
),
),
),
};
(status, node_provider_status)
}
fn synthetic_effective_control_device( fn synthetic_effective_control_device(
factory_tag: u32, factory_tag: u32,
descriptor_tag: u32, descriptor_tag: u32,
@@ -2988,6 +3366,16 @@ mod tests {
fn synthetic_control_device_table( fn synthetic_control_device_table(
status: MissionControlDeviceTableStatus, status: MissionControlDeviceTableStatus,
) -> MissionControlDeviceTableState {
synthetic_control_device_table_with_providers(
status,
MissionControlNodeProviderTableStatus::Available(Vec::new()),
)
}
fn synthetic_control_device_table_with_providers(
status: MissionControlDeviceTableStatus,
node_provider_status: MissionControlNodeProviderTableStatus,
) -> MissionControlDeviceTableState { ) -> MissionControlDeviceTableState {
MissionControlDeviceTableState { MissionControlDeviceTableState {
original_id: Some(OriginalObjectId(7)), original_id: Some(OriginalObjectId(7)),
@@ -2996,6 +3384,7 @@ mod tests {
slot: 7, slot: 7,
}, },
status, status,
node_provider_status,
} }
} }
@@ -3384,6 +3773,10 @@ mod tests {
query_loaded_mission_control_resource(&engine, handle, 1), query_loaded_mission_control_resource(&engine, handle, 1),
Err(ControlResourceAccessError::MissionUnavailable) Err(ControlResourceAccessError::MissionUnavailable)
); );
assert_eq!(
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
Err(ControlResourceAccessError::MissionUnavailable)
);
let stale = ObjectHandle { let stale = ObjectHandle {
generation: handle.generation.wrapping_add(1), generation: handle.generation.wrapping_add(1),
@@ -3395,6 +3788,12 @@ mod tests {
fparkan_world::WorldError::StaleHandle fparkan_world::WorldError::StaleHandle
)) ))
); );
assert_eq!(
query_loaded_mission_control_device_selector(&engine, stale, 0, 0xe00),
Err(ControlResourceAccessError::World(
fparkan_world::WorldError::StaleHandle
))
);
assert_eq!( assert_eq!(
set_loaded_mission_control_multiplier(&mut engine, stale, 2.0), set_loaded_mission_control_multiplier(&mut engine, stale, 2.0),
Err(ControlResourceAccessError::World( Err(ControlResourceAccessError::World(
@@ -3409,6 +3808,12 @@ mod tests {
fparkan_world::WorldError::StaleHandle fparkan_world::WorldError::StaleHandle
)) ))
); );
assert_eq!(
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
Err(ControlResourceAccessError::World(
fparkan_world::WorldError::StaleHandle
))
);
} }
#[test] #[test]
@@ -3747,6 +4152,291 @@ mod tests {
); );
} }
#[test]
fn synthetic_node_provider_projection_keeps_native_rows_separate_from_mesh_keys() {
let controls = vec![
Some(control_with_synthetic_node_data(
0,
vec![synthetic_control_record(26, 1, 2.0_f32.to_bits(), None)],
2,
)),
Some(control_with_synthetic_node_data(
1,
vec![synthetic_control_record(26, 0, 3.0_f32.to_bits(), None)],
7,
)),
Some(control_with_synthetic_node_data(
2,
vec![synthetic_control_record(26, 5, 4.0_f32.to_bits(), None)],
3,
)),
Some(control_with_synthetic_node_data(
3,
vec![synthetic_control_record(26, 0, 5.0_f32.to_bits(), None)],
2,
)),
Some(control_with_synthetic_node_data(
4,
vec![synthetic_control_record(26, 0, 6.0_f32.to_bits(), None)],
6,
)),
Some(control_with_synthetic_node_data(
5,
vec![synthetic_control_record(26, 1, 6.0_f32.to_bits(), None)],
5,
)),
Some(control_with_synthetic_node_data(
6,
vec![synthetic_control_record(26, 1, 7.0_f32.to_bits(), None)],
4,
)),
];
let (status, node_provider_status) = synthetic_control_projection(
&[
None,
Some(PROTOTYPE_TYPE_INTO),
Some(PROTOTYPE_TYPE_EXTO),
Some(PROTOTYPE_TYPE_EXTO),
Some(PROTOTYPE_TYPE_EXTO),
None,
None,
],
&[1, 1, 1, 1, 1, 0, 1],
&[4, 0, 1, 0, 0, 1, 0],
&[0, -1, 2, 0, -1, 0, 0],
&[1, 1, 4, 2, 1, 1, 1],
controls,
);
let table = synthetic_control_device_table_with_providers(status, node_provider_status);
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
else {
panic!(
"synthetic provider rows should be available: {:?}",
table.node_provider_status
);
};
// Root contributes all rows, INTO contributes none, and each mounted
// EXTO skips its local row zero. The EXTO with C4=FFFFFFFF contributes
// no provider rows. The kind-zero component and its child are absent
// from the native active-component walk.
assert_eq!(rows.len(), 5);
assert_eq!(
rows.iter()
.map(|row| (row.unit_component_index, row.source_row_index))
.collect::<Vec<_>>(),
[(0, 0), (0, 1), (2, 1), (2, 2), (3, 1)]
);
assert!(rows
.iter()
.all(|row| row.initial_type1_value_bits == 1.0_f32.to_bits()));
let MissionControlDeviceTableStatus::Available(devices) = &table.status else {
panic!("synthetic devices should be available: {:?}", table.status);
};
assert_eq!(
devices.iter().map(|device| device.key).collect::<Vec<_>>(),
[1, u32::MAX, 5, 0, u32::MAX]
);
assert_eq!(
query_control_device_table_selector(&table, 0, 0xe00)
.expect("root tag-26 selector")
.map(f32::to_bits),
Some(2.0_f32.to_bits())
);
// Device keys still use the confirmed MSH mapping. The provider list
// is the real concatenated NDP row count, so no model-node padding or
// second key remap hides an out-of-range key.
assert_eq!(
query_control_device_table_selector(&table, 2, 0xe00),
Err(ControlResourceAccessError::NodeProviderKeyOutOfRange {
key: 5,
row_count: 5
})
);
assert_eq!(
query_control_device_table_selector(&table, 3, 0xe00),
Ok(Some(5.0))
);
assert_eq!(
query_control_device_table_selector(&table, 4, 0xe00),
Err(ControlResourceAccessError::NodeProviderKeyOutOfRange {
key: u32::MAX,
row_count: 5
})
);
}
#[test]
fn missing_node_provider_does_not_hide_aggregate_queries() {
let controls = vec![Some(synthetic_component_control(
0,
vec![
synthetic_control_record(26, 0, 2.0_f32.to_bits(), None),
synthetic_control_record(19, 1, 4.0_f32.to_bits(), None),
],
PreparedControlEffectSection::Parsed(Vec::new()),
))];
let (status, node_provider_status) =
synthetic_control_projection(&[None], &[1], &[0], &[0], &[1], controls);
let table = synthetic_control_device_table_with_providers(status, node_provider_status);
assert!(matches!(
&table.node_provider_status,
MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::MissingNodeData {
unit_component_index: 0,
..
}
)
));
assert_eq!(
query_control_device_table_resource(&table, 1)
.expect("aggregate selector 1 remains available")
.map(f32::to_bits),
Some(1.0_f32.to_bits())
);
assert_eq!(
query_control_device_table_resource(&table, 2)
.expect("aggregate selector 2 remains available")
.map(f32::to_bits),
Some(4.0_f32.to_bits())
);
assert!(matches!(
query_control_device_table_selector(&table, 0, 0xe00),
Err(ControlResourceAccessError::NodeProviderTableUnavailable(_))
));
}
#[test]
fn mounted_exto_requires_a_row_before_native_skip_first() {
let controls = vec![
Some(control_with_synthetic_node_data(
0,
vec![synthetic_control_record(26, 0, 1.0_f32.to_bits(), None)],
0,
)),
Some(control_with_synthetic_node_data(1, Vec::new(), 0)),
];
let (_, node_provider_status) = synthetic_control_projection(
&[None, Some(PROTOTYPE_TYPE_EXTO)],
&[1, 1],
&[1, 0],
&[0, 0],
&[2, 2],
controls,
);
assert_eq!(
node_provider_status,
MissionControlNodeProviderTableStatus::Unavailable(
MissionControlNodeProviderTableUnavailableReason::MissingExToRootRow {
unit_component_index: 1,
}
)
);
// An empty root table is legal, and a mounted EXTO with exactly its
// root row skips that row and contributes an empty range.
let controls = vec![
Some(control_with_synthetic_node_data(
0,
vec![synthetic_control_record(26, 0, 1.0_f32.to_bits(), None)],
0,
)),
Some(control_with_synthetic_node_data(1, Vec::new(), 1)),
];
let (_, node_provider_status) = synthetic_control_projection(
&[None, Some(PROTOTYPE_TYPE_EXTO)],
&[1, 1],
&[1, 0],
&[0, 0],
&[2, 2],
controls,
);
assert_eq!(
node_provider_status,
MissionControlNodeProviderTableStatus::Available(Vec::new())
);
}
#[test]
fn indexed_node_provider_query_handles_current_tags_and_factory_scope() {
let available = MissionControlNodeProviderTableStatus::Available(vec![
synthetic_node_provider_row(0, 0, 1.0_f32.to_bits()),
synthetic_node_provider_row(0, 1, 1.0_f32.to_bits()),
synthetic_node_provider_row(0, 2, 1.0_f32.to_bits()),
]);
let table = synthetic_control_device_table_with_providers(
MissionControlDeviceTableStatus::Available(vec![
synthetic_effective_control_device(
26,
26,
2.0_f32.to_bits(),
Some(3.0_f32.to_bits()),
None,
),
synthetic_effective_control_device(
26,
19,
4.0_f32.to_bits(),
Some(5.0_f32.to_bits()),
None,
),
synthetic_effective_control_device(
26,
5,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
),
synthetic_effective_control_device(
9,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
Some(0.5_f32.to_bits()),
),
]),
available,
);
assert_eq!(
query_control_device_table_selector(&table, 0, 0xe00)
.expect("tag-26 query")
.map(f32::to_bits),
Some(6.0_f32.to_bits())
);
assert_eq!(
query_control_device_table_selector(&table, 1, 0xe00),
Ok(None)
);
for tag in [5, 10, 21] {
let mut unsupported = table.clone();
let MissionControlDeviceTableStatus::Available(entries) = &mut unsupported.status
else {
unreachable!();
};
entries[1].descriptor.tag = tag;
assert_eq!(
query_control_device_table_selector(&unsupported, 1, 0xe00),
Err(ControlResourceAccessError::UnsupportedCurrentTag { tag })
);
}
assert_eq!(
query_control_device_table_selector(&table, 3, 0xe00),
Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: 9 })
);
assert_eq!(
query_control_device_table_selector(&table, 0, 1),
Err(ControlResourceAccessError::UnsupportedSelector(1))
);
assert_eq!(
query_control_device_table_selector(&table, 8, 0xe00),
Err(ControlResourceAccessError::DeviceIndexOutOfRange {
device_index: 8,
device_count: 4
})
);
}
#[test] #[test]
fn synthetic_into_builder_appends_all_rows_and_overrides_only_first_record() { fn synthetic_into_builder_appends_all_rows_and_overrides_only_first_record() {
let append = synthetic_control_table( let append = synthetic_control_table(
@@ -4049,6 +4739,49 @@ mod tests {
[1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 5] [1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 5]
); );
assert_eq!(entries[8].descriptor_provenance.typed_record_index, 8); assert_eq!(entries[8].descriptor_provenance.typed_record_index, 8);
assert_eq!(
query_loaded_mission_control_device_selector(&engine, control_handle, 8, 0xe00)
.expect("licensed tag-26 selector")
.map(f32::to_bits),
Some(1.0_f32.to_bits())
);
let prepared_controls = &engine
.loaded
.as_ref()
.expect("loaded mission state")
.mission_assets
.object_component_controls[0];
let root_node_data = prepared_controls[0]
.as_ref()
.and_then(|control| control.node_data.as_ref())
.expect("root NDP sidecar");
let ext_node_data = prepared_controls[5]
.as_ref()
.and_then(|control| control.node_data.as_ref())
.expect("EXTO NDP sidecar");
let provider_rows = match &tables[0].node_provider_status {
MissionControlNodeProviderTableStatus::Available(rows) => rows,
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
panic!("licensed provider table should be available: {reason:?}");
}
};
assert_eq!(root_node_data.rows.len(), 22);
assert_eq!(ext_node_data.rows.len(), 4);
assert_eq!(provider_rows.len(), 25);
let expected_provider_sources = (0..root_node_data.rows.len())
.map(|row| (0, row))
.chain((1..ext_node_data.rows.len()).map(|row| (5, row)))
.collect::<Vec<_>>();
assert_eq!(
provider_rows
.iter()
.map(|row| (row.unit_component_index, row.source_row_index))
.collect::<Vec<_>>(),
expected_provider_sources
);
assert!(provider_rows
.iter()
.all(|row| row.initial_type1_value_bits == 1.0_f32.to_bits()));
assert_eq!( assert_eq!(
( (
entries[9].factory_tag, entries[9].factory_tag,
@@ -29,6 +29,8 @@ internal static class BehaviorDistributorTickOracle {
private const int ControlResourceAggregateSetRva = 0x2ba30; private const int ControlResourceAggregateSetRva = 0x2ba30;
private const int ControlProviderInterfaceVtableRva = 0x3d254; private const int ControlProviderInterfaceVtableRva = 0x3d254;
private const int ControlProviderType1GetterRva = 0x0dc40; private const int ControlProviderType1GetterRva = 0x0dc40;
private const int ControlProviderRowConstructorRva = 0x0b240;
private const int ControlProviderAmountUpdateRva = 0x10f30;
private const int ArealMapUnitDatComponentBuilderRva = 0x14b90; private const int ArealMapUnitDatComponentBuilderRva = 0x14b90;
private const int ControlResourceDeviceConstructorRva = 0x22980; private const int ControlResourceDeviceConstructorRva = 0x22980;
private const int ControlResourceComponentOverrideRva = 0x2d890; private const int ControlResourceComponentOverrideRva = 0x2d890;
@@ -79,6 +81,9 @@ internal static class BehaviorDistributorTickOracle {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlNodeUpdateCallback(IntPtr self, uint nodeIndex, uint selector, uint valueBits);
private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap; private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap;
private static readonly VectorInsert VectorInsertCallback = InsertVector; private static readonly VectorInsert VectorInsertCallback = InsertVector;
@@ -92,7 +97,12 @@ internal static class BehaviorDistributorTickOracle {
private static readonly InterfaceGetProperty InterfaceGetCallback = GetInterfaceProperty; private static readonly InterfaceGetProperty InterfaceGetCallback = GetInterfaceProperty;
private static readonly InterfaceSetProperty InterfaceSetCallback = SetInterfaceProperty; private static readonly InterfaceSetProperty InterfaceSetCallback = SetInterfaceProperty;
private static readonly NativeArealMapEvent ArealMapEventCallback = OnArealMapEvent; private static readonly NativeArealMapEvent ArealMapEventCallback = OnArealMapEvent;
private static readonly NativeControlNodeUpdateCallback ControlNodeUpdateCallback = OnControlNodeUpdate;
private static ArealMapFixture activeArealMap; private static ArealMapFixture activeArealMap;
private static int controlNodeUpdateCount;
private static uint controlNodeUpdateIndex;
private static uint controlNodeUpdateSelector;
private static uint controlNodeUpdateValueBits;
private static IntPtr fastcallThunks; private static IntPtr fastcallThunks;
private static IntPtr queryObjectThunk; private static IntPtr queryObjectThunk;
private static IntPtr getInterfacePropertyThunk; private static IntPtr getInterfacePropertyThunk;
@@ -398,6 +408,12 @@ internal static class BehaviorDistributorTickOracle {
active.Trace.Add("iface-set:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":" + selector.ToString("x") + ":" + number.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); active.Trace.Add("iface-set:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":" + selector.ToString("x") + ":" + number.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
return 1; return 1;
} }
private static void OnControlNodeUpdate(IntPtr self, uint nodeIndex, uint selector, uint valueBits) {
controlNodeUpdateCount++;
controlNodeUpdateIndex = nodeIndex;
controlNodeUpdateSelector = selector;
controlNodeUpdateValueBits = valueBits;
}
private static float SequenceValue(float[] sequence, int index, float fallback) { private static float SequenceValue(float[] sequence, int index, float fallback) {
if (sequence == null || sequence.Length == 0) return fallback; if (sequence == null || sequence.Length == 0) return fallback;
return sequence[Math.Min(index, sequence.Length - 1)]; return sequence[Math.Min(index, sequence.Length - 1)];
@@ -493,6 +509,7 @@ internal static class BehaviorDistributorTickOracle {
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback); GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback); GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback); GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback);
GC.KeepAlive(ControlNodeUpdateCallback);
GC.KeepAlive(ArealMapEventCallback); GC.KeepAlive(ArealMapEventCallback);
if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease)) if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease))
throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error()); throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
@@ -514,6 +531,7 @@ internal static class BehaviorDistributorTickOracle {
RunNativeArealMapComponentBuilderCase(arealMapModule); RunNativeArealMapComponentBuilderCase(arealMapModule);
RunNativeControlIntoOverrideCase(controlModule); RunNativeControlIntoOverrideCase(controlModule);
RunNativeControlResourceDeviceCase(controlModule); RunNativeControlResourceDeviceCase(controlModule);
RunNativeControlProviderLiveStateCase(controlModule);
RunNativeControlAggregateResourceCases(controlModule); RunNativeControlAggregateResourceCases(controlModule);
} finally { } finally {
SetX87ControlWord(savedControlWord); SetX87ControlWord(savedControlWord);
@@ -594,6 +612,100 @@ internal static class BehaviorDistributorTickOracle {
} }
} }
private static void RunNativeControlProviderLiveStateCase(IntPtr module) {
NativeControlProviderRowConstructor constructProviderRow = (NativeControlProviderRowConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva),
typeof(NativeControlProviderRowConstructor));
NativeControlProviderAmountUpdate update = (NativeControlProviderAmountUpdate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderAmountUpdateRva),
typeof(NativeControlProviderAmountUpdate));
NativeControlResourceDeviceGet getter = (NativeControlResourceDeviceGet)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceDeviceGetterRva),
typeof(NativeControlResourceDeviceGet));
using (Fixture fixture = NewFixture()) {
IntPtr owner = fixture.Allocate(0x664);
IntPtr rows = fixture.Allocate(2 * 0x2c);
IntPtr ndpDescriptor = fixture.Allocate(0x4c);
IntPtr initializedRow = fixture.Allocate(0x2c);
constructProviderRow(initializedRow, ndpDescriptor);
uint[] initialWords = new uint[11];
for (int i = 0; i < initialWords.Length; i++)
initialWords[i] = unchecked((uint)Marshal.ReadInt32(initializedRow, i * 4));
uint[] expectedInitialWords = {
unchecked((uint)ndpDescriptor.ToInt32()), 0xffffffff, 0x3f800000,
0x3f800000, 0, 0x3f800000, 0, 1, 0, 0xffffffff, 0
};
Expect(String.Join(",", initialWords) == String.Join(",", expectedInitialWords),
"Control native NDP provider row constructor initializes all eleven words and type-1 current to 1.0");
IntPtr providerInterface = IntPtr.Add(owner, 4);
IntPtr providerVtable = IntPtr.Add(module, ControlProviderInterfaceVtableRva);
IntPtr callbackObject = fixture.Allocate(IntPtr.Size);
IntPtr callbackVtable = fixture.Allocate(0x38);
IntPtr device = fixture.Allocate(0x94);
IntPtr row = IntPtr.Add(rows, 0x2c); // Keep the root row untouched.
Marshal.WriteIntPtr(providerInterface, providerVtable);
Marshal.WriteIntPtr(owner, 0x55c, rows);
Marshal.WriteInt32(owner, 0x548, FloatBits(1.0f));
Marshal.WriteInt32(owner, 0x590, FloatBits(40.0f));
Marshal.WriteInt32(owner, 0x660, FloatBits(1.0f));
Marshal.WriteIntPtr(row, 0x00, ndpDescriptor);
Marshal.WriteInt32(ndpDescriptor, 0x04, FloatBits(100.0f)); // Native selector 3 maximum.
Marshal.WriteInt32(row, 0x04, -1); // Root row has no parent node.
Marshal.WriteInt32(row, 0x08, FloatBits(40.0f));
Marshal.WriteInt32(row, 0x14, FloatBits(0.4f));
Marshal.WriteIntPtr(callbackObject, 0, callbackVtable);
Marshal.WriteIntPtr(callbackVtable, 0x34, Marshal.GetFunctionPointerForDelegate(ControlNodeUpdateCallback));
Marshal.WriteIntPtr(owner, 0x20, callbackObject);
Marshal.WriteInt32(device, 0x04, 1);
Marshal.WriteInt32(device, 0x4c, FloatBits(3.0f));
Marshal.WriteInt32(device, 0x54, FloatBits(2.0f));
Marshal.WriteIntPtr(device, 0x44, providerInterface);
controlNodeUpdateCount = 0;
update(owner, 1, 20.0f, 0);
Expect(ReadFloat(row, 0x08) == 60.0f && ReadFloat(row, 0x14) == 0.6f &&
ReadFloat(owner, 0x590) == 60.0f,
"Control native amount update clamps the node amount, updates the normalized provider value, and keeps the aggregate amount in sync");
Expect(controlNodeUpdateCount == 1 && controlNodeUpdateIndex == 1 &&
controlNodeUpdateSelector == 0x200 && controlNodeUpdateValueBits == unchecked((uint)FloatBits(0.6f)),
"Control native amount update publishes the normalized provider state through the existing node callback");
Expect(unchecked((uint)FloatBits(getter(device, 0x300))) ==
unchecked((uint)FloatBits(0.6f * 2.0f * 3.0f)),
"Control tag-26 selector 0x300 observes the updated NDP provider value through the native provider interface");
update(owner, 1, -10.0f, 0);
Expect(ReadFloat(row, 0x08) == 50.0f && ReadFloat(row, 0x14) == 0.5f &&
unchecked((uint)FloatBits(getter(device, 0x300))) == 0x40400000,
"Control native amount update is reversible and tag-26 selector 0x300 tracks the changed provider row");
update(owner, 1, 1000.0f, 0);
Expect(ReadFloat(row, 0x08) == 100.0f && ReadFloat(row, 0x14) == 1.0f &&
unchecked((uint)FloatBits(getter(device, 0x300))) == 0x40c00000,
"Control native amount update clamps the provider value at its native selector-3 maximum");
update(owner, 1, -200.0f, 0);
Expect(ReadFloat(row, 0x08) == 0.0f && ReadFloat(row, 0x14) == 0.0f &&
(unchecked((uint)Marshal.ReadInt32(row, 0x28)) & 0x10) != 0 &&
unchecked((uint)FloatBits(getter(device, 0x300))) == 0,
"Control native depletion sets the exhausted flag and selector 0x300 returns the live zero provider value");
Expect(controlNodeUpdateCount == 4 && controlNodeUpdateIndex == 1 &&
controlNodeUpdateSelector == 0x200 && controlNodeUpdateValueBits == 0,
"Control native node callback receives every normalized provider update through exhaustion");
Console.WriteLine("native-control-live-ndp-provider: controlHash=" + ExpectedControlSha256 +
" rowCtorRva=0xb240 initialType1=1 rowStride=0x2c node=1 max=100 amount=0..100 tag26Selector300=live");
GC.KeepAlive(constructProviderRow);
GC.KeepAlive(update);
GC.KeepAlive(getter);
}
}
private static void RunNativeControlAggregateResourceCases(IntPtr module) { private static void RunNativeControlAggregateResourceCases(IntPtr module) {
NativeControlAggregateQuery query = (NativeControlAggregateQuery)Marshal.GetDelegateForFunctionPointer( NativeControlAggregateQuery query = (NativeControlAggregateQuery)Marshal.GetDelegateForFunctionPointer(
WriteStdCallToFastcallThunk(0x1c0, IntPtr.Add(module, ControlResourceAggregateQueryRva)), WriteStdCallToFastcallThunk(0x1c0, IntPtr.Add(module, ControlResourceAggregateQueryRva)),
+40 -10
View File
@@ -307,18 +307,48 @@ mappings; preview и объекты без Unit DAT root её не строят.
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26 `bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26
getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по
ключу `fixed_words[1]`, затем умножает provider, CTLD coefficient `fixed_words[11]` ключу `fixed_words[1]`, затем умножает provider, CTLD coefficient `fixed_words[11]`
и поле объекта `+0x4c` именно в таком порядке. Rust helper принимает текущие и поле объекта `+0x4c` именно в таком порядке. Provider getter RVA `0xdc40`
provider и multiplier извне; он не восстанавливает их жизненный цикл или весь читает type-1 поле `+0x14` строки, которую `FUN_10008b10` (RVA `0x8b10`)
Control tick. В x86 oracle при x87 control word `0x007f` проверены порядок, добавляет из NDP в таблицу с шагом `0x2c`; `FUN_1000b240` (RVA `0xb240`)
signed zero, отрицательные входы, расширенный промежуточный диапазон и инициализирует это поле как `1.0`. Начальный Rust snapshot повторяет порядок
subnormal return. Helper ограничивает себя normal-or-zero значениями, поэтому NDP строк корневого ресурса и примонтированных `EXTO` (без первой строки
явно отклоняет промежуточные случаи, где результат выходит за поддерживаемый последнего), сохраняя исходные таблицу и строку как provenance. Ключ устройства
диапазон f32, даже если позднее умножение вернуло бы его в диапазон. Это сначала меняется по подтверждённому MSH mapping для `EXTO`, затем это готовое
ограничение поддержки Rust helper-а, а не неверный native-результат. значение применяется напрямую как индекс provider sequence. Длина этой
sequence определяется NDP: она не дополняется по числу узлов модели и ключ не
пересчитывается ещё раз по числу provider rows. `INTO` provider строк не
добавляет. Для примонтированного `EXTO` с пустым NDP невозможно безопасно
применить native skip-first путь, поэтому Rust явно сообщает unavailable;
таблица из одной строки допустима и даёт нулевой вклад. Если обязательный NDP
sidecar недоступен, запрос `0xe00` сообщает это отдельно, оставляя независимые
агрегатные selectors `1/2` доступными.
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
factory paths остаются явными границами. Snapshot показывает состояние сразу
после native-конструкторов, но он не обновляется при Control tick-е. Native
`FUN_10010f30` (RVA `0x10f30`) изменяет и ограничивает provider amount, а
связанные callbacks переносят его между строками; их общий переход в Rust ещё
не реализован. Поэтому вызов нельзя считать живым ресурсным симулятором.
Behavior selector `0x1002` остаётся отдельной задачей: его множитель
`Transfer_Ore_OffBoard` приходит из блока `+0x820`, но источник фактических
текущих настроек для полного runtime-пути пока не подтверждён.
В x86 oracle при x87 control word `0x007f` проверены порядок, signed zero,
отрицательные входы, расширенный промежуточный диапазон и subnormal return.
Rust запрос ограничивает себя normal-or-zero значениями, поэтому явно отклоняет
промежуточные случаи, где результат выходит за поддерживаемый диапазон f32,
даже если позднее умножение вернуло бы его в диапазон. Это ограничение
поддержки Rust snapshot, а не неверный native-результат.
В Unit DAT fixture `UNITS/BUILDS/MINE/min_l_n1.dat` проверена реальная связка В Unit DAT fixture `UNITS/BUILDS/MINE/min_l_n1.dat` проверена реальная связка
`fr_l_mine.ctl` + `fr_l_mine.ndp`: tag `26`, ключ `2` в таблице из `22` строк, `fr_l_mine.ctl` + `fr_l_mine.ndp`: tag `26`, ключ `2` в локальной NDP таблице
коэффициент `1.0`. Это не запись живого resource-device значения в миссии. из `22` строк, коэффициент `1.0`. Игнорируемый runtime-тест выполняет полный
загрузочный путь, подтверждает root/`EXTO` размеры `22` и `4` и итоговые `25`
provider rows после пропуска первого `EXTO` row. Он вызывает Rust query по
восстановленному native правилу адресации и получает начальное значение `1.0`;
getter оригинальной DLL здесь не исполняется. Это не запись живого
resource-device значения во время игры.
Закрывающий эксперимент: трассировать `LoadControlSystem`, Закрывающий эксперимент: трассировать `LoadControlSystem`,
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать `LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
+44 -15
View File
@@ -732,8 +732,21 @@ Provider interface направляет selector `1` и ключ в Control RVA
читается текущее type-1 значение выбранной строки. Затем selector умножает читается текущее type-1 значение выбранной строки. Затем selector умножает
сначала provider на коэффициент, после чего на множитель: `provider * base * сначала provider на коэффициент, после чего на множитель: `provider * base *
multiplier`. Коэффициент приходит из CTLD; NDP связывает ключ с описанной multiplier`. Коэффициент приходит из CTLD; NDP связывает ключ с описанной
строкой, а текущее значение provider нельзя заменить предположением о состоянии строкой. Native loader создаёт для этой строки отдельное runtime-состояние: у
мира. `Control.dll` (та же сборка, SHA-256
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`)
`FUN_1000b240` (RVA `0xb240`) строит 11 слов строки provider и ставит её
selector-1 поле `+0x14` в `1.0`. `FUN_10008b10` (RVA `0x8b10`) выбирает строки
NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую таблицу с шагом
`0x2c`; getter `FUN_1000dc40` (RVA `0xdc40`) использует ключ устройства как
индекс этой таблицы. Для корня загружаются все строки. `INTO` не добавляет
строк, а примонтированный `EXTO` пропускает свою первую строку. Пустая
таблица такого `EXTO` не содержит строки, которую native loader должен
пропустить, поэтому Rust оставляет provider lookup недоступным; sidecar с одной
строкой даёт пустой вклад. Если mount узел не разрешился, строки этого `EXTO`
в таблицу не попадают. Это отдельная
последовательность provider-строк: её длина берётся из NDP, она не дополняется
до числа узлов MSH и не участвует во второй перенумерации ключа.
Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке
`Behavior.dll` (SHA-256 `Behavior.dll` (SHA-256
@@ -747,24 +760,40 @@ multiplier`. Коэффициент приходит из CTLD; NDP связыв
5.0 — RVA `0x22d90`; config selector 2 загружает блок через RVA `0x0a1a0`). 5.0 — RVA `0x22d90`; config selector 2 загружает блок через RVA `0x0a1a0`).
Отдельный query `0x1005` (RVA `0xa490`) возвращает поле `+0x60` Отдельный query `0x1005` (RVA `0xa490`) возвращает поле `+0x60`
`Transfer_Power_Out` (имя — RVA `0x22e20`, default 100.0 — RVA `0x22d90`; `Transfer_Power_Out` (имя — RVA `0x22e20`, default 100.0 — RVA `0x22d90`;
setter — RVA `0xaa40`). Rust helper пока описывает один Control selector `0x300` setter — RVA `0xaa40`). В полной загрузке FParkan начальные provider-строки
при явно переданных входах; он не реализует эту сумму, Behavior-умножение или хранятся вместе с таблицей Control конкретного `ObjectHandle`. Запрос
подключение к игровому циклу. `0xe00` проходит по ключу устройства в этот снимок и пока поддержан только для
текущего descriptor tag `26` с factory tag `26`; для него результат равен
`provider * fixed_words[11] * multiplier`. Лицензированный тест загружает
оригинальные CTL/NDP и подтверждает, что tag-26 запись с ключом `2` читает
начальное значение строки `1.0`. Это проверка начального состояния после
конструкторов, не результат живого игрового тика.
Поведение provider-строк меняется и после загрузки: например,
`Control.dll!FUN_10010f30` (RVA `0x10f30`) обновляет и ограничивает текущее
значение, а соседние процедуры распространяют его между связанными строками.
Эти изменения пока не отражены в Rust-снимке. Поэтому `0xe00` не является
исполнением всего Control tick-а, а Behavior query `0x1002` не подключён к
этому API; его множитель `Transfer_Ore_OffBoard` требует отдельного источника
текущей конфигурации.
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит, На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
следующем умножении. Rust helper поэтому принимает явные текущие значения следующем умножении. Native oracle проверяет такой порядок для getter-а
provider и multiplier, проверяет tag, ключ и строку NDP, сохраняет порядок `0x300`. Rust запрос сохраняет тот же порядок, но поддерживает только конечные
операций, но поддерживает только конечные нормализованные значения или ноль на нормализованные значения или ноль на входе, промежуточном и конечном этапах.
входе, промежуточном и конечном этапах. Остальные случаи за этим численным Остальные случаи за этим численным диапазоном он отклоняет; полный x87
диапазоном он отклоняет; полный x87 accumulator здесь не эмулируется. accumulator здесь не эмулируется.
Лицензированный тест на Unit DAT `UNITS/BUILDS/MINE/min_l_n1.dat` проходит через Лицензированный тест на Unit DAT `UNITS/BUILDS/MINE/min_l_n1.dat` проходит через
компонент `fr_l_mine.ctl` и sidecar `fr_l_mine.ndp`: у найденного tag-26 `fr_l_mine.ctl` и `fr_l_mine.ndp`: у найденного tag-26 descriptor ключ равен
дескриптора ключ равен `2` из `22` строк, а raw коэффициент — `0x3f800000`. `2` в своей записи, raw коэффициент — `0x3f800000`. Корневой NDP содержит 22
Тест передаёт helper-у искусственные текущие значения `1.0` только для проверки строки, sidecar примонтированного `EXTO` — 4; в общую provider-таблицу входит
записи и привязки ключа; этот Unit DAT не является наблюдением живого Control 25 строк после пропуска локальной строки `0`. Тест также сравнивает порядок
tick-а. всех source-row индексов с порядком строк самих sidecar таблиц. Он вызывает
Rust query по восстановленному native правилу адресации, а не getter оригинальной
DLL на полной миссии. Значение `1.0` подтверждает начальное состояние по native
конструктору, но не наблюдение живого Control tick-а.
### Control system и physical model ### Control system и physical model