feat(runtime): retain Control owner profiles during loading
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
This commit is contained in:
6 files changed
+271
No files matched your search
@@ -7410,6 +7410,7 @@ mod tests {
|
|||||||
type_id: None,
|
type_id: None,
|
||||||
},
|
},
|
||||||
model_node_count: 1,
|
model_node_count: 1,
|
||||||
|
native_profile_words: [0; 27],
|
||||||
bindings: vec![],
|
bindings: vec![],
|
||||||
timeline_rows: vec![fparkan_assets::PreparedControlTimelineRow {
|
timeline_rows: vec![fparkan_assets::PreparedControlTimelineRow {
|
||||||
row_index: 0,
|
row_index: 0,
|
||||||
|
|||||||
@@ -1081,6 +1081,8 @@ pub struct PreparedVisual {
|
|||||||
pub struct PreparedComponentControl {
|
pub struct PreparedComponentControl {
|
||||||
/// CTLD resource that supplied these bindings.
|
/// CTLD resource that supplied these bindings.
|
||||||
pub source: ResourceKey,
|
pub source: ResourceKey,
|
||||||
|
/// The 27 raw native owner-profile words at CTLD bytes 20..128.
|
||||||
|
pub native_profile_words: [u32; 27],
|
||||||
/// Number of nodes in the component model this control targets.
|
/// Number of nodes in the component model this control targets.
|
||||||
pub model_node_count: usize,
|
pub model_node_count: usize,
|
||||||
/// Enabled source rows in their original order, including duplicate nodes.
|
/// Enabled source rows in their original order, including duplicate nodes.
|
||||||
@@ -4191,6 +4193,19 @@ fn decode_component_control_payload(
|
|||||||
return Err(invalid("complete binding table extends past the payload"));
|
return Err(invalid("complete binding table extends past the payload"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let profile_bytes = payload
|
||||||
|
.get(HEADER_BYTES..HEADER_BYTES + FIXED_SECTION_BYTES)
|
||||||
|
.ok_or_else(|| invalid("fixed owner profile extends past the payload"))?;
|
||||||
|
let mut native_profile_words = [0_u32; FIXED_SECTION_BYTES / 4];
|
||||||
|
for (index, word) in native_profile_words.iter_mut().enumerate() {
|
||||||
|
let offset = index * 4;
|
||||||
|
*word = u32::from_le_bytes(
|
||||||
|
profile_bytes[offset..offset + 4]
|
||||||
|
.try_into()
|
||||||
|
.expect("profile word is exactly four bytes"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut bindings = Vec::with_capacity(row_count);
|
let mut bindings = Vec::with_capacity(row_count);
|
||||||
let mut timeline_rows = Vec::with_capacity(row_count);
|
let mut timeline_rows = Vec::with_capacity(row_count);
|
||||||
for row_index in 0..row_count {
|
for row_index in 0..row_count {
|
||||||
@@ -4253,6 +4268,7 @@ fn decode_component_control_payload(
|
|||||||
|
|
||||||
Ok(PreparedComponentControl {
|
Ok(PreparedComponentControl {
|
||||||
source,
|
source,
|
||||||
|
native_profile_words,
|
||||||
model_node_count,
|
model_node_count,
|
||||||
bindings,
|
bindings,
|
||||||
timeline_rows,
|
timeline_rows,
|
||||||
@@ -4840,6 +4856,66 @@ mod tests {
|
|||||||
assert_eq!(control.bindings[1].flags, 1);
|
assert_eq!(control.bindings[1].flags, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_preserves_raw_profile_between_header_and_sections() {
|
||||||
|
const PROFILE_START: usize = 20;
|
||||||
|
const PROFILE_WORDS: usize = 27;
|
||||||
|
const SECTION_BYTES: usize = 156 + 16;
|
||||||
|
const MATRIX_BYTES: usize = 4;
|
||||||
|
const ROW_START: usize = PROFILE_START + PROFILE_WORDS * 4 + SECTION_BYTES + MATRIX_BYTES;
|
||||||
|
const ROW_BYTES: usize = 36;
|
||||||
|
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"profile.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let mut payload = vec![0; ROW_START + ROW_BYTES];
|
||||||
|
payload[0..4].copy_from_slice(&1_u32.to_le_bytes()); // one section
|
||||||
|
payload[4..8].copy_from_slice(&1_u32.to_le_bytes()); // one node per section
|
||||||
|
payload[8..12].copy_from_slice(&1_u32.to_le_bytes()); // one binding row
|
||||||
|
let profile = std::array::from_fn::<_, PROFILE_WORDS, _>(|index| match index {
|
||||||
|
0 => 0xDEAD_BEEF,
|
||||||
|
1 => 0x7FC0_1234,
|
||||||
|
index => 0xA500_0000 | u32::try_from(index).expect("index fits u32"),
|
||||||
|
});
|
||||||
|
for (index, word) in profile.iter().enumerate() {
|
||||||
|
let start = PROFILE_START + index * 4;
|
||||||
|
payload[start..start + 4].copy_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
payload[ROW_START..ROW_START + 4].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
payload[ROW_START + 4..ROW_START + 8].copy_from_slice(&1.25_f32.to_bits().to_le_bytes());
|
||||||
|
payload[ROW_START + 8..ROW_START + 12].copy_from_slice(&2.5_f32.to_bits().to_le_bytes());
|
||||||
|
payload[ROW_START + 12..ROW_START + 16].copy_from_slice(&0.75_f32.to_bits().to_le_bytes());
|
||||||
|
payload[ROW_START + 0x20..ROW_START + 0x24].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
|
||||||
|
let control =
|
||||||
|
decode_component_control_payload(&payload, source, 2, 0).expect("profile CTLD");
|
||||||
|
|
||||||
|
assert_eq!(control.native_profile_words, profile);
|
||||||
|
assert_eq!(control.timeline_rows.len(), 1);
|
||||||
|
assert_eq!(control.timeline_rows[0].node_index, 1);
|
||||||
|
assert_eq!(
|
||||||
|
control.timeline_rows[0].frame_a.to_bits(),
|
||||||
|
1.25_f32.to_bits()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
control.timeline_rows[0].frame_b.to_bits(),
|
||||||
|
2.5_f32.to_bits()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_rejects_truncated_native_profile() {
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"truncated-profile.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(decode_component_control_payload(&vec![0; 127], source, 0, 0).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_control_rejects_bad_bounds_but_keeps_sampler_inputs_raw() {
|
fn component_control_rejects_bad_bounds_but_keeps_sampler_inputs_raw() {
|
||||||
let source = ResourceKey {
|
let source = ResourceKey {
|
||||||
|
|||||||
@@ -454,6 +454,8 @@ 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,
|
||||||
|
/// Raw CTLD profile retained by native component ID zero and its per-owner copy.
|
||||||
|
pub owner_profile: Option<MissionControlOwnerProfile>,
|
||||||
/// Global NDP provider rows used by indexed Control selectors.
|
/// Global NDP provider rows used by indexed Control selectors.
|
||||||
pub node_provider_status: MissionControlNodeProviderTableStatus,
|
pub node_provider_status: MissionControlNodeProviderTableStatus,
|
||||||
/// Native `+0x58c`: total provider capacity in the supported unit-scale domain.
|
/// Native `+0x58c`: total provider capacity in the supported unit-scale domain.
|
||||||
@@ -470,8 +472,24 @@ pub struct MissionControlDeviceTableState {
|
|||||||
pub current_output_rate_5e0_bits: u32,
|
pub current_output_rate_5e0_bits: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CTLD owner profile selected by native component ID zero.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MissionControlOwnerProfile {
|
||||||
|
/// CTLD resource that supplied the profile words.
|
||||||
|
pub source: ResourceKey,
|
||||||
|
/// Unit DAT component that supplied this CTLD resource.
|
||||||
|
pub unit_component_index: usize,
|
||||||
|
/// Native component ID used by Control's profile-selection condition.
|
||||||
|
pub native_component_id: u32,
|
||||||
|
/// Original profile words held by the Control resource cache.
|
||||||
|
pub original_words: [u32; 27],
|
||||||
|
/// Initial per-owner copy; this is mutated independently from `original_words`.
|
||||||
|
pub current_words: [u32; 27],
|
||||||
|
}
|
||||||
|
|
||||||
struct ControlDeviceProjection {
|
struct ControlDeviceProjection {
|
||||||
devices: Vec<EffectiveControlDevice>,
|
devices: Vec<EffectiveControlDevice>,
|
||||||
|
owner_profile: Option<MissionControlOwnerProfile>,
|
||||||
node_provider_status: MissionControlNodeProviderTableStatus,
|
node_provider_status: MissionControlNodeProviderTableStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1514,6 +1532,7 @@ fn load_mission_with_options(
|
|||||||
original_id: draft.original_id,
|
original_id: draft.original_id,
|
||||||
handle: *handle,
|
handle: *handle,
|
||||||
status: MissionControlDeviceTableStatus::Available(projection.devices),
|
status: MissionControlDeviceTableStatus::Available(projection.devices),
|
||||||
|
owner_profile: projection.owner_profile,
|
||||||
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
||||||
&projection.node_provider_status,
|
&projection.node_provider_status,
|
||||||
),
|
),
|
||||||
@@ -1528,6 +1547,7 @@ fn load_mission_with_options(
|
|||||||
original_id: draft.original_id,
|
original_id: draft.original_id,
|
||||||
handle: *handle,
|
handle: *handle,
|
||||||
status: MissionControlDeviceTableStatus::Unavailable(reason.clone()),
|
status: MissionControlDeviceTableStatus::Unavailable(reason.clone()),
|
||||||
|
owner_profile: None,
|
||||||
node_provider_maximum_amount_bits: None,
|
node_provider_maximum_amount_bits: None,
|
||||||
node_provider_amount_bits: None,
|
node_provider_amount_bits: None,
|
||||||
node_provider_dirty: false,
|
node_provider_dirty: false,
|
||||||
@@ -5318,6 +5338,7 @@ fn build_control_device_projection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut devices = Vec::new();
|
let mut devices = Vec::new();
|
||||||
|
let mut owner_profile = None;
|
||||||
let mut node_provider_status = MissionControlNodeProviderTableStatus::Available(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 {
|
||||||
@@ -5353,6 +5374,15 @@ fn build_control_device_projection(
|
|||||||
|
|
||||||
let component_id =
|
let component_id =
|
||||||
component_ids[component_index].ok_or(Unavailable::ComponentAlignmentMismatch)?;
|
component_ids[component_index].ok_or(Unavailable::ComponentAlignmentMismatch)?;
|
||||||
|
if component_id == 0 {
|
||||||
|
owner_profile = Some(MissionControlOwnerProfile {
|
||||||
|
source: control.source.clone(),
|
||||||
|
unit_component_index: component_index,
|
||||||
|
native_component_id: component_id,
|
||||||
|
original_words: control.native_profile_words,
|
||||||
|
current_words: control.native_profile_words,
|
||||||
|
});
|
||||||
|
}
|
||||||
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 raw_local_index = draft.unit_components[component_index].parent_or_link;
|
||||||
@@ -5478,6 +5508,7 @@ fn build_control_device_projection(
|
|||||||
}
|
}
|
||||||
Ok(ControlDeviceProjection {
|
Ok(ControlDeviceProjection {
|
||||||
devices,
|
devices,
|
||||||
|
owner_profile,
|
||||||
node_provider_status,
|
node_provider_status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -6026,6 +6057,7 @@ mod tests {
|
|||||||
) -> PreparedComponentControl {
|
) -> PreparedComponentControl {
|
||||||
PreparedComponentControl {
|
PreparedComponentControl {
|
||||||
source: synthetic_resource_key(100 + index),
|
source: synthetic_resource_key(100 + index),
|
||||||
|
native_profile_words: [0; 27],
|
||||||
model_node_count: 0,
|
model_node_count: 0,
|
||||||
bindings: Vec::new(),
|
bindings: Vec::new(),
|
||||||
timeline_rows: Vec::new(),
|
timeline_rows: Vec::new(),
|
||||||
@@ -6387,6 +6419,7 @@ mod tests {
|
|||||||
slot: 7,
|
slot: 7,
|
||||||
},
|
},
|
||||||
status,
|
status,
|
||||||
|
owner_profile: None,
|
||||||
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
||||||
&node_provider_status,
|
&node_provider_status,
|
||||||
),
|
),
|
||||||
@@ -9088,6 +9121,89 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_projection_keeps_native_component_zero_profile_and_mutable_copy() {
|
||||||
|
let root_profile = std::array::from_fn(|index| 0xA500_0000 | index as u32);
|
||||||
|
let child_profile = [0xC11D_0000; 27];
|
||||||
|
let mut root = synthetic_component_control(
|
||||||
|
0,
|
||||||
|
vec![synthetic_control_record(19, 7, 0x10, None)],
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new()),
|
||||||
|
);
|
||||||
|
root.native_profile_words = root_profile;
|
||||||
|
let mut child = synthetic_component_control(
|
||||||
|
1,
|
||||||
|
vec![synthetic_control_record(19, 7, 0x20, None)],
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new()),
|
||||||
|
);
|
||||||
|
child.native_profile_words = child_profile;
|
||||||
|
let root_source = root.source.clone();
|
||||||
|
let (draft, assets, prototypes) = synthetic_control_fixture(
|
||||||
|
&[None, Some(PROTOTYPE_TYPE_INTO)],
|
||||||
|
&[1, 1],
|
||||||
|
&[1, 0],
|
||||||
|
&[0, 0],
|
||||||
|
&[1, 1],
|
||||||
|
vec![Some(root), Some(child)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut projection = build_control_device_projection(
|
||||||
|
0,
|
||||||
|
&draft,
|
||||||
|
&assets,
|
||||||
|
Some(&(0..prototypes.len())),
|
||||||
|
&prototypes,
|
||||||
|
)
|
||||||
|
.expect("root plus INTO projection");
|
||||||
|
assert_eq!(projection.devices.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
projection.devices[0]
|
||||||
|
.descriptor_provenance
|
||||||
|
.unit_component_index,
|
||||||
|
1,
|
||||||
|
"INTO with local index zero overrides the parent's first device"
|
||||||
|
);
|
||||||
|
|
||||||
|
let owner_profile = projection.owner_profile.as_mut().expect("root profile");
|
||||||
|
assert_eq!(owner_profile.unit_component_index, 0);
|
||||||
|
assert_eq!(owner_profile.native_component_id, 0);
|
||||||
|
assert_eq!(owner_profile.source, root_source);
|
||||||
|
assert_eq!(owner_profile.original_words, root_profile);
|
||||||
|
assert_eq!(owner_profile.current_words, root_profile);
|
||||||
|
owner_profile.current_words[3] ^= 0xFFFF;
|
||||||
|
assert_eq!(owner_profile.original_words, root_profile);
|
||||||
|
assert_ne!(owner_profile.current_words, owner_profile.original_words);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_projection_does_not_fall_back_to_child_profile_without_id_zero() {
|
||||||
|
let mut child = synthetic_component_control(
|
||||||
|
1,
|
||||||
|
vec![synthetic_control_record(19, 7, 0x20, None)],
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new()),
|
||||||
|
);
|
||||||
|
child.native_profile_words = [0xC11D_0000; 27];
|
||||||
|
let (draft, assets, prototypes) = synthetic_control_fixture(
|
||||||
|
&[None, Some(PROTOTYPE_TYPE_INTO)],
|
||||||
|
&[1, 1],
|
||||||
|
&[1, 0],
|
||||||
|
&[0, 0],
|
||||||
|
&[1, 1],
|
||||||
|
vec![None, Some(child)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let projection = build_control_device_projection(
|
||||||
|
0,
|
||||||
|
&draft,
|
||||||
|
&assets,
|
||||||
|
Some(&(0..prototypes.len())),
|
||||||
|
&prototypes,
|
||||||
|
)
|
||||||
|
.expect("child-only Control projection");
|
||||||
|
|
||||||
|
assert_eq!(projection.owner_profile, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn synthetic_node_provider_projection_keeps_native_rows_separate_from_mesh_keys() {
|
fn synthetic_node_provider_projection_keeps_native_rows_separate_from_mesh_keys() {
|
||||||
let controls = vec![
|
let controls = vec![
|
||||||
@@ -9780,6 +9896,38 @@ mod tests {
|
|||||||
let mine_control_before = loaded_mission_control_device_tables(&mine_engine)
|
let mine_control_before = loaded_mission_control_device_tables(&mine_engine)
|
||||||
.expect("licensed mine Control table")
|
.expect("licensed mine Control table")
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
let mine_prepared_controls = &mine_engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.expect("loaded licensed mine")
|
||||||
|
.mission_assets
|
||||||
|
.object_component_controls[0];
|
||||||
|
let root_control = mine_prepared_controls[0]
|
||||||
|
.as_ref()
|
||||||
|
.expect("licensed root CTLD");
|
||||||
|
let child_control = mine_prepared_controls[5]
|
||||||
|
.as_ref()
|
||||||
|
.expect("licensed EXTO CTLD");
|
||||||
|
let owner_profile = mine_control_before[0]
|
||||||
|
.owner_profile
|
||||||
|
.as_ref()
|
||||||
|
.expect("loaded root owner profile");
|
||||||
|
assert_eq!(owner_profile.unit_component_index, 0);
|
||||||
|
assert_eq!(owner_profile.native_component_id, 0);
|
||||||
|
assert_eq!(owner_profile.source, root_control.source);
|
||||||
|
assert_eq!(owner_profile.source.name.0.as_slice(), b"fr_l_mine.ctl");
|
||||||
|
assert_eq!(
|
||||||
|
owner_profile.original_words,
|
||||||
|
root_control.native_profile_words
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
owner_profile.current_words,
|
||||||
|
root_control.native_profile_words
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
root_control.native_profile_words, child_control.native_profile_words,
|
||||||
|
"the licensed child CTLD has its own profile words"
|
||||||
|
);
|
||||||
let mine_control_handle = mine_control_before[0].handle;
|
let mine_control_handle = mine_control_before[0].handle;
|
||||||
let MissionControlDeviceTableStatus::Available(mine_control_devices) =
|
let MissionControlDeviceTableStatus::Available(mine_control_devices) =
|
||||||
&mine_control_before[0].status
|
&mine_control_before[0].status
|
||||||
@@ -11672,6 +11820,7 @@ mod tests {
|
|||||||
original_id: Some(OriginalObjectId(original_id)),
|
original_id: Some(OriginalObjectId(original_id)),
|
||||||
handle,
|
handle,
|
||||||
status: MissionControlDeviceTableStatus::Available(vec![device]),
|
status: MissionControlDeviceTableStatus::Available(vec![device]),
|
||||||
|
owner_profile: None,
|
||||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
||||||
node_provider_maximum_amount_bits: Some(0),
|
node_provider_maximum_amount_bits: Some(0),
|
||||||
node_provider_amount_bits: Some(0),
|
node_provider_amount_bits: Some(0),
|
||||||
@@ -11780,6 +11929,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
ore_device,
|
ore_device,
|
||||||
]),
|
]),
|
||||||
|
owner_profile: None,
|
||||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(vec![
|
node_provider_status: MissionControlNodeProviderTableStatus::Available(vec![
|
||||||
synthetic_node_provider_row(0, 0, 1.0_f32.to_bits()),
|
synthetic_node_provider_row(0, 0, 1.0_f32.to_bits()),
|
||||||
]),
|
]),
|
||||||
@@ -11834,6 +11984,7 @@ mod tests {
|
|||||||
original_id: Some(object.original_id),
|
original_id: Some(object.original_id),
|
||||||
handle: object.handle,
|
handle: object.handle,
|
||||||
status: MissionControlDeviceTableStatus::Available(Vec::new()),
|
status: MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
owner_profile: None,
|
||||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
||||||
node_provider_maximum_amount_bits: Some(0),
|
node_provider_maximum_amount_bits: Some(0),
|
||||||
node_provider_amount_bits: Some(0),
|
node_provider_amount_bits: Some(0),
|
||||||
|
|||||||
@@ -520,6 +520,13 @@ provider rows после пропуска первого `EXTO` row. Он выз
|
|||||||
getter оригинальной DLL здесь не исполняется. Это не запись живого
|
getter оригинальной DLL здесь не исполняется. Это не запись живого
|
||||||
resource-device значения во время игры.
|
resource-device значения во время игры.
|
||||||
|
|
||||||
|
### Профиль владельца Control из CTLD
|
||||||
|
|
||||||
|
Смысл отдельных слов, последующие изменения mutable-копии и чтения в Control
|
||||||
|
`fca0/fac0` остаются открытыми. Загрузочный тест проверяет CPU-проекцию, но не
|
||||||
|
исполнение native полного загрузчика или игрового tick. Точные writer, RVA и
|
||||||
|
указатели описаны в [техническом разделе о загрузке CTLD](script-vm.md#профиль-владельца-control-при-загрузке-ctld).
|
||||||
|
|
||||||
Закрывающий эксперимент: трассировать `LoadControlSystem`,
|
Закрывающий эксперимент: трассировать `LoadControlSystem`,
|
||||||
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
|
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
|
||||||
каждый изменяемый field с созданным shape, contact или реакцией на движение.
|
каждый изменяемый field с созданным shape, contact или реакцией на движение.
|
||||||
|
|||||||
@@ -953,6 +953,30 @@ Ignored Rust-тест также проходит полный неотфиль
|
|||||||
Это проверка CPU-проекции реальной таблицы, а не сравнение целой миссии с
|
Это проверка CPU-проекции реальной таблицы, а не сравнение целой миссии с
|
||||||
оригиналом.
|
оригиналом.
|
||||||
|
|
||||||
|
#### Профиль владельца Control при загрузке CTLD
|
||||||
|
|
||||||
|
Для `Control.dll` с SHA-256
|
||||||
|
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`
|
||||||
|
`FUN_10008b10` (RVA `0x8b10`) проверяет native component ID из параметра
|
||||||
|
`+0xc0`. Только при значении `0` запись около RVA `0x8ee9` указывает на блок
|
||||||
|
`payload + 0x14` в `owner + 0x46c` и копирует `0x1b` DWORD в `owner + 0x470`.
|
||||||
|
Затем `FUN_100143e0` (RVA `0x143e0`) связывает указатель исходного блока и
|
||||||
|
указатель изменяемой копии; при `this = owner + 0x1b4` это указатели
|
||||||
|
`owner + 0x360` и `owner + 0x364`. Итого CTLD bytes `20..128` — 27 сырых слов:
|
||||||
|
они первоначально одинаковы в двух областях, но изменение копии владельца не
|
||||||
|
меняет оригинал.
|
||||||
|
|
||||||
|
Текущая Control-проекция назначает корню native ID `0`, а следующим активным
|
||||||
|
компонентам — ID от `1`. `INTO` с локальным индексом `0` может переопределять
|
||||||
|
первую строку таблицы, не меняя native ID самого компонента. В Rust
|
||||||
|
`PreparedComponentControl` сохраняет блок как `[u32; 27]`, а полная проекция
|
||||||
|
прикрепляет к ID `0` источник CTLD, исходный массив и отдельную начальную копию
|
||||||
|
владельца. Загрузочный тест `UNITS/BUILDS/MINE/min_l_n1.dat` подтверждает
|
||||||
|
источник `fr_l_mine.ctl` и сырые слова корня; отдельный synthetic-тест проверяет
|
||||||
|
`INTO` slot `0`, независимость массивов и отсутствие fallback к дочернему
|
||||||
|
профилю. Это подтверждает декодирование и CPU-проекцию, но не исполнение native
|
||||||
|
полного загрузчика.
|
||||||
|
|
||||||
#### Активация и начальные часы
|
#### Активация и начальные часы
|
||||||
|
|
||||||
Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490`
|
Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490`
|
||||||
|
|||||||
@@ -691,6 +691,18 @@ Rust full-loader теста на оригинальных ресурсах. Ве
|
|||||||
`EXTO` подтверждены статически по native ASM; отдельный native full-load oracle
|
`EXTO` подтверждены статически по native ASM; отдельный native full-load oracle
|
||||||
для всей таблицы не запускался.
|
для всей таблицы не запускался.
|
||||||
|
|
||||||
|
Профиль владельца читается из отдельного блока каждого CTLD сразу после пяти
|
||||||
|
слов заголовка: это 27 исходных DWORD, начиная с байта `20`. Control выбирает
|
||||||
|
для владельца только компонент с native ID `0`; его блок остаётся исходной
|
||||||
|
копией, а те же 27 слов отдельно копируются в изменяемую память владельца.
|
||||||
|
Поэтому профиль дочернего компонента, даже если его `INTO` обновляет строку
|
||||||
|
таблицы `0`, не подменяет профиль корня. В Rust `PreparedComponentControl`
|
||||||
|
сохраняет эти слова без предположений об их типах, а полная Control-проекция
|
||||||
|
хранит источник, native ID, исходный массив и отдельную начальную копию. Эти
|
||||||
|
массивы сначала равны; отсутствие CTLD у компонента с ID `0` остаётся явным
|
||||||
|
`None`, без дочернего fallback или синтетических нулей. Точные native writes и
|
||||||
|
открытые вопросы о смысле полей приведены в [техническом разборе загрузки CTLD](../appendices/script-vm.md#профиль-владельца-control-при-загрузке-ctld).
|
||||||
|
|
||||||
Для tag `19` фабрика создаёт объект с его исходными классом и vtable. Базовый
|
Для tag `19` фабрика создаёт объект с его исходными классом и vtable. Базовый
|
||||||
конструктор `FUN_100218b0` задаёт multiplier `+0x4c = 1.0`. Override через
|
конструктор `FUN_100218b0` задаёт multiplier `+0x4c = 1.0`. Override через
|
||||||
`FUN_1002d890` вызывает у выбранного объекта vtable slot `+0x1c`, то есть
|
`FUN_1002d890` вызывает у выбранного объекта vtable slot `+0x1c`, то есть
|
||||||
|
|||||||
Reference in new issue
Block a user