feat(runtime): load Control node providers for resource queries
Docs Deploy / Build and Deploy MkDocs (push) Failing after 3h0m1s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 3h0m1s
This commit is contained in:
4 files changed
+968
-64
No files matched your search
@@ -1,14 +1,16 @@
|
||||
#![forbid(unsafe_code)]
|
||||
//! Runtime orchestration for mission loading and world stepping.
|
||||
|
||||
#[cfg(test)]
|
||||
use fparkan_assets::PreparedControlResourcePair;
|
||||
use fparkan_assets::{
|
||||
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
||||
derive_mission_land_paths, extend_graph_report_with_visual_dependencies,
|
||||
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
|
||||
AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
|
||||
MissionTerrainPaths, NresError, PreparedComponentControl, PreparedControlEffectSection,
|
||||
PreparedControlTypedRecord, PreparedVisual, TerrainFormatError, TerrainPreparationError,
|
||||
TerrainWorld, TmaProfile,
|
||||
PreparedControlNodeDataRow, PreparedControlNodeDataTable, PreparedControlTypedRecord,
|
||||
PreparedVisual, TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile,
|
||||
};
|
||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
||||
use fparkan_prototype::{
|
||||
@@ -190,6 +192,49 @@ pub struct EffectiveControlDevice {
|
||||
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.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ControlResourceAccessError {
|
||||
@@ -205,6 +250,24 @@ pub enum ControlResourceAccessError {
|
||||
UnsupportedSelector(u32),
|
||||
/// A selected row has a factory class whose getter has not been checked.
|
||||
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.
|
||||
UnsupportedField {
|
||||
factory_tag: u32,
|
||||
@@ -283,6 +346,13 @@ pub struct MissionControlDeviceTableState {
|
||||
pub handle: ObjectHandle,
|
||||
/// Complete base projection, or an explicit unsupported boundary.
|
||||
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.
|
||||
@@ -1223,11 +1293,8 @@ fn load_mission_with_options(
|
||||
.iter()
|
||||
.zip(&handles)
|
||||
.enumerate()
|
||||
.map(
|
||||
|(object_index, (draft, handle))| MissionControlDeviceTableState {
|
||||
original_id: draft.original_id,
|
||||
handle: *handle,
|
||||
status: build_control_device_table(
|
||||
.map(|(object_index, (draft, handle))| {
|
||||
let projection = build_control_device_projection(
|
||||
object_index,
|
||||
draft,
|
||||
&mission_assets,
|
||||
@@ -1235,13 +1302,27 @@ fn load_mission_with_options(
|
||||
.root_prototype_request_spans
|
||||
.get(object_index),
|
||||
&resolved_prototypes,
|
||||
)
|
||||
.map_or_else(
|
||||
MissionControlDeviceTableStatus::Unavailable,
|
||||
MissionControlDeviceTableStatus::Available,
|
||||
);
|
||||
match projection {
|
||||
Ok(projection) => MissionControlDeviceTableState {
|
||||
original_id: draft.original_id,
|
||||
handle: *handle,
|
||||
status: MissionControlDeviceTableStatus::Available(projection.devices),
|
||||
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()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -1652,6 +1733,35 @@ pub fn query_loaded_mission_control_resource(
|
||||
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
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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(
|
||||
table: &mut MissionControlDeviceTableState,
|
||||
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(
|
||||
devices: &[EffectiveControlDevice],
|
||||
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(
|
||||
object_index: usize,
|
||||
draft: &MissionObjectDraft,
|
||||
@@ -2098,6 +2312,17 @@ fn build_control_device_table(
|
||||
prototype_span: Option<&std::ops::Range<usize>>,
|
||||
prototypes: &[fparkan_prototype::EffectivePrototype],
|
||||
) -> 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;
|
||||
|
||||
draft
|
||||
@@ -2250,6 +2475,7 @@ fn build_control_device_table(
|
||||
}
|
||||
|
||||
let mut devices = Vec::new();
|
||||
let mut node_provider_status = MissionControlNodeProviderTableStatus::Available(Vec::new());
|
||||
let mut has_control_resource = false;
|
||||
for component_index in active_components {
|
||||
let prototype =
|
||||
@@ -2286,6 +2512,40 @@ fn build_control_device_table(
|
||||
component_ids[component_index].ok_or(Unavailable::ComponentAlignmentMismatch)?;
|
||||
let is_root = component_index == 0;
|
||||
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 {
|
||||
for (record_index, descriptor) in control.typed_records.iter().enumerate() {
|
||||
append_effective_control_device(
|
||||
@@ -2301,12 +2561,6 @@ fn build_control_device_table(
|
||||
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(
|
||||
prototype.source_type,
|
||||
&devices,
|
||||
@@ -2315,25 +2569,6 @@ fn build_control_device_table(
|
||||
);
|
||||
match native_control_target_action(native_target) {
|
||||
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();
|
||||
for (record_index, descriptor) in control.typed_records.iter().enumerate() {
|
||||
let raw_key = descriptor.fixed_words[1];
|
||||
@@ -2395,7 +2630,49 @@ fn build_control_device_table(
|
||||
if !has_control_resource {
|
||||
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(
|
||||
@@ -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(
|
||||
source_types: &[Option<u32>],
|
||||
kinds: &[u32],
|
||||
@@ -2902,8 +3235,11 @@ mod tests {
|
||||
control_resource: controls[index]
|
||||
.as_ref()
|
||||
.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,
|
||||
node_data_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(
|
||||
factory_tag: u32,
|
||||
descriptor_tag: u32,
|
||||
@@ -2988,6 +3366,16 @@ mod tests {
|
||||
|
||||
fn synthetic_control_device_table(
|
||||
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 {
|
||||
original_id: Some(OriginalObjectId(7)),
|
||||
@@ -2996,6 +3384,7 @@ mod tests {
|
||||
slot: 7,
|
||||
},
|
||||
status,
|
||||
node_provider_status,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3384,6 +3773,10 @@ mod tests {
|
||||
query_loaded_mission_control_resource(&engine, handle, 1),
|
||||
Err(ControlResourceAccessError::MissionUnavailable)
|
||||
);
|
||||
assert_eq!(
|
||||
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
|
||||
Err(ControlResourceAccessError::MissionUnavailable)
|
||||
);
|
||||
|
||||
let stale = ObjectHandle {
|
||||
generation: handle.generation.wrapping_add(1),
|
||||
@@ -3395,6 +3788,12 @@ mod tests {
|
||||
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!(
|
||||
set_loaded_mission_control_multiplier(&mut engine, stale, 2.0),
|
||||
Err(ControlResourceAccessError::World(
|
||||
@@ -3409,6 +3808,12 @@ mod tests {
|
||||
fparkan_world::WorldError::StaleHandle
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
|
||||
Err(ControlResourceAccessError::World(
|
||||
fparkan_world::WorldError::StaleHandle
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn synthetic_into_builder_appends_all_rows_and_overrides_only_first_record() {
|
||||
let append = synthetic_control_table(
|
||||
@@ -4049,6 +4739,49 @@ mod tests {
|
||||
[1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 5]
|
||||
);
|
||||
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!(
|
||||
(
|
||||
entries[9].factory_tag,
|
||||
|
||||
@@ -29,6 +29,8 @@ internal static class BehaviorDistributorTickOracle {
|
||||
private const int ControlResourceAggregateSetRva = 0x2ba30;
|
||||
private const int ControlProviderInterfaceVtableRva = 0x3d254;
|
||||
private const int ControlProviderType1GetterRva = 0x0dc40;
|
||||
private const int ControlProviderRowConstructorRva = 0x0b240;
|
||||
private const int ControlProviderAmountUpdateRva = 0x10f30;
|
||||
private const int ArealMapUnitDatComponentBuilderRva = 0x14b90;
|
||||
private const int ControlResourceDeviceConstructorRva = 0x22980;
|
||||
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.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.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 VectorInsert VectorInsertCallback = InsertVector;
|
||||
@@ -92,7 +97,12 @@ internal static class BehaviorDistributorTickOracle {
|
||||
private static readonly InterfaceGetProperty InterfaceGetCallback = GetInterfaceProperty;
|
||||
private static readonly InterfaceSetProperty InterfaceSetCallback = SetInterfaceProperty;
|
||||
private static readonly NativeArealMapEvent ArealMapEventCallback = OnArealMapEvent;
|
||||
private static readonly NativeControlNodeUpdateCallback ControlNodeUpdateCallback = OnControlNodeUpdate;
|
||||
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 queryObjectThunk;
|
||||
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));
|
||||
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) {
|
||||
if (sequence == null || sequence.Length == 0) return fallback;
|
||||
return sequence[Math.Min(index, sequence.Length - 1)];
|
||||
@@ -493,6 +509,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
|
||||
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
|
||||
GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback);
|
||||
GC.KeepAlive(ControlNodeUpdateCallback);
|
||||
GC.KeepAlive(ArealMapEventCallback);
|
||||
if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease))
|
||||
throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
|
||||
@@ -514,6 +531,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
RunNativeArealMapComponentBuilderCase(arealMapModule);
|
||||
RunNativeControlIntoOverrideCase(controlModule);
|
||||
RunNativeControlResourceDeviceCase(controlModule);
|
||||
RunNativeControlProviderLiveStateCase(controlModule);
|
||||
RunNativeControlAggregateResourceCases(controlModule);
|
||||
} finally {
|
||||
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) {
|
||||
NativeControlAggregateQuery query = (NativeControlAggregateQuery)Marshal.GetDelegateForFunctionPointer(
|
||||
WriteStdCallToFastcallThunk(0x1c0, IntPtr.Add(module, ControlResourceAggregateQueryRva)),
|
||||
|
||||
@@ -307,18 +307,48 @@ mappings; preview и объекты без Unit DAT root её не строят.
|
||||
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26
|
||||
getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по
|
||||
ключу `fixed_words[1]`, затем умножает provider, CTLD coefficient `fixed_words[11]`
|
||||
и поле объекта `+0x4c` именно в таком порядке. Rust helper принимает текущие
|
||||
provider и multiplier извне; он не восстанавливает их жизненный цикл или весь
|
||||
Control tick. В x86 oracle при x87 control word `0x007f` проверены порядок,
|
||||
signed zero, отрицательные входы, расширенный промежуточный диапазон и
|
||||
subnormal return. Helper ограничивает себя normal-or-zero значениями, поэтому
|
||||
явно отклоняет промежуточные случаи, где результат выходит за поддерживаемый
|
||||
диапазон f32, даже если позднее умножение вернуло бы его в диапазон. Это
|
||||
ограничение поддержки Rust helper-а, а не неверный native-результат.
|
||||
и поле объекта `+0x4c` именно в таком порядке. Provider getter RVA `0xdc40`
|
||||
читает type-1 поле `+0x14` строки, которую `FUN_10008b10` (RVA `0x8b10`)
|
||||
добавляет из NDP в таблицу с шагом `0x2c`; `FUN_1000b240` (RVA `0xb240`)
|
||||
инициализирует это поле как `1.0`. Начальный Rust snapshot повторяет порядок
|
||||
NDP строк корневого ресурса и примонтированных `EXTO` (без первой строки
|
||||
последнего), сохраняя исходные таблицу и строку как provenance. Ключ устройства
|
||||
сначала меняется по подтверждённому MSH mapping для `EXTO`, затем это готовое
|
||||
значение применяется напрямую как индекс 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` проверена реальная связка
|
||||
`fr_l_mine.ctl` + `fr_l_mine.ndp`: tag `26`, ключ `2` в таблице из `22` строк,
|
||||
коэффициент `1.0`. Это не запись живого resource-device значения в миссии.
|
||||
`fr_l_mine.ctl` + `fr_l_mine.ndp`: tag `26`, ключ `2` в локальной NDP таблице
|
||||
из `22` строк, коэффициент `1.0`. Игнорируемый runtime-тест выполняет полный
|
||||
загрузочный путь, подтверждает root/`EXTO` размеры `22` и `4` и итоговые `25`
|
||||
provider rows после пропуска первого `EXTO` row. Он вызывает Rust query по
|
||||
восстановленному native правилу адресации и получает начальное значение `1.0`;
|
||||
getter оригинальной DLL здесь не исполняется. Это не запись живого
|
||||
resource-device значения во время игры.
|
||||
|
||||
Закрывающий эксперимент: трассировать `LoadControlSystem`,
|
||||
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
|
||||
|
||||
+44
-15
@@ -732,8 +732,21 @@ Provider interface направляет selector `1` и ключ в Control RVA
|
||||
читается текущее type-1 значение выбранной строки. Затем selector умножает
|
||||
сначала provider на коэффициент, после чего на множитель: `provider * base *
|
||||
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
|
||||
@@ -747,24 +760,40 @@ multiplier`. Коэффициент приходит из CTLD; NDP связыв
|
||||
5.0 — RVA `0x22d90`; config selector 2 загружает блок через RVA `0x0a1a0`).
|
||||
Отдельный query `0x1005` (RVA `0xa490`) возвращает поле `+0x60`
|
||||
`Transfer_Power_Out` (имя — RVA `0x22e20`, default 100.0 — RVA `0x22d90`;
|
||||
setter — RVA `0xaa40`). Rust helper пока описывает один Control selector `0x300`
|
||||
при явно переданных входах; он не реализует эту сумму, Behavior-умножение или
|
||||
подключение к игровому циклу.
|
||||
setter — RVA `0xaa40`). В полной загрузке FParkan начальные provider-строки
|
||||
хранятся вместе с таблицей 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 бит,
|
||||
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
|
||||
следующем умножении. Rust helper поэтому принимает явные текущие значения
|
||||
provider и multiplier, проверяет tag, ключ и строку NDP, сохраняет порядок
|
||||
операций, но поддерживает только конечные нормализованные значения или ноль на
|
||||
входе, промежуточном и конечном этапах. Остальные случаи за этим численным
|
||||
диапазоном он отклоняет; полный x87 accumulator здесь не эмулируется.
|
||||
следующем умножении. Native oracle проверяет такой порядок для getter-а
|
||||
`0x300`. Rust запрос сохраняет тот же порядок, но поддерживает только конечные
|
||||
нормализованные значения или ноль на входе, промежуточном и конечном этапах.
|
||||
Остальные случаи за этим численным диапазоном он отклоняет; полный x87
|
||||
accumulator здесь не эмулируется.
|
||||
|
||||
Лицензированный тест на Unit DAT `UNITS/BUILDS/MINE/min_l_n1.dat` проходит через
|
||||
компонент `fr_l_mine.ctl` и sidecar `fr_l_mine.ndp`: у найденного tag-26
|
||||
дескриптора ключ равен `2` из `22` строк, а raw коэффициент — `0x3f800000`.
|
||||
Тест передаёт helper-у искусственные текущие значения `1.0` только для проверки
|
||||
записи и привязки ключа; этот Unit DAT не является наблюдением живого Control
|
||||
tick-а.
|
||||
`fr_l_mine.ctl` и `fr_l_mine.ndp`: у найденного tag-26 descriptor ключ равен
|
||||
`2` в своей записи, raw коэффициент — `0x3f800000`. Корневой NDP содержит 22
|
||||
строки, sidecar примонтированного `EXTO` — 4; в общую provider-таблицу входит
|
||||
25 строк после пропуска локальной строки `0`. Тест также сравнивает порядок
|
||||
всех source-row индексов с порядком строк самих sidecar таблиц. Он вызывает
|
||||
Rust query по восстановленному native правилу адресации, а не getter оригинальной
|
||||
DLL на полной миссии. Значение `1.0` подтверждает начальное состояние по native
|
||||
конструктору, но не наблюдение живого Control tick-а.
|
||||
|
||||
### Control system и physical model
|
||||
|
||||
|
||||
Reference in new issue
Block a user