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
+977
-73
No files matched your search
@@ -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)),
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user