feat(runtime): load Behavior profiles for resource queries
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m14s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m14s
This commit is contained in:
7 files changed
+1088
-24
No files matched your search
Generated
+1
@@ -469,6 +469,7 @@ version = "0.1.0"
|
|||||||
name = "fparkan-assets"
|
name = "fparkan-assets"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fparkan-binary",
|
||||||
"fparkan-material",
|
"fparkan-material",
|
||||||
"fparkan-mission-format",
|
"fparkan-mission-format",
|
||||||
"fparkan-msh",
|
"fparkan-msh",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
fparkan-material = { path = "../fparkan-material" }
|
fparkan-material = { path = "../fparkan-material" }
|
||||||
fparkan-msh = { path = "../fparkan-msh" }
|
fparkan-msh = { path = "../fparkan-msh" }
|
||||||
fparkan-nres = { path = "../fparkan-nres" }
|
fparkan-nres = { path = "../fparkan-nres" }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
//! Asset manager ports and transactional preparation models.
|
//! Asset manager ports and transactional preparation models.
|
||||||
|
|
||||||
|
use fparkan_binary::{checked_count_bytes, read_lp_bytes, Cursor, DecodeError};
|
||||||
use fparkan_material::{
|
use fparkan_material::{
|
||||||
decode_mat0, decode_wear, resolve_material, MaterialFallback, ResolvedMaterial, WearTable,
|
decode_mat0, decode_wear, resolve_material, MaterialFallback, ResolvedMaterial, WearTable,
|
||||||
MAT0_KIND, WEAR_KIND,
|
MAT0_KIND, WEAR_KIND,
|
||||||
@@ -62,6 +63,150 @@ pub struct PreparedMaterialPhase {
|
|||||||
pub page_uv_transform: [f32; 4],
|
pub page_uv_transform: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The two Behavior profile values currently used by recovered resource queries.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BehaviorProfileRates {
|
||||||
|
/// Second raw value blob for `Transfer_Ore_OffBoard`.
|
||||||
|
pub transfer_ore_off_board_bits: u32,
|
||||||
|
/// Second raw value blob for `Transfer_Power_Out`.
|
||||||
|
pub transfer_power_out_bits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error while reading a bounded binary Behavior `.var` profile.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BehaviorProfileVarError {
|
||||||
|
/// The binary record framing is malformed.
|
||||||
|
Decode(DecodeError),
|
||||||
|
/// A bound for one of the two fields used here is not a 32-bit float.
|
||||||
|
InvalidFloatField {
|
||||||
|
name: &'static str,
|
||||||
|
size: u32,
|
||||||
|
type_index: u32,
|
||||||
|
},
|
||||||
|
/// A registered runtime field was absent from the profile.
|
||||||
|
MissingField { name: &'static str },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BehaviorProfileVarError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Decode(source) => write!(f, "{source}"),
|
||||||
|
Self::InvalidFloatField {
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type_index,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"field {name} has size {size} and type index {type_index}, expected a 4-byte float"
|
||||||
|
),
|
||||||
|
Self::MissingField { name } => write!(f, "required field {name} is missing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for BehaviorProfileVarError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Decode(source) => Some(source),
|
||||||
|
Self::InvalidFloatField { .. } | Self::MissingField { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DecodeError> for BehaviorProfileVarError {
|
||||||
|
fn from(value: DecodeError) -> Self {
|
||||||
|
Self::Decode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the two Behavior rate fields from a native binary `.var` profile.
|
||||||
|
///
|
||||||
|
/// Native records contain two equally sized value blobs. After binding a
|
||||||
|
/// registered destination, the original applies the second blob. Field names
|
||||||
|
/// are matched case-sensitively as C strings (up to the first NUL), and the
|
||||||
|
/// first occurrence wins. Other fields are consumed but their meanings remain
|
||||||
|
/// outside this decoder.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`BehaviorProfileVarError`] when record framing is truncated,
|
||||||
|
/// contains trailing bytes, omits a registered field, or declares a selected
|
||||||
|
/// field that is not a float row.
|
||||||
|
pub fn decode_behavior_profile_rates(
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<BehaviorProfileRates, BehaviorProfileVarError> {
|
||||||
|
let mut cursor = Cursor::new(bytes);
|
||||||
|
let count = cursor.read_u32_le()?;
|
||||||
|
checked_count_bytes(u64::from(count), 28, cursor.remaining() as u64)?;
|
||||||
|
|
||||||
|
let mut transfer_ore_off_board_bits = None;
|
||||||
|
let mut transfer_power_out_bits = None;
|
||||||
|
for _ in 0..count {
|
||||||
|
let size = cursor.read_u32_le()?;
|
||||||
|
let type_index = cursor.read_u32_le()?;
|
||||||
|
let _flags = cursor.read_u32_le()?;
|
||||||
|
let field_name = read_lp_bytes(&mut cursor, 64 * 1024)?;
|
||||||
|
let c_string_name = field_name
|
||||||
|
.split(|byte| *byte == 0)
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let target_name = match c_string_name {
|
||||||
|
b"Transfer_Ore_OffBoard" => Some("Transfer_Ore_OffBoard"),
|
||||||
|
b"Transfer_Power_Out" => Some("Transfer_Power_Out"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let _type_name = read_lp_bytes(&mut cursor, 64 * 1024)?;
|
||||||
|
let value_size = usize::try_from(size).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
cursor.read_exact(value_size)?;
|
||||||
|
let capture_target = target_name.is_some_and(|name| {
|
||||||
|
if name == "Transfer_Ore_OffBoard" {
|
||||||
|
transfer_ore_off_board_bits.is_none()
|
||||||
|
} else {
|
||||||
|
transfer_power_out_bits.is_none()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let second_value_bits = if capture_target {
|
||||||
|
let name = target_name.expect("capture requires a target field");
|
||||||
|
if size != 4 || type_index != 3 {
|
||||||
|
return Err(BehaviorProfileVarError::InvalidFloatField {
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let value = cursor.read_exact(value_size)?;
|
||||||
|
Some(u32::from_le_bytes(
|
||||||
|
value.try_into().expect("validated four-byte float row"),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
cursor.read_exact(value_size)?;
|
||||||
|
None
|
||||||
|
};
|
||||||
|
cursor.read_exact(8)?;
|
||||||
|
|
||||||
|
if let (Some(name), Some(value_bits)) = (target_name, second_value_bits) {
|
||||||
|
if name == "Transfer_Ore_OffBoard" {
|
||||||
|
transfer_ore_off_board_bits = Some(value_bits);
|
||||||
|
} else {
|
||||||
|
transfer_power_out_bits = Some(value_bits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.require_eof()?;
|
||||||
|
Ok(BehaviorProfileRates {
|
||||||
|
transfer_ore_off_board_bits: transfer_ore_off_board_bits.ok_or(
|
||||||
|
BehaviorProfileVarError::MissingField {
|
||||||
|
name: "Transfer_Ore_OffBoard",
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
transfer_power_out_bits: transfer_power_out_bits.ok_or(
|
||||||
|
BehaviorProfileVarError::MissingField {
|
||||||
|
name: "Transfer_Power_Out",
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type WearValidationCache =
|
type WearValidationCache =
|
||||||
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
|
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
|
||||||
|
|
||||||
@@ -4222,6 +4367,100 @@ mod tests {
|
|||||||
// Preserve the authored CTLD fixture bits; this is not the `f32` value of tau.
|
// Preserve the authored CTLD fixture bits; this is not the `f32` value of tau.
|
||||||
const AUTHORED_TAG21_RAW_PARAMETER_3: f32 = f32::from_bits(0x40C8_F5C3);
|
const AUTHORED_TAG21_RAW_PARAMETER_3: f32 = f32::from_bits(0x40C8_F5C3);
|
||||||
|
|
||||||
|
fn behavior_var_row(name: &[u8], type_index: u32, first: u32, second: u32) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(&4_u32.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&type_index.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&0_u32.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&(name.len() as u32).to_le_bytes());
|
||||||
|
bytes.extend_from_slice(name);
|
||||||
|
bytes.extend_from_slice(&0_u32.to_le_bytes()); // Native transfer rows use an empty type-name.
|
||||||
|
bytes.extend_from_slice(&first.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&second.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&0_u32.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&0_u32.to_le_bytes());
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn behavior_var(rows: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(&(rows.len() as u32).to_le_bytes());
|
||||||
|
for row in rows {
|
||||||
|
bytes.extend_from_slice(row);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_profile_decoder_uses_first_exact_rows_second_blob_and_float_kind() {
|
||||||
|
let profile = behavior_var(&[
|
||||||
|
behavior_var_row(b"transfer_ore_off_board", 3, 0x1111_1111, 0x2222_2222),
|
||||||
|
behavior_var_row(
|
||||||
|
b"Transfer_Ore_OffBoard\0legacy-suffix",
|
||||||
|
3,
|
||||||
|
0x3333_3333,
|
||||||
|
0x3f80_0000,
|
||||||
|
),
|
||||||
|
behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0x4444_4444, 0x4000_0000),
|
||||||
|
behavior_var_row(b"Transfer_Power_Out", 3, 0x5555_5555, 0x4120_0000),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
decode_behavior_profile_rates(&profile),
|
||||||
|
Ok(BehaviorProfileRates {
|
||||||
|
transfer_ore_off_board_bits: 0x3f80_0000,
|
||||||
|
transfer_power_out_bits: 0x4120_0000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_profile_decoder_rejects_every_truncation_and_trailing_bytes() {
|
||||||
|
let valid = behavior_var(&[
|
||||||
|
behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0, 0x3f80_0000),
|
||||||
|
behavior_var_row(b"Transfer_Power_Out", 3, 0, 0x4120_0000),
|
||||||
|
]);
|
||||||
|
for end in 0..valid.len() {
|
||||||
|
assert!(
|
||||||
|
decode_behavior_profile_rates(&valid[..end]).is_err(),
|
||||||
|
"prefix ending at byte {end} was accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut with_trailing = valid.clone();
|
||||||
|
with_trailing.push(0);
|
||||||
|
assert!(matches!(
|
||||||
|
decode_behavior_profile_rates(&with_trailing),
|
||||||
|
Err(BehaviorProfileVarError::Decode(
|
||||||
|
fparkan_binary::DecodeError::TrailingBytes { .. }
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_profile_decoder_requires_both_registered_float_fields() {
|
||||||
|
let profile = behavior_var(&[behavior_var_row(b"Transfer_Power_Out", 3, 0, 0x4120_0000)]);
|
||||||
|
assert_eq!(
|
||||||
|
decode_behavior_profile_rates(&profile),
|
||||||
|
Err(BehaviorProfileVarError::MissingField {
|
||||||
|
name: "Transfer_Ore_OffBoard"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let profile = behavior_var(&[
|
||||||
|
behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0, 0x3f80_0000),
|
||||||
|
behavior_var_row(b"Transfer_Power_Out", 1, 0, 0x4120_0000),
|
||||||
|
]);
|
||||||
|
assert!(matches!(
|
||||||
|
decode_behavior_profile_rates(&profile),
|
||||||
|
Err(BehaviorProfileVarError::InvalidFloatField {
|
||||||
|
name: "Transfer_Power_Out",
|
||||||
|
size: 4,
|
||||||
|
type_index: 1,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
fn resource_device_record(tag: u32, key: u32, base_bits: u32) -> PreparedControlTypedRecord {
|
fn resource_device_record(tag: u32, key: u32, base_bits: u32) -> PreparedControlTypedRecord {
|
||||||
let mut fixed_words = [0; 44];
|
let mut fixed_words = [0; 44];
|
||||||
fixed_words[1] = key;
|
fixed_words[1] = key;
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ 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, BehaviorProfileRates, BehaviorProfileVarError, BuildCategory,
|
||||||
MissionTerrainPaths, NresError, PreparedComponentControl, PreparedControlEffectSection,
|
MissionAssetPlan, MissionDocument, MissionError, MissionTerrainPaths, NresError,
|
||||||
PreparedControlNodeDataRow, PreparedControlNodeDataTable, PreparedControlTypedRecord,
|
PreparedComponentControl, PreparedControlEffectSection, PreparedControlNodeDataRow,
|
||||||
PreparedVisual, TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile,
|
PreparedControlNodeDataTable, PreparedControlTypedRecord, 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::{
|
||||||
@@ -30,6 +31,7 @@ use fparkan_world::{
|
|||||||
step, transform_state, InputSnapshot, ObjectDraft, ObjectHandle, OriginalObjectId,
|
step, transform_state, InputSnapshot, ObjectDraft, ObjectHandle, OriginalObjectId,
|
||||||
TransformState, World, WorldConfig, WorldSnapshot,
|
TransformState, World, WorldConfig, WorldSnapshot,
|
||||||
};
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -153,6 +155,23 @@ pub struct BehaviorOreState {
|
|||||||
pub maximum_ore_bits: u32,
|
pub maximum_ore_bits: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initial Behavior rates loaded for one full Unit DAT mission object.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BehaviorProfileState {
|
||||||
|
/// Stable identity from the source TMA object order.
|
||||||
|
pub original_id: OriginalObjectId,
|
||||||
|
/// Corresponding live Rust world object.
|
||||||
|
pub handle: ObjectHandle,
|
||||||
|
/// Class word recovered from the root Unit DAT header.
|
||||||
|
pub class_flags: u32,
|
||||||
|
/// Profile selected by the native class mapper; unknown classes use defaults.
|
||||||
|
pub profile_entry: Option<&'static str>,
|
||||||
|
/// Raw bits of `Transfer_Ore_OffBoard` after applying the profile.
|
||||||
|
pub transfer_ore_off_board_bits: u32,
|
||||||
|
/// Raw bits of `Transfer_Power_Out` after applying the profile.
|
||||||
|
pub transfer_power_out_bits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Origin of a Control descriptor retained in the effective table projection.
|
/// Origin of a Control descriptor retained in the effective table projection.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ControlDeviceRecordProvenance {
|
pub struct ControlDeviceRecordProvenance {
|
||||||
@@ -246,7 +265,7 @@ pub enum ControlResourceAccessError {
|
|||||||
ObjectUnavailable { handle: ObjectHandle },
|
ObjectUnavailable { handle: ObjectHandle },
|
||||||
/// The object's Control table could not be completely projected.
|
/// The object's Control table could not be completely projected.
|
||||||
TableUnavailable(MissionControlDeviceTableUnavailableReason),
|
TableUnavailable(MissionControlDeviceTableUnavailableReason),
|
||||||
/// This wrapper only implements native resource selectors 1 and 2.
|
/// The requested resource selector is not implemented for this operation.
|
||||||
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 },
|
||||||
@@ -355,6 +374,15 @@ struct ControlDeviceProjection {
|
|||||||
node_provider_status: MissionControlNodeProviderTableStatus,
|
node_provider_status: MissionControlNodeProviderTableStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PreparedBehaviorProfileState {
|
||||||
|
original_id: OriginalObjectId,
|
||||||
|
class_flags: u32,
|
||||||
|
profile_entry: Option<&'static str>,
|
||||||
|
transfer_ore_off_board_bits: u32,
|
||||||
|
transfer_power_out_bits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Resource distributor state associated with one TMA clan's constructed AI.
|
/// Resource distributor state associated with one TMA clan's constructed AI.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct BehaviorDistributorState {
|
pub struct BehaviorDistributorState {
|
||||||
@@ -688,6 +716,7 @@ struct LoadedMissionState {
|
|||||||
object_drafts: Vec<MissionObjectDraft>,
|
object_drafts: Vec<MissionObjectDraft>,
|
||||||
behavior_distributors: Vec<BehaviorDistributorState>,
|
behavior_distributors: Vec<BehaviorDistributorState>,
|
||||||
behavior_ore_states: Vec<BehaviorOreState>,
|
behavior_ore_states: Vec<BehaviorOreState>,
|
||||||
|
behavior_profiles: Vec<BehaviorProfileState>,
|
||||||
control_device_tables: Vec<MissionControlDeviceTableState>,
|
control_device_tables: Vec<MissionControlDeviceTableState>,
|
||||||
script_bundles: Vec<MissionScriptBundle>,
|
script_bundles: Vec<MissionScriptBundle>,
|
||||||
script_varset: Option<MissionScriptVarSet>,
|
script_varset: Option<MissionScriptVarSet>,
|
||||||
@@ -795,6 +824,15 @@ pub enum EngineError {
|
|||||||
/// Source error.
|
/// Source error.
|
||||||
source: AssetPreparationError,
|
source: AssetPreparationError,
|
||||||
},
|
},
|
||||||
|
/// A native Behavior profile could not be found or decoded.
|
||||||
|
BehaviorProfile {
|
||||||
|
/// Behavior profile archive path.
|
||||||
|
path: String,
|
||||||
|
/// Selected `.var` entry name.
|
||||||
|
entry: String,
|
||||||
|
/// Entry, field, or format diagnostic.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
/// World error.
|
/// World error.
|
||||||
World(fparkan_world::WorldError),
|
World(fparkan_world::WorldError),
|
||||||
/// Reference movement input or terrain query failed.
|
/// Reference movement input or terrain query failed.
|
||||||
@@ -917,6 +955,11 @@ impl std::fmt::Display for EngineError {
|
|||||||
Self::AssetPreparation { mission, source } => {
|
Self::AssetPreparation { mission, source } => {
|
||||||
write!(f, "{mission}: asset preparation failed: {source}")
|
write!(f, "{mission}: asset preparation failed: {source}")
|
||||||
}
|
}
|
||||||
|
Self::BehaviorProfile {
|
||||||
|
path,
|
||||||
|
entry,
|
||||||
|
message,
|
||||||
|
} => write!(f, "{path}:{entry}: Behavior profile failed: {message}"),
|
||||||
Self::World(source) => write!(f, "{source}"),
|
Self::World(source) => write!(f, "{source}"),
|
||||||
Self::Movement(message) => write!(f, "reference movement: {message}"),
|
Self::Movement(message) => write!(f, "reference movement: {message}"),
|
||||||
Self::RegistrationTeardown {
|
Self::RegistrationTeardown {
|
||||||
@@ -950,6 +993,7 @@ impl std::error::Error for EngineError {
|
|||||||
| Self::ScriptRuntime { .. }
|
| Self::ScriptRuntime { .. }
|
||||||
| Self::Movement(_)
|
| Self::Movement(_)
|
||||||
| Self::PrototypeGraph { .. }
|
| Self::PrototypeGraph { .. }
|
||||||
|
| Self::BehaviorProfile { .. }
|
||||||
| Self::RegistrationTeardown { .. } => None,
|
| Self::RegistrationTeardown { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,6 +1229,11 @@ fn load_mission_with_options(
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let prepared_behavior_profiles = if options.asset_scope == MissionAssetScope::Full {
|
||||||
|
prepare_behavior_profile_states(&vfs, &object_drafts)?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
let mut new_runtime_world = new_world(WorldConfig);
|
let mut new_runtime_world = new_world(WorldConfig);
|
||||||
let mut handles = Vec::with_capacity(mission.objects.len());
|
let mut handles = Vec::with_capacity(mission.objects.len());
|
||||||
for (index, object) in mission.objects.iter().enumerate() {
|
for (index, object) in mission.objects.iter().enumerate() {
|
||||||
@@ -1201,6 +1250,20 @@ fn load_mission_with_options(
|
|||||||
)?;
|
)?;
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
let behavior_profiles = prepared_behavior_profiles
|
||||||
|
.into_iter()
|
||||||
|
.zip(&handles)
|
||||||
|
.filter_map(|(profile, handle)| {
|
||||||
|
profile.map(|profile| BehaviorProfileState {
|
||||||
|
original_id: profile.original_id,
|
||||||
|
handle: *handle,
|
||||||
|
class_flags: profile.class_flags,
|
||||||
|
profile_entry: profile.profile_entry,
|
||||||
|
transfer_ore_off_board_bits: profile.transfer_ore_off_board_bits,
|
||||||
|
transfer_power_out_bits: profile.transfer_power_out_bits,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let mut registered_objects = 0;
|
let mut registered_objects = 0;
|
||||||
for handle in &handles {
|
for handle in &handles {
|
||||||
if options.fail_after_registered_objects == Some(registered_objects) {
|
if options.fail_after_registered_objects == Some(registered_objects) {
|
||||||
@@ -1404,6 +1467,7 @@ fn load_mission_with_options(
|
|||||||
object_drafts,
|
object_drafts,
|
||||||
behavior_distributors,
|
behavior_distributors,
|
||||||
behavior_ore_states,
|
behavior_ore_states,
|
||||||
|
behavior_profiles,
|
||||||
control_device_tables,
|
control_device_tables,
|
||||||
script_bundles: loaded_scripts.bundles,
|
script_bundles: loaded_scripts.bundles,
|
||||||
script_varset: loaded_scripts.varset,
|
script_varset: loaded_scripts.varset,
|
||||||
@@ -1696,6 +1760,15 @@ pub fn loaded_mission_behavior_ore_states(engine: &Engine) -> Option<&[BehaviorO
|
|||||||
.map(|state| state.behavior_ore_states.as_slice())
|
.map(|state| state.behavior_ore_states.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns profile-derived Behavior rates attached to full Unit DAT objects.
|
||||||
|
#[must_use]
|
||||||
|
pub fn loaded_mission_behavior_profiles(engine: &Engine) -> Option<&[BehaviorProfileState]> {
|
||||||
|
engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.behavior_profiles.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the base Control device table projections for loaded mission objects.
|
/// Returns the base Control device table projections for loaded mission objects.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn loaded_mission_control_device_tables(
|
pub fn loaded_mission_control_device_tables(
|
||||||
@@ -1762,6 +1835,51 @@ pub fn query_loaded_mission_control_device_selector(
|
|||||||
query_control_device_table_selector(table, device_index, selector)
|
query_control_device_table_selector(table, device_index, selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queries the recovered Behavior resource selectors `0x1002` and `0x1005`.
|
||||||
|
///
|
||||||
|
/// Selector `0x1005` returns the loaded profile's `Transfer_Power_Out` value.
|
||||||
|
/// Selector `0x1002` sums supported Control `0xe00` results in native table
|
||||||
|
/// order, rounding each addition to `f32`, then multiplies by the profile's
|
||||||
|
/// `Transfer_Ore_OffBoard` value. Unsupported table/query paths return an error.
|
||||||
|
/// Profile and provider values are initial full-load snapshots; later damage,
|
||||||
|
/// Control updates, and profile changes are not synchronized into this state.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error for an invalid handle, unavailable mission/profile/table,
|
||||||
|
/// unsupported selector or Control query path, or an unsupported float range.
|
||||||
|
pub fn query_loaded_mission_behavior_resource(
|
||||||
|
engine: &Engine,
|
||||||
|
handle: ObjectHandle,
|
||||||
|
selector: u32,
|
||||||
|
) -> Result<f32, ControlResourceAccessError> {
|
||||||
|
transform_state(&engine.world, handle).map_err(ControlResourceAccessError::World)?;
|
||||||
|
let loaded = engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ControlResourceAccessError::MissionUnavailable)?;
|
||||||
|
let profile = loaded
|
||||||
|
.behavior_profiles
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.handle == handle)
|
||||||
|
.ok_or(ControlResourceAccessError::ObjectUnavailable { handle })?;
|
||||||
|
match selector {
|
||||||
|
0x1005 => checked_control_float(
|
||||||
|
0,
|
||||||
|
"Transfer_Power_Out",
|
||||||
|
f32::from_bits(profile.transfer_power_out_bits),
|
||||||
|
),
|
||||||
|
0x1002 => {
|
||||||
|
let table = loaded
|
||||||
|
.control_device_tables
|
||||||
|
.iter()
|
||||||
|
.find(|table| table.handle == handle)
|
||||||
|
.ok_or(ControlResourceAccessError::ObjectUnavailable { handle })?;
|
||||||
|
query_behavior_profile_ore_rate(table, profile.transfer_ore_off_board_bits)
|
||||||
|
}
|
||||||
|
_ => Err(ControlResourceAccessError::UnsupportedSelector(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.
|
||||||
///
|
///
|
||||||
@@ -1910,6 +2028,37 @@ fn query_control_device_table_selector(
|
|||||||
.map(Some)
|
.map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_behavior_profile_ore_rate(
|
||||||
|
table: &MissionControlDeviceTableState,
|
||||||
|
transfer_ore_off_board_bits: u32,
|
||||||
|
) -> Result<f32, ControlResourceAccessError> {
|
||||||
|
let devices = match &table.status {
|
||||||
|
MissionControlDeviceTableStatus::Available(devices) => devices,
|
||||||
|
MissionControlDeviceTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::TableUnavailable(reason.clone()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut total = 0.0_f32;
|
||||||
|
for (device_index, device) in devices.iter().enumerate() {
|
||||||
|
if device.descriptor.tag != 26 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(value) = query_control_device_table_selector(table, device_index, 0xe00)? {
|
||||||
|
total = checked_control_sum(26, "Behavior resource sum", total, value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checked_control_product(
|
||||||
|
26,
|
||||||
|
"Transfer_Ore_OffBoard",
|
||||||
|
total,
|
||||||
|
checked_control_float(
|
||||||
|
26,
|
||||||
|
"Transfer_Ore_OffBoard",
|
||||||
|
f32::from_bits(transfer_ore_off_board_bits),
|
||||||
|
)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn set_control_device_table_multiplier(
|
fn set_control_device_table_multiplier(
|
||||||
table: &mut MissionControlDeviceTableState,
|
table: &mut MissionControlDeviceTableState,
|
||||||
value: f32,
|
value: f32,
|
||||||
@@ -2079,6 +2228,123 @@ fn native_behavior_ore_defaults(class_flags: u32) -> (u32, u32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn behavior_profile_entry_for_class(class_flags: u32) -> Option<&'static str> {
|
||||||
|
match class_flags {
|
||||||
|
0x8000_0004 => Some("prof_mine.var"),
|
||||||
|
0x8000_0008 => Some("prof_storage.var"),
|
||||||
|
0x8000_0002 => Some("prof_generator.var"),
|
||||||
|
0x8000_0010 => Some("prof_plant.var"),
|
||||||
|
0x8000_0040 => Some("prof_angar.var"),
|
||||||
|
0x8000_0200 => Some("prof_mtp.var"),
|
||||||
|
0x8000_0400 => Some("prof_institute.var"),
|
||||||
|
0x8001_0000 | 0x8002_0000 | 0x8004_0000 => Some("prof_bunker.var"),
|
||||||
|
0x8010_0000 | 0x8020_0000 => Some("prof_tower.var"),
|
||||||
|
0x0100_2000 => Some("prof_trn.var"),
|
||||||
|
0x0100_4000 => Some("prof_bld.var"),
|
||||||
|
0x0100_8000 => Some("prof_war.var"),
|
||||||
|
0x0101_0000 => Some("prof_hq.var"),
|
||||||
|
0x0102_0000 => Some("prof_hero.var"),
|
||||||
|
0x2000_0000 => Some("prof_animal.var"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_behavior_profile_states(
|
||||||
|
vfs: &Arc<dyn Vfs>,
|
||||||
|
object_drafts: &[MissionObjectDraft],
|
||||||
|
) -> Result<Vec<Option<PreparedBehaviorProfileState>>, EngineError> {
|
||||||
|
const DEFAULT_TRANSFER_ORE_OFF_BOARD_BITS: u32 = 0x40a0_0000;
|
||||||
|
const DEFAULT_TRANSFER_POWER_OUT_BITS: u32 = 0x42c8_0000;
|
||||||
|
|
||||||
|
let has_known_profile = object_drafts.iter().any(|draft| {
|
||||||
|
draft
|
||||||
|
.unit_dat_header_flags
|
||||||
|
.and_then(behavior_profile_entry_for_class)
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
|
let archive_path = has_known_profile
|
||||||
|
.then(|| normalize_engine_path("Behavior profile archive", "behpsp.res"))
|
||||||
|
.transpose()?;
|
||||||
|
let profile_archive = archive_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| {
|
||||||
|
let bytes = read_vfs(vfs, path)?;
|
||||||
|
decode_nres_payload(bytes).map_err(|source| EngineError::Nres {
|
||||||
|
path: path.as_str().to_string(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let mut rates_by_entry = HashMap::<&'static str, BehaviorProfileRates>::new();
|
||||||
|
let mut states = Vec::with_capacity(object_drafts.len());
|
||||||
|
|
||||||
|
for draft in object_drafts {
|
||||||
|
let Some(class_flags) = draft.unit_dat_header_flags else {
|
||||||
|
states.push(None);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(original_id) = draft.original_id else {
|
||||||
|
states.push(None);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let profile_entry = behavior_profile_entry_for_class(class_flags);
|
||||||
|
let (transfer_ore_off_board_bits, transfer_power_out_bits) = if let Some(entry_name) =
|
||||||
|
profile_entry
|
||||||
|
{
|
||||||
|
let rates = if let Some(rates) = rates_by_entry.get(entry_name) {
|
||||||
|
*rates
|
||||||
|
} else {
|
||||||
|
let path = archive_path
|
||||||
|
.as_ref()
|
||||||
|
.expect("known profiles require the profile archive path");
|
||||||
|
let archive = profile_archive
|
||||||
|
.as_ref()
|
||||||
|
.expect("known profiles require the decoded profile archive");
|
||||||
|
let entry_id =
|
||||||
|
archive
|
||||||
|
.find(entry_name)
|
||||||
|
.ok_or_else(|| EngineError::BehaviorProfile {
|
||||||
|
path: path.as_str().to_string(),
|
||||||
|
entry: entry_name.to_string(),
|
||||||
|
message: "entry was not found".to_string(),
|
||||||
|
})?;
|
||||||
|
let payload = archive
|
||||||
|
.payload(entry_id)
|
||||||
|
.map_err(|source| EngineError::Nres {
|
||||||
|
path: path.as_str().to_string(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let rates = fparkan_assets::decode_behavior_profile_rates(payload).map_err(
|
||||||
|
|source: BehaviorProfileVarError| EngineError::BehaviorProfile {
|
||||||
|
path: path.as_str().to_string(),
|
||||||
|
entry: entry_name.to_string(),
|
||||||
|
message: source.to_string(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
rates_by_entry.insert(entry_name, rates);
|
||||||
|
rates
|
||||||
|
};
|
||||||
|
(
|
||||||
|
rates.transfer_ore_off_board_bits,
|
||||||
|
rates.transfer_power_out_bits,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
DEFAULT_TRANSFER_ORE_OFF_BOARD_BITS,
|
||||||
|
DEFAULT_TRANSFER_POWER_OUT_BITS,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
states.push(Some(PreparedBehaviorProfileState {
|
||||||
|
original_id,
|
||||||
|
class_flags,
|
||||||
|
profile_entry,
|
||||||
|
transfer_ore_off_board_bits,
|
||||||
|
transfer_power_out_bits,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(states)
|
||||||
|
}
|
||||||
|
|
||||||
fn initialize_behavior_ore_state(
|
fn initialize_behavior_ore_state(
|
||||||
original_id: OriginalObjectId,
|
original_id: OriginalObjectId,
|
||||||
handle: ObjectHandle,
|
handle: ObjectHandle,
|
||||||
@@ -3388,6 +3654,82 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_profile_selector_1002_sums_control_e00_rows_then_multiplies_profile_rate() {
|
||||||
|
let table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(vec![
|
||||||
|
synthetic_effective_control_device(
|
||||||
|
26,
|
||||||
|
26,
|
||||||
|
3.0_f32.to_bits(),
|
||||||
|
Some(4.0_f32.to_bits()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
synthetic_effective_control_device(
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
100.0_f32.to_bits(),
|
||||||
|
Some(100.0_f32.to_bits()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
synthetic_effective_control_device(
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
100.0_f32.to_bits(),
|
||||||
|
Some(100.0_f32.to_bits()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
synthetic_effective_control_device(
|
||||||
|
21,
|
||||||
|
21,
|
||||||
|
100.0_f32.to_bits(),
|
||||||
|
Some(100.0_f32.to_bits()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
synthetic_effective_control_device(
|
||||||
|
26,
|
||||||
|
26,
|
||||||
|
2.0_f32.to_bits(),
|
||||||
|
Some(2.0_f32.to_bits()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(
|
||||||
|
(0..3)
|
||||||
|
.map(|index| synthetic_node_provider_row(0, index, 2.0_f32.to_bits()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query_behavior_profile_ore_rate(&table, 1.5_f32.to_bits())
|
||||||
|
.expect("supported Behavior ore query")
|
||||||
|
.to_bits(),
|
||||||
|
48.0_f32.to_bits()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut factory_mismatch = table.clone();
|
||||||
|
let MissionControlDeviceTableStatus::Available(devices) = &mut factory_mismatch.status
|
||||||
|
else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
devices[0].factory_tag = 9;
|
||||||
|
assert_eq!(
|
||||||
|
query_behavior_profile_ore_rate(&factory_mismatch, 1.5_f32.to_bits()),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: 9 })
|
||||||
|
);
|
||||||
|
|
||||||
|
let unavailable =
|
||||||
|
synthetic_control_device_table(MissionControlDeviceTableStatus::Unavailable(
|
||||||
|
MissionControlDeviceTableUnavailableReason::NoControlResource,
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
query_behavior_profile_ore_rate(&unavailable, 1.0_f32.to_bits()),
|
||||||
|
Err(ControlResourceAccessError::TableUnavailable(
|
||||||
|
MissionControlDeviceTableUnavailableReason::NoControlResource
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn control_resource_queries_filter_current_tag_and_use_factory_getters() {
|
fn control_resource_queries_filter_current_tag_and_use_factory_getters() {
|
||||||
let table =
|
let table =
|
||||||
@@ -3777,6 +4119,10 @@ mod tests {
|
|||||||
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
|
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
|
||||||
Err(ControlResourceAccessError::MissionUnavailable)
|
Err(ControlResourceAccessError::MissionUnavailable)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&engine, handle, 0x1005),
|
||||||
|
Err(ControlResourceAccessError::MissionUnavailable)
|
||||||
|
);
|
||||||
|
|
||||||
let stale = ObjectHandle {
|
let stale = ObjectHandle {
|
||||||
generation: handle.generation.wrapping_add(1),
|
generation: handle.generation.wrapping_add(1),
|
||||||
@@ -3794,6 +4140,12 @@ mod tests {
|
|||||||
fparkan_world::WorldError::StaleHandle
|
fparkan_world::WorldError::StaleHandle
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&engine, stale, 0x1005),
|
||||||
|
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(
|
||||||
@@ -3814,6 +4166,12 @@ mod tests {
|
|||||||
fparkan_world::WorldError::StaleHandle
|
fparkan_world::WorldError::StaleHandle
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&engine, handle, 0x1005),
|
||||||
|
Err(ControlResourceAccessError::World(
|
||||||
|
fparkan_world::WorldError::StaleHandle
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4650,12 +5008,28 @@ mod tests {
|
|||||||
let land_path = decode_mission_land_path(&source_tma, TmaProfile::Strict)
|
let land_path = decode_mission_land_path(&source_tma, TmaProfile::Strict)
|
||||||
.expect("source mission land path")
|
.expect("source mission land path")
|
||||||
.raw;
|
.raw;
|
||||||
|
// The TMA keeps its strict marker; only the Unit DAT class header is
|
||||||
|
// overridden in the in-memory VFS to exercise both native profiles.
|
||||||
|
let load_full_fixture = |class_flags: u32| {
|
||||||
let mut overlay = MemoryVfs::default();
|
let mut overlay = MemoryVfs::default();
|
||||||
overlay.insert(
|
overlay.insert(
|
||||||
normalize_engine_path("test mission", "MISSIONS/TEST/MINE/data.tma")
|
normalize_engine_path("test mission", "MISSIONS/TEST/MINE/data.tma")
|
||||||
.expect("mission path"),
|
.expect("mission path"),
|
||||||
Arc::from(single_unit_tma(&land_path)),
|
Arc::from(single_unit_tma(&land_path)),
|
||||||
);
|
);
|
||||||
|
let mut unit_dat = std::fs::read(root.join("UNITS/BUILDS/MINE/min_l_n1.dat"))
|
||||||
|
.expect("licensed Unit DAT");
|
||||||
|
let original_class_flags =
|
||||||
|
u32::from_le_bytes(unit_dat[4..8].try_into().expect("Unit DAT header word"));
|
||||||
|
assert_eq!(original_class_flags, 0x8000_0004);
|
||||||
|
if class_flags != original_class_flags {
|
||||||
|
unit_dat[4..8].copy_from_slice(&class_flags.to_le_bytes());
|
||||||
|
}
|
||||||
|
overlay.insert(
|
||||||
|
normalize_engine_path("test Unit DAT", "UNITS/BUILDS/MINE/min_l_n1.dat")
|
||||||
|
.expect("Unit DAT path"),
|
||||||
|
Arc::from(unit_dat),
|
||||||
|
);
|
||||||
let vfs = Arc::new(OverlayVfs::from_layers(vec![
|
let vfs = Arc::new(OverlayVfs::from_layers(vec![
|
||||||
Arc::new(overlay),
|
Arc::new(overlay),
|
||||||
Arc::new(DirectoryVfs::new(&root)),
|
Arc::new(DirectoryVfs::new(&root)),
|
||||||
@@ -4671,6 +5045,10 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("full normal mission load");
|
.expect("full normal mission load");
|
||||||
assert_eq!(loaded.object_count, 1);
|
assert_eq!(loaded.object_count, 1);
|
||||||
|
engine
|
||||||
|
};
|
||||||
|
let mut engine = load_full_fixture(0x8000_0002);
|
||||||
|
let mine_engine = load_full_fixture(0x8000_0004);
|
||||||
|
|
||||||
let draft = &loaded_mission_object_drafts(&engine).expect("object drafts")[0];
|
let draft = &loaded_mission_object_drafts(&engine).expect("object drafts")[0];
|
||||||
assert_eq!(draft.unit_components.len(), 6);
|
assert_eq!(draft.unit_components.len(), 6);
|
||||||
@@ -4745,6 +5123,38 @@ mod tests {
|
|||||||
.map(f32::to_bits),
|
.map(f32::to_bits),
|
||||||
Some(1.0_f32.to_bits())
|
Some(1.0_f32.to_bits())
|
||||||
);
|
);
|
||||||
|
let profiles = loaded_mission_behavior_profiles(&engine).expect("Behavior profiles");
|
||||||
|
assert_eq!(profiles.len(), 1);
|
||||||
|
assert_eq!(profiles[0].original_id, OriginalObjectId(0));
|
||||||
|
assert_eq!(profiles[0].handle, control_handle);
|
||||||
|
assert_eq!(profiles[0].class_flags, 0x8000_0002);
|
||||||
|
assert_eq!(profiles[0].profile_entry, Some("prof_generator.var"));
|
||||||
|
assert_eq!(profiles[0].transfer_ore_off_board_bits, 0);
|
||||||
|
assert_eq!(profiles[0].transfer_power_out_bits, 10.0_f32.to_bits());
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&engine, control_handle, 0x1005)
|
||||||
|
.expect("licensed profile power-out rate")
|
||||||
|
.to_bits(),
|
||||||
|
10.0_f32.to_bits()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&engine, control_handle, 0x1002)
|
||||||
|
.expect("licensed profile ore-off-board rate")
|
||||||
|
.to_bits(),
|
||||||
|
0.0_f32.to_bits()
|
||||||
|
);
|
||||||
|
let mine_profile =
|
||||||
|
loaded_mission_behavior_profiles(&mine_engine).expect("mine Behavior profile")[0];
|
||||||
|
assert_eq!(mine_profile.class_flags, 0x8000_0004);
|
||||||
|
assert_eq!(mine_profile.profile_entry, Some("prof_mine.var"));
|
||||||
|
assert_eq!(mine_profile.transfer_ore_off_board_bits, 1.0_f32.to_bits());
|
||||||
|
assert_eq!(mine_profile.transfer_power_out_bits, 0.0_f32.to_bits());
|
||||||
|
assert_eq!(
|
||||||
|
query_loaded_mission_behavior_resource(&mine_engine, mine_profile.handle, 0x1002)
|
||||||
|
.expect("mine profile ore resource query")
|
||||||
|
.to_bits(),
|
||||||
|
1.0_f32.to_bits()
|
||||||
|
);
|
||||||
let prepared_controls = &engine
|
let prepared_controls = &engine
|
||||||
.loaded
|
.loaded
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -5544,6 +5954,48 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_profile_mapper_matches_native_classes_and_unknowns_keep_constructor_defaults() {
|
||||||
|
let mapped = [
|
||||||
|
(0x8000_0004, "prof_mine.var"),
|
||||||
|
(0x8000_0008, "prof_storage.var"),
|
||||||
|
(0x8000_0002, "prof_generator.var"),
|
||||||
|
(0x8000_0010, "prof_plant.var"),
|
||||||
|
(0x8000_0040, "prof_angar.var"),
|
||||||
|
(0x8000_0200, "prof_mtp.var"),
|
||||||
|
(0x8000_0400, "prof_institute.var"),
|
||||||
|
(0x8001_0000, "prof_bunker.var"),
|
||||||
|
(0x8002_0000, "prof_bunker.var"),
|
||||||
|
(0x8004_0000, "prof_bunker.var"),
|
||||||
|
(0x8010_0000, "prof_tower.var"),
|
||||||
|
(0x8020_0000, "prof_tower.var"),
|
||||||
|
(0x0100_2000, "prof_trn.var"),
|
||||||
|
(0x0100_4000, "prof_bld.var"),
|
||||||
|
(0x0100_8000, "prof_war.var"),
|
||||||
|
(0x0101_0000, "prof_hq.var"),
|
||||||
|
(0x0102_0000, "prof_hero.var"),
|
||||||
|
(0x2000_0000, "prof_animal.var"),
|
||||||
|
];
|
||||||
|
for (class_flags, entry) in mapped {
|
||||||
|
assert_eq!(behavior_profile_entry_for_class(class_flags), Some(entry));
|
||||||
|
}
|
||||||
|
assert_eq!(behavior_profile_entry_for_class(0xdead_beef), None);
|
||||||
|
|
||||||
|
let vfs: Arc<dyn Vfs> = Arc::new(MemoryVfs::default());
|
||||||
|
let drafts = [basement_draft(Some(0xdead_beef), 0)];
|
||||||
|
let states = prepare_behavior_profile_states(&vfs, &drafts)
|
||||||
|
.expect("unknown profile class preserves native constructor defaults");
|
||||||
|
assert_eq!(states.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
states[0].map(|state| (
|
||||||
|
state.profile_entry,
|
||||||
|
state.transfer_ore_off_board_bits,
|
||||||
|
state.transfer_power_out_bits,
|
||||||
|
)),
|
||||||
|
Some((None, 0x40a0_0000, 0x42c8_0000))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn behavior_distributor_normalize_matches_native_24_bit_vectors() {
|
fn behavior_distributor_normalize_matches_native_24_bit_vectors() {
|
||||||
let mut pinned = BehaviorDistributorState {
|
let mut pinned = BehaviorDistributorState {
|
||||||
@@ -5645,6 +6097,9 @@ mod tests {
|
|||||||
assert!(loaded_mission_behavior_ore_states(&engine)
|
assert!(loaded_mission_behavior_ore_states(&engine)
|
||||||
.expect("preview Behavior ore state")
|
.expect("preview Behavior ore state")
|
||||||
.is_empty());
|
.is_empty());
|
||||||
|
assert!(loaded_mission_behavior_profiles(&engine)
|
||||||
|
.expect("preview Behavior profiles")
|
||||||
|
.is_empty());
|
||||||
assert!(bundles
|
assert!(bundles
|
||||||
.iter()
|
.iter()
|
||||||
.all(|bundle| bundle.behavior_warm_start.is_none()));
|
.all(|bundle| bundle.behavior_warm_start.is_none()));
|
||||||
@@ -5677,6 +6132,51 @@ mod tests {
|
|||||||
assert!(loaded.script_bundle_count >= 1);
|
assert!(loaded.script_bundle_count >= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires licensed corpus"]
|
||||||
|
fn malformed_known_behavior_profile_keeps_previous_mission_and_world() {
|
||||||
|
let root = licensed_root("IS");
|
||||||
|
let mut overlay = MemoryVfs::default();
|
||||||
|
overlay.insert(
|
||||||
|
normalize_engine_path("broken Behavior archive", "behpsp.res")
|
||||||
|
.expect("profile archive path"),
|
||||||
|
Arc::from(&b"bad!"[..]),
|
||||||
|
);
|
||||||
|
let vfs: Arc<dyn Vfs> = Arc::new(OverlayVfs::from_layers(vec![
|
||||||
|
Arc::new(overlay),
|
||||||
|
Arc::new(DirectoryVfs::new(&root)),
|
||||||
|
]));
|
||||||
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
|
let request = || MissionRequest {
|
||||||
|
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||||
|
mode: MissionMode::SingleMission,
|
||||||
|
};
|
||||||
|
load_mission_static_preview_roots(
|
||||||
|
&mut engine,
|
||||||
|
request(),
|
||||||
|
NonZeroUsize::new(1).expect("one root"),
|
||||||
|
)
|
||||||
|
.expect("static preview does not require Behavior profile parsing");
|
||||||
|
let previous_mission = loaded_mission(&engine).expect("loaded preview").clone();
|
||||||
|
let before = step_headless(&mut engine, InputSnapshot)
|
||||||
|
.expect("step preview world")
|
||||||
|
.snapshot
|
||||||
|
.objects;
|
||||||
|
|
||||||
|
let error = load_mission(&mut engine, request(), 0)
|
||||||
|
.expect_err("malformed profile archive prevents full mission load");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
EngineError::Nres { ref path, .. } if path == "behpsp.res"
|
||||||
|
));
|
||||||
|
assert_eq!(loaded_mission(&engine), Some(&previous_mission));
|
||||||
|
let after = step_headless(&mut engine, InputSnapshot)
|
||||||
|
.expect("step retained preview world")
|
||||||
|
.snapshot
|
||||||
|
.objects;
|
||||||
|
assert_eq!(before, after);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn loaded_host_callback_uses_requested_mode_and_constructor_status() {
|
fn loaded_host_callback_uses_requested_mode_and_constructor_status() {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private const int IndexedPropertySetRva = 0x0b470;
|
private const int IndexedPropertySetRva = 0x0b470;
|
||||||
private const int IndexedPropertyGetRva = 0x0b5b0;
|
private const int IndexedPropertyGetRva = 0x0b5b0;
|
||||||
private const int OreInitializationRva = 0x08790;
|
private const int OreInitializationRva = 0x08790;
|
||||||
|
private const int BehaviorProfileMapRva = 0x08a80;
|
||||||
|
private const int BehaviorVarBlockConstructorRva = 0x22d90;
|
||||||
|
private const int BehaviorVarFieldRegisterRva = 0x22e20;
|
||||||
private const int GetSystemArealMapIatRva = 0x59008;
|
private const int GetSystemArealMapIatRva = 0x59008;
|
||||||
private const int ControlResourceDeviceGetterRva = 0x21d00;
|
private const int ControlResourceDeviceGetterRva = 0x21d00;
|
||||||
private const int ControlResourceAggregateQueryRva = 0x2b410;
|
private const int ControlResourceAggregateQueryRva = 0x2b410;
|
||||||
@@ -45,6 +48,10 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private const int ControlResourceNoOpStringPairRva = 0x0bcd0;
|
private const int ControlResourceNoOpStringPairRva = 0x0bcd0;
|
||||||
private const int ControlResourceTag19VtableRva = 0x3c608;
|
private const int ControlResourceTag19VtableRva = 0x3c608;
|
||||||
private const int ControlResourceTag19StringPairNoOpRva = 0x0bcd0;
|
private const int ControlResourceTag19StringPairNoOpRva = 0x0bcd0;
|
||||||
|
private const int BehaviorVarTableVtableRva = 0x591b0;
|
||||||
|
private const int BehaviorVarArrayLoadRva = 0x49fa0;
|
||||||
|
private const int BehaviorVarFieldBindRva = 0x49090;
|
||||||
|
private const int BehaviorVarApplyRva = 0x49170;
|
||||||
private const uint ResourceProperty = 0x02000100;
|
private const uint ResourceProperty = 0x02000100;
|
||||||
private static Fixture active;
|
private static Fixture active;
|
||||||
|
|
||||||
@@ -74,6 +81,13 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void IndexedPropertySet(IntPtr self, uint index, uint valueBits);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void IndexedPropertySet(IntPtr self, uint index, uint valueBits);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int IndexedPropertyGet(IntPtr self, uint index, IntPtr resultBits);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int IndexedPropertyGet(IntPtr self, uint index, IntPtr resultBits);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeOreInitialization(IntPtr self);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeOreInitialization(IntPtr self);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeBehaviorVarArrayLoad(IntPtr self, IntPtr bytes, IntPtr cursorOut);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarFieldBind(IntPtr self, IntPtr typeName, IntPtr fieldName, IntPtr destination);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarApply(IntPtr self);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorProfileMap(IntPtr self);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarBlockConstructor(IntPtr self);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarFieldRegister(IntPtr self, IntPtr table);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeBehaviorProfileLoadCallback(IntPtr owner, uint selector, IntPtr archive, IntPtr entry);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceGet(IntPtr self, uint selector);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceGet(IntPtr self, uint selector);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeFastcallComponentBuilder(IntPtr owner, IntPtr node, uint parentId);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeFastcallComponentBuilder(IntPtr owner, IntPtr node, uint parentId);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeArealMapEvent(IntPtr owner, uint message, uint subcode, IntPtr descriptor);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeArealMapEvent(IntPtr owner, uint message, uint subcode, IntPtr descriptor);
|
||||||
@@ -111,6 +125,15 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private static IntPtr x87ControlWordCell;
|
private static IntPtr x87ControlWordCell;
|
||||||
private static X87ControlWordReader x87ControlWordReader;
|
private static X87ControlWordReader x87ControlWordReader;
|
||||||
private static X87ControlWordWriter x87ControlWordWriter;
|
private static X87ControlWordWriter x87ControlWordWriter;
|
||||||
|
private static readonly NativeBehaviorProfileLoadCallback BehaviorProfileLoadCallback = CaptureBehaviorProfileLoad;
|
||||||
|
private static readonly List<BehaviorProfileSelection> behaviorProfileSelections = new List<BehaviorProfileSelection>();
|
||||||
|
|
||||||
|
private sealed class BehaviorProfileSelection {
|
||||||
|
public IntPtr Owner;
|
||||||
|
public uint Selector;
|
||||||
|
public string Archive;
|
||||||
|
public string Entry;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class NativeObject {
|
private sealed class NativeObject {
|
||||||
public IntPtr Self;
|
public IntPtr Self;
|
||||||
@@ -533,6 +556,9 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
RunNativeControlResourceDeviceCase(controlModule);
|
RunNativeControlResourceDeviceCase(controlModule);
|
||||||
RunNativeControlProviderLiveStateCase(controlModule);
|
RunNativeControlProviderLiveStateCase(controlModule);
|
||||||
RunNativeControlAggregateResourceCases(controlModule);
|
RunNativeControlAggregateResourceCases(controlModule);
|
||||||
|
RunNativeBehaviorProfileMapCases(module);
|
||||||
|
RunNativeBehaviorVarSyntheticCase(module);
|
||||||
|
RunNativeBehaviorRealProfileCases(module);
|
||||||
} finally {
|
} finally {
|
||||||
SetX87ControlWord(savedControlWord);
|
SetX87ControlWord(savedControlWord);
|
||||||
Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4"));
|
Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4"));
|
||||||
@@ -1092,6 +1118,243 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Console.WriteLine("native-behavior-ore-initialization: classes=80000004,80000008,01002000,01004000,01008000; defaults=verified");
|
Console.WriteLine("native-behavior-ore-initialization: classes=80000004,80000008,01002000,01004000,01008000; defaults=verified");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CaptureBehaviorProfileLoad(IntPtr owner, uint selector, IntPtr archive, IntPtr entry) {
|
||||||
|
behaviorProfileSelections.Add(new BehaviorProfileSelection {
|
||||||
|
Owner = owner,
|
||||||
|
Selector = selector,
|
||||||
|
Archive = Marshal.PtrToStringAnsi(archive),
|
||||||
|
Entry = Marshal.PtrToStringAnsi(entry)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeBehaviorProfileMapCases(IntPtr module) {
|
||||||
|
NativeBehaviorProfileMap map = (NativeBehaviorProfileMap)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorProfileMapRva), typeof(NativeBehaviorProfileMap));
|
||||||
|
string[][] cases = new string[][] {
|
||||||
|
new string[] { "80000004", "prof_mine.var" },
|
||||||
|
new string[] { "80000008", "prof_storage.var" },
|
||||||
|
new string[] { "80000002", "prof_generator.var" },
|
||||||
|
new string[] { "80000010", "prof_plant.var" },
|
||||||
|
new string[] { "80000040", "prof_angar.var" },
|
||||||
|
new string[] { "80000200", "prof_mtp.var" },
|
||||||
|
new string[] { "80000400", "prof_institute.var" },
|
||||||
|
new string[] { "80010000", "prof_bunker.var" },
|
||||||
|
new string[] { "80020000", "prof_bunker.var" },
|
||||||
|
new string[] { "80040000", "prof_bunker.var" },
|
||||||
|
new string[] { "80100000", "prof_tower.var" },
|
||||||
|
new string[] { "80200000", "prof_tower.var" },
|
||||||
|
new string[] { "01002000", "prof_trn.var" },
|
||||||
|
new string[] { "01004000", "prof_bld.var" },
|
||||||
|
new string[] { "01008000", "prof_war.var" },
|
||||||
|
new string[] { "01010000", "prof_hq.var" },
|
||||||
|
new string[] { "01020000", "prof_hero.var" },
|
||||||
|
new string[] { "20000000", "prof_animal.var" }
|
||||||
|
};
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr owner = fixture.Allocate(0x0b00);
|
||||||
|
IntPtr vtable = fixture.Allocate(0x68);
|
||||||
|
Marshal.WriteIntPtr(owner, vtable);
|
||||||
|
Marshal.WriteIntPtr(vtable, 0x64, Marshal.GetFunctionPointerForDelegate(BehaviorProfileLoadCallback));
|
||||||
|
foreach (string[] test in cases) {
|
||||||
|
behaviorProfileSelections.Clear();
|
||||||
|
Marshal.WriteInt32(owner, 0x0afc, unchecked((int)Convert.ToUInt32(test[0], 16)));
|
||||||
|
map(owner);
|
||||||
|
Expect(behaviorProfileSelections.Count == 1, "profile mapper callback count for class 0x" + test[0]);
|
||||||
|
BehaviorProfileSelection selection = behaviorProfileSelections[0];
|
||||||
|
Expect(selection.Owner == owner && selection.Selector == 2 && selection.Archive == "behpsp.res" &&
|
||||||
|
selection.Entry == test[1], "profile mapper result for class 0x" + test[0]);
|
||||||
|
}
|
||||||
|
string[] noMatch = new string[] { "deadbeef", "8000000c" };
|
||||||
|
foreach (string test in noMatch) {
|
||||||
|
behaviorProfileSelections.Clear();
|
||||||
|
Marshal.WriteInt32(owner, 0x0afc, unchecked((int)Convert.ToUInt32(test, 16)));
|
||||||
|
map(owner);
|
||||||
|
Expect(behaviorProfileSelections.Count == 0, "profile mapper leaves unknown/combined class 0x" + test + " unconfigured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine("native-behavior-profile-map: mapped-classes=" + cases.Length + " (including three bunker and two tower codes); unknown-and-combined=no callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeBehaviorVarSyntheticCase(IntPtr module) {
|
||||||
|
NativeBehaviorVarArrayLoad load = (NativeBehaviorVarArrayLoad)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarArrayLoadRva), typeof(NativeBehaviorVarArrayLoad));
|
||||||
|
NativeBehaviorVarFieldBind bind = (NativeBehaviorVarFieldBind)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarFieldBindRva), typeof(NativeBehaviorVarFieldBind));
|
||||||
|
NativeBehaviorVarApply apply = (NativeBehaviorVarApply)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarApplyRva), typeof(NativeBehaviorVarApply));
|
||||||
|
byte[] bytes = SerializeOneBehaviorVarField("Transfer_Ore_OffBoard", "float", 0x3f000000, 0x41200000);
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr table = fixture.Allocate(0x18);
|
||||||
|
IntPtr target = fixture.Allocate(sizeof(uint));
|
||||||
|
IntPtr input = fixture.Allocate(bytes.Length);
|
||||||
|
IntPtr cursorOut = fixture.Allocate(IntPtr.Size);
|
||||||
|
IntPtr fieldName = fixture.Allocate(32);
|
||||||
|
IntPtr typeName = fixture.Allocate(8);
|
||||||
|
CopyBytes(input, bytes);
|
||||||
|
WriteFixedAnsi(fieldName, 0, "Transfer_Ore_OffBoard", 32);
|
||||||
|
WriteFixedAnsi(typeName, 0, "float", 8);
|
||||||
|
Marshal.WriteIntPtr(table, IntPtr.Add(module, BehaviorVarTableVtableRva));
|
||||||
|
Marshal.WriteInt32(input, 0, 1);
|
||||||
|
|
||||||
|
Expect(load(table, input, cursorOut) == 1, "native .var parser accepts the synthetic one-row profile");
|
||||||
|
Expect(Marshal.ReadInt32(table, 4) == 1 && Marshal.ReadIntPtr(cursorOut) == IntPtr.Add(input, bytes.Length),
|
||||||
|
"native .var parser consumes the full synthetic profile");
|
||||||
|
bind(table, typeName, fieldName, target);
|
||||||
|
apply(table);
|
||||||
|
Expect(unchecked((uint)Marshal.ReadInt32(target)) == 0x41200000,
|
||||||
|
"native .var apply uses the second value blob after binding");
|
||||||
|
Console.WriteLine("native-behavior-var-synthetic: first=0x3f000000 second=0x41200000 applied=0x" +
|
||||||
|
unchecked((uint)Marshal.ReadInt32(target)).ToString("x8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeBehaviorRealProfileCases(IntPtr module) {
|
||||||
|
string archivePath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_BEHAVIOR_PROFILE_RES");
|
||||||
|
if (String.IsNullOrEmpty(archivePath)) archivePath = @"C:\GOG Games\Parkan - Iron Strategy\behpsp.res";
|
||||||
|
if (!File.Exists(archivePath)) throw new FileNotFoundException(
|
||||||
|
"Set FPARKAN_ORIGINAL_BEHAVIOR_PROFILE_RES to the installed behpsp.res.", archivePath);
|
||||||
|
byte[] archiveBytes = File.ReadAllBytes(archivePath);
|
||||||
|
NativeBehaviorProfileMap map = (NativeBehaviorProfileMap)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorProfileMapRva), typeof(NativeBehaviorProfileMap));
|
||||||
|
NativeBehaviorVarArrayLoad load = (NativeBehaviorVarArrayLoad)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarArrayLoadRva), typeof(NativeBehaviorVarArrayLoad));
|
||||||
|
NativeBehaviorVarBlockConstructor construct = (NativeBehaviorVarBlockConstructor)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarBlockConstructorRva), typeof(NativeBehaviorVarBlockConstructor));
|
||||||
|
NativeBehaviorVarFieldRegister register = (NativeBehaviorVarFieldRegister)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarFieldRegisterRva), typeof(NativeBehaviorVarFieldRegister));
|
||||||
|
NativeBehaviorVarApply apply = (NativeBehaviorVarApply)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, BehaviorVarApplyRva), typeof(NativeBehaviorVarApply));
|
||||||
|
|
||||||
|
string selectedEntry = SelectBehaviorProfile(map, 0x80000004);
|
||||||
|
Expect(selectedEntry == "prof_mine.var", "mine profile is selected by native class mapper");
|
||||||
|
byte[] profile = ReadNresEntry(archiveBytes, selectedEntry);
|
||||||
|
Expect(ReadUInt32(profile, 0) == 27, "native mine profile contains all 27 rows");
|
||||||
|
uint expectedOreOffBoard = FindBehaviorVarSecondBlob(profile, "Transfer_Ore_OffBoard");
|
||||||
|
uint expectedPowerOut = FindBehaviorVarSecondBlob(profile, "Transfer_Power_Out");
|
||||||
|
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr table = fixture.Allocate(0x18);
|
||||||
|
IntPtr rawProfile = fixture.Allocate(profile.Length);
|
||||||
|
IntPtr cursorOut = fixture.Allocate(IntPtr.Size);
|
||||||
|
IntPtr block = fixture.Allocate(0x80);
|
||||||
|
CopyBytes(rawProfile, profile);
|
||||||
|
Marshal.WriteIntPtr(table, IntPtr.Add(module, BehaviorVarTableVtableRva));
|
||||||
|
|
||||||
|
Expect(load(table, rawProfile, cursorOut) == 1, "native parser accepts the licensed mine profile");
|
||||||
|
Expect(Marshal.ReadInt32(table, 4) == 27 && Marshal.ReadIntPtr(cursorOut) == IntPtr.Add(rawProfile, profile.Length),
|
||||||
|
"native parser consumes all rows in the licensed mine profile");
|
||||||
|
construct(block);
|
||||||
|
Expect(unchecked((uint)Marshal.ReadInt32(block, 0x58)) == 0x40a00000 &&
|
||||||
|
unchecked((uint)Marshal.ReadInt32(block, 0x60)) == 0x42c80000,
|
||||||
|
"native variable block starts with transfer defaults 5 and 100");
|
||||||
|
register(block, table);
|
||||||
|
apply(table);
|
||||||
|
Expect(unchecked((uint)Marshal.ReadInt32(block, 0x58)) == expectedOreOffBoard &&
|
||||||
|
unchecked((uint)Marshal.ReadInt32(block, 0x60)) == expectedPowerOut,
|
||||||
|
"native profile bind/apply copies second blobs into Transfer_Ore_OffBoard and Transfer_Power_Out");
|
||||||
|
Console.WriteLine("native-behavior-real-profile: class=80000004 entry=" + selectedEntry + " rows=27 Transfer_Ore_OffBoard=" +
|
||||||
|
Hex(expectedOreOffBoard) + " Transfer_Power_Out=" + Hex(expectedPowerOut) + "; native-bind-and-apply=verified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SelectBehaviorProfile(NativeBehaviorProfileMap map, uint classCode) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr owner = fixture.Allocate(0x0b00);
|
||||||
|
IntPtr vtable = fixture.Allocate(0x68);
|
||||||
|
Marshal.WriteIntPtr(owner, vtable);
|
||||||
|
Marshal.WriteIntPtr(vtable, 0x64, Marshal.GetFunctionPointerForDelegate(BehaviorProfileLoadCallback));
|
||||||
|
behaviorProfileSelections.Clear();
|
||||||
|
Marshal.WriteInt32(owner, 0x0afc, unchecked((int)classCode));
|
||||||
|
map(owner);
|
||||||
|
Expect(behaviorProfileSelections.Count == 1, "native mapper returned a single profile for class 0x" + classCode.ToString("x8"));
|
||||||
|
BehaviorProfileSelection selection = behaviorProfileSelections[0];
|
||||||
|
Expect(selection.Owner == owner && selection.Selector == 2 && selection.Archive == "behpsp.res",
|
||||||
|
"native mapper selected the behavior profile archive and selector");
|
||||||
|
return selection.Entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] SerializeOneBehaviorVarField(string fieldName, string typeName, uint firstBits, uint secondBits) {
|
||||||
|
byte[] field = Encoding.ASCII.GetBytes(fieldName);
|
||||||
|
byte[] type = Encoding.ASCII.GetBytes(typeName);
|
||||||
|
using (MemoryStream stream = new MemoryStream()) using (BinaryWriter writer = new BinaryWriter(stream)) {
|
||||||
|
writer.Write((uint)1); // row count
|
||||||
|
writer.Write((uint)4); // value size
|
||||||
|
writer.Write((uint)3); // float type index
|
||||||
|
writer.Write((uint)0); // flags
|
||||||
|
writer.Write((uint)field.Length); writer.Write(field);
|
||||||
|
writer.Write((uint)type.Length); writer.Write(type);
|
||||||
|
writer.Write(firstBits); writer.Write(secondBits);
|
||||||
|
writer.Write((uint)0); writer.Write((uint)0); // trailing row metadata
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ReadNresEntry(byte[] archive, string name) {
|
||||||
|
if (archive.Length < 16) throw new InvalidDataException("NRes archive is shorter than its header");
|
||||||
|
uint count = ReadUInt32(archive, 8);
|
||||||
|
if (count > Int32.MaxValue / 64) throw new InvalidDataException("NRes directory count is too large");
|
||||||
|
int directory = checked(archive.Length - (int)count * 64);
|
||||||
|
if (directory < 16) throw new InvalidDataException("NRes directory falls inside the header");
|
||||||
|
for (int index = 0; index < (int)count; index++) {
|
||||||
|
int record = checked(directory + index * 64);
|
||||||
|
int nameEnd = record + 20;
|
||||||
|
while (nameEnd < record + 56 && archive[nameEnd] != 0) nameEnd++;
|
||||||
|
string entryName = Encoding.ASCII.GetString(archive, record + 20, nameEnd - (record + 20));
|
||||||
|
if (!String.Equals(entryName, name, StringComparison.Ordinal)) continue;
|
||||||
|
uint size = ReadUInt32(archive, record + 12);
|
||||||
|
uint dataOffset = ReadUInt32(archive, record + 56);
|
||||||
|
if (size > Int32.MaxValue || dataOffset > Int32.MaxValue || dataOffset + size > archive.Length)
|
||||||
|
throw new InvalidDataException("NRes entry range is outside the archive");
|
||||||
|
byte[] data = new byte[(int)size];
|
||||||
|
Buffer.BlockCopy(archive, (int)dataOffset, data, 0, data.Length);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
throw new InvalidDataException("NRes archive does not contain entry " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint FindBehaviorVarSecondBlob(byte[] profile, string fieldName) {
|
||||||
|
if (profile.Length < 4) throw new InvalidDataException("Behavior profile is shorter than its row count");
|
||||||
|
uint count = ReadUInt32(profile, 0);
|
||||||
|
if (count > 1024) throw new InvalidDataException("Behavior profile row count is unreasonable");
|
||||||
|
int cursor = 4;
|
||||||
|
for (int index = 0; index < (int)count; index++) {
|
||||||
|
EnsureRange(profile, cursor, 12);
|
||||||
|
uint size = ReadUInt32(profile, cursor);
|
||||||
|
if (size > Int32.MaxValue) throw new InvalidDataException("Behavior variable size is too large");
|
||||||
|
uint type = ReadUInt32(profile, cursor + 4);
|
||||||
|
cursor += 12;
|
||||||
|
uint fieldLength = ReadUInt32(profile, cursor);
|
||||||
|
if (fieldLength > Int32.MaxValue) throw new InvalidDataException("Behavior field name is too large");
|
||||||
|
cursor += 4; EnsureRange(profile, cursor, (int)fieldLength);
|
||||||
|
string name = Encoding.ASCII.GetString(profile, cursor, (int)fieldLength);
|
||||||
|
cursor += (int)fieldLength;
|
||||||
|
uint typeLength = ReadUInt32(profile, cursor);
|
||||||
|
if (typeLength > Int32.MaxValue) throw new InvalidDataException("Behavior variable type name is too large");
|
||||||
|
cursor += 4; EnsureRange(profile, cursor, (int)typeLength);
|
||||||
|
cursor += (int)typeLength;
|
||||||
|
EnsureRange(profile, cursor, checked((int)size * 2 + 8));
|
||||||
|
int secondBlob = checked(cursor + (int)size);
|
||||||
|
if (String.Equals(name, fieldName, StringComparison.Ordinal)) {
|
||||||
|
if (type != 3 || size != 4) throw new InvalidDataException("Expected float profile field " + fieldName);
|
||||||
|
return ReadUInt32(profile, secondBlob);
|
||||||
|
}
|
||||||
|
cursor = checked(cursor + (int)size * 2 + 8);
|
||||||
|
}
|
||||||
|
throw new InvalidDataException("Behavior profile does not contain field " + fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ReadUInt32(byte[] data, int offset) {
|
||||||
|
EnsureRange(data, offset, 4);
|
||||||
|
return unchecked((uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureRange(byte[] data, int offset, int size) {
|
||||||
|
if (offset < 0 || size < 0 || offset > data.Length - size) throw new InvalidDataException("Behavior profile range is outside its input");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyBytes(IntPtr destination, byte[] source) { Marshal.Copy(source, 0, destination, source.Length); }
|
||||||
|
|
||||||
private static ushort CurrentX87ControlWord() {
|
private static ushort CurrentX87ControlWord() {
|
||||||
if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero)
|
if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero)
|
||||||
throw new InvalidOperationException("x87 control-word reader is not initialized");
|
throw new InvalidOperationException("x87 control-word reader is not initialized");
|
||||||
|
|||||||
@@ -330,9 +330,19 @@ factory paths остаются явными границами. Snapshot пок
|
|||||||
`FUN_10010f30` (RVA `0x10f30`) изменяет и ограничивает provider amount, а
|
`FUN_10010f30` (RVA `0x10f30`) изменяет и ограничивает provider amount, а
|
||||||
связанные callbacks переносят его между строками; их общий переход в Rust ещё
|
связанные callbacks переносят его между строками; их общий переход в Rust ещё
|
||||||
не реализован. Поэтому вызов нельзя считать живым ресурсным симулятором.
|
не реализован. Поэтому вызов нельзя считать живым ресурсным симулятором.
|
||||||
Behavior selector `0x1002` остаётся отдельной задачей: его множитель
|
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
|
||||||
`Transfer_Ore_OffBoard` приходит из блока `+0x820`, но источник фактических
|
профиля из `behpsp.res`, разобранного до создания нового мира. `0x1002` складывает
|
||||||
текущих настроек для полного runtime-пути пока не подтверждён.
|
только текущие descriptor tag `26` запросы `0xe00`, затем применяет
|
||||||
|
`Transfer_Ore_OffBoard`; `0x1005` возвращает `Transfer_Power_Out`. Это начальный
|
||||||
|
снимок: он не отражает последующие изменения provider-строк, Control tick-а или
|
||||||
|
профиля. Полная загрузка сохраняет результат у `ObjectHandle` и возвращает
|
||||||
|
ошибку, если известный код класса не имеет корректной записи; класс,
|
||||||
|
отсутствующий в таблице выбора, сохраняет начальные значения конструктора.
|
||||||
|
Preview и объекты без полного Unit DAT не получают профильное состояние.
|
||||||
|
Native oracle подтвердил соответствия кода класса выбранной записи, разбор
|
||||||
|
`.var` и применение второй области данных;
|
||||||
|
оригинальный aggregate getter Behavior RVA `0xa490` остаётся статически
|
||||||
|
исследованным, но не исполнен в oracle.
|
||||||
|
|
||||||
В x86 oracle при x87 control word `0x007f` проверены порядок, signed zero,
|
В x86 oracle при x87 control word `0x007f` проверены порядок, signed zero,
|
||||||
отрицательные входы, расширенный промежуточный диапазон и subnormal return.
|
отрицательные входы, расширенный промежуточный диапазон и subnormal return.
|
||||||
|
|||||||
+66
-16
@@ -755,27 +755,77 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та
|
|||||||
`FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector
|
`FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector
|
||||||
`0xe00`. В парной `Control.dll` выше `FUN_1002bb40` (RVA `0x2bb40`) для типа
|
`0xe00`. В парной `Control.dll` выше `FUN_1002bb40` (RVA `0x2bb40`) для типа
|
||||||
`0x1a` вызывает selector `0x300`, описанный в этом разделе. Затем Behavior query
|
`0x1a` вызывает selector `0x300`, описанный в этом разделе. Затем Behavior query
|
||||||
`0x1002` (RVA `0xa490`) умножает полученную сумму на текущее значение block
|
`0x1002` (RVA `0xa490`) умножает полученную сумму на поле `+0x58`
|
||||||
`+0x820`, поле `+0x58` `Transfer_Ore_OffBoard` (имя — RVA `0x22e20`, default
|
`Transfer_Ore_OffBoard` блока `+0x820`; отдельный selector `0x1005` возвращает
|
||||||
5.0 — RVA `0x22d90`; config selector 2 загружает блок через RVA `0x0a1a0`).
|
`+0x60` `Transfer_Power_Out`. Имена регистрируются в RVA `0x22e20`, а конструктор
|
||||||
Отдельный query `0x1005` (RVA `0xa490`) возвращает поле `+0x60`
|
RVA `0x22d90` задаёт начальные значения 5.0 и 100.0. Восемнадцать точных class
|
||||||
`Transfer_Power_Out` (имя — RVA `0x22e20`, default 100.0 — RVA `0x22d90`;
|
codes выбирают записи `.var` из `behpsp.res`: например, `0x80000004` выбирает
|
||||||
setter — RVA `0xaa40`). В полной загрузке FParkan начальные provider-строки
|
`prof_mine.var`, `0x80000002` — `prof_generator.var`, а три кода bunker-класса
|
||||||
хранятся вместе с таблицей Control конкретного `ObjectHandle`. Запрос
|
выбирают `prof_bunker.var`. Таблицу этих соответствий задаёт mapper
|
||||||
`0xe00` проходит по ключу устройства в этот снимок и пока поддержан только для
|
`FUN_10008a80` (RVA `0x8a80`) в Behavior.dll SHA-256
|
||||||
текущего descriptor tag `26` с factory tag `26`; для него результат равен
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`.
|
||||||
|
|
||||||
|
Установленные соответствия mapper-а:
|
||||||
|
|
||||||
|
| Class flags | Запись `behpsp.res` |
|
||||||
|
| --- | --- |
|
||||||
|
| `0x80000004` | `prof_mine.var` |
|
||||||
|
| `0x80000008` | `prof_storage.var` |
|
||||||
|
| `0x80000002` | `prof_generator.var` |
|
||||||
|
| `0x80000010` | `prof_plant.var` |
|
||||||
|
| `0x80000040` | `prof_angar.var` |
|
||||||
|
| `0x80000200` | `prof_mtp.var` |
|
||||||
|
| `0x80000400` | `prof_institute.var` |
|
||||||
|
| `0x80010000`, `0x80020000`, `0x80040000` | `prof_bunker.var` |
|
||||||
|
| `0x80100000`, `0x80200000` | `prof_tower.var` |
|
||||||
|
| `0x01002000` | `prof_trn.var` |
|
||||||
|
| `0x01004000` | `prof_bld.var` |
|
||||||
|
| `0x01008000` | `prof_war.var` |
|
||||||
|
| `0x01010000` | `prof_hq.var` |
|
||||||
|
| `0x01020000` | `prof_hero.var` |
|
||||||
|
| `0x20000000` | `prof_animal.var` |
|
||||||
|
|
||||||
|
У бинарной строки `.var` есть имя, индекс типа, размер и две области данных.
|
||||||
|
Native parser RVA `0x49fa0` читает запись, binder RVA `0x49090` связывает её с
|
||||||
|
зарегистрированным полем, а apply-loop RVA `0x49170` копирует в поле именно
|
||||||
|
вторую область. В профиле используются точные C-string имена
|
||||||
|
`Transfer_Ore_OffBoard` и `Transfer_Power_Out`; регистр учитывается, первый
|
||||||
|
совпавший ряд побеждает. Rust ограничивает чтение исходной записи NRes её
|
||||||
|
границами и сохраняет значения как исходные 32-битные слова у соответствующего
|
||||||
|
`ObjectHandle`. Разбор профиля для полной миссии завершается до создания нового
|
||||||
|
мира: отсутствие или повреждение профиля известного класса прерывает загрузку,
|
||||||
|
не подменяя предыдущую миссию. Если mapper не знает class code, остаются
|
||||||
|
конструкторские значения 5.0 и 100.0; это не догадка о неизвестном профиле.
|
||||||
|
Preview не загружает профили.
|
||||||
|
|
||||||
|
При вычислении `0x1002` native `FUN_100186b0` (RVA `0x186b0`) добавляет в список
|
||||||
|
только текущие Control descriptor-и tag `26`, после чего
|
||||||
|
`FUN_100198e0` (RVA `0x198e0`) суммирует их успешные `0xe00` запросы. FParkan
|
||||||
|
делает то же для загруженной таблицы в её исходном порядке и затем умножает
|
||||||
|
сумму на профильный `Transfer_Ore_OffBoard`. Записи tag `5`, `10` и `21` в общей
|
||||||
|
таблице в этот список не попадают. Запрос `0xe00` проходит по ключу устройства
|
||||||
|
в исходную provider-таблицу и пока поддержан только для текущего descriptor
|
||||||
|
tag `26` с factory tag `26`; для него результат равен
|
||||||
`provider * fixed_words[11] * multiplier`. Лицензированный тест загружает
|
`provider * fixed_words[11] * multiplier`. Лицензированный тест загружает
|
||||||
оригинальные CTL/NDP и подтверждает, что tag-26 запись с ключом `2` читает
|
оригинальные CTL/NDP и полную миссию с этим Unit DAT. В установленном
|
||||||
начальное значение строки `1.0`. Это проверка начального состояния после
|
`min_l_n1.dat` class word равен `0x80000004`, поэтому исходный прогон выбирает
|
||||||
конструкторов, не результат живого игрового тика.
|
`prof_mine.var`. Для проверки другого отображения второй прогон оставляет тот же
|
||||||
|
TMA и компоненты, но во временном VFS заменяет слово класса заголовка DAT на
|
||||||
|
`0x80000002`; это тестовая подмена, а не содержимое лицензированного файла.
|
||||||
|
Профиль mine даёт `Transfer_Power_Out` 0.0 и `Transfer_Ore_OffBoard` 1.0;
|
||||||
|
оригинальный профиль генератора — 10.0 и 0.0 соответственно. Для исходного
|
||||||
|
tag-26 ряда получается `1.0` provider × `1.0` coefficient × `1.0` multiplier;
|
||||||
|
профиль mine умножает эту сумму на `1.0`, поэтому selector `0x1002` возвращает
|
||||||
|
`1.0`. Эти запросы используют начальные provider-значения после конструкторов,
|
||||||
|
а не результаты живого игрового тика.
|
||||||
|
|
||||||
Поведение provider-строк меняется и после загрузки: например,
|
Поведение provider-строк меняется и после загрузки: например,
|
||||||
`Control.dll!FUN_10010f30` (RVA `0x10f30`) обновляет и ограничивает текущее
|
`Control.dll!FUN_10010f30` (RVA `0x10f30`) обновляет и ограничивает текущее
|
||||||
значение, а соседние процедуры распространяют его между связанными строками.
|
значение, а соседние процедуры распространяют его между связанными строками.
|
||||||
Эти изменения пока не отражены в Rust-снимке. Поэтому `0xe00` не является
|
Эти изменения пока не отражены в Rust-снимке. Поэтому запрос Control `0xe00` и
|
||||||
исполнением всего Control tick-а, а Behavior query `0x1002` не подключён к
|
Behavior selector `0x1002`, который его суммирует, используют исходное состояние
|
||||||
этому API; его множитель `Transfer_Ore_OffBoard` требует отдельного источника
|
полной загрузки. Selector `0x1005` прямо возвращает загруженную ставку профиля.
|
||||||
текущей конфигурации.
|
Ни один из них пока не исполняет Control tick или последующие изменения профиля.
|
||||||
|
|
||||||
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
|
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
|
||||||
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
|
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
|
||||||
|
|||||||
Reference in new issue
Block a user