feat(runtime): propagate Control node stage changes
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
This commit is contained in:
5 files changed
+410
-47
No files matched your search
@@ -2240,6 +2240,32 @@ pub fn update_loaded_mission_control_node_stage(
|
|||||||
update_control_node_stage(table, key, update_effects)
|
update_control_node_stage(table, key, update_effects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates one loaded Control node and then walks its provider descendants in
|
||||||
|
/// native depth-first order (`Control.dll+0x11130`).
|
||||||
|
///
|
||||||
|
/// Unlike [`update_loaded_mission_control_node_stage`], this also propagates
|
||||||
|
/// depletion and stage flags through descendants. The whole provider table is
|
||||||
|
/// committed only if every node can be updated; this CPU projection does not
|
||||||
|
/// run unsupported visibility, FX, or host callbacks.
|
||||||
|
pub fn propagate_loaded_mission_control_node_stage(
|
||||||
|
engine: &mut Engine,
|
||||||
|
handle: ObjectHandle,
|
||||||
|
key: u32,
|
||||||
|
update_effects: bool,
|
||||||
|
) -> Result<(), ControlResourceAccessError> {
|
||||||
|
transform_state(&engine.world, handle).map_err(ControlResourceAccessError::World)?;
|
||||||
|
let loaded = engine
|
||||||
|
.loaded
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(ControlResourceAccessError::MissionUnavailable)?;
|
||||||
|
let table = loaded
|
||||||
|
.control_device_tables
|
||||||
|
.iter_mut()
|
||||||
|
.find(|table| table.handle == handle)
|
||||||
|
.ok_or(ControlResourceAccessError::ObjectUnavailable { handle })?;
|
||||||
|
propagate_control_node_stage(table, key, update_effects)
|
||||||
|
}
|
||||||
|
|
||||||
fn update_control_node_stage(
|
fn update_control_node_stage(
|
||||||
table: &mut MissionControlDeviceTableState,
|
table: &mut MissionControlDeviceTableState,
|
||||||
key: u32,
|
key: u32,
|
||||||
@@ -2346,6 +2372,120 @@ fn update_control_node_stage(
|
|||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn propagate_control_node_stage(
|
||||||
|
table: &mut MissionControlDeviceTableState,
|
||||||
|
key: u32,
|
||||||
|
update_effects: bool,
|
||||||
|
) -> Result<(), ControlResourceAccessError> {
|
||||||
|
let mut updated = table.clone();
|
||||||
|
propagate_control_node_stage_inner(&mut updated, key, update_effects)?;
|
||||||
|
*table = updated;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn propagate_control_node_stage_inner(
|
||||||
|
table: &mut MissionControlDeviceTableState,
|
||||||
|
key: u32,
|
||||||
|
update_effects: bool,
|
||||||
|
) -> Result<(), ControlResourceAccessError> {
|
||||||
|
let (index, row_count, previous_stage) = match &table.node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => {
|
||||||
|
let row_count = rows.len();
|
||||||
|
let index = usize::try_from(key)
|
||||||
|
.ok()
|
||||||
|
.filter(|index| *index < row_count)
|
||||||
|
.ok_or(ControlResourceAccessError::NodeProviderKeyOutOfRange { key, row_count })?;
|
||||||
|
(index, row_count, rows[index].current_stage)
|
||||||
|
}
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
|
||||||
|
reason.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update_control_node_stage(table, key, update_effects)?;
|
||||||
|
|
||||||
|
let (stage, flags) = match &table.node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => {
|
||||||
|
let row = &rows[index];
|
||||||
|
let flags = row
|
||||||
|
.current_flags
|
||||||
|
.ok_or(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider flags",
|
||||||
|
})?;
|
||||||
|
(row.current_stage, flags)
|
||||||
|
}
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
|
||||||
|
reason.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let depleted_or_worsened = previous_stage < stage || flags & 0x10 != 0;
|
||||||
|
|
||||||
|
for child_index in index + 1..row_count {
|
||||||
|
let (metadata, child_flags) = match &table.node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => {
|
||||||
|
let row = &rows[child_index];
|
||||||
|
let metadata =
|
||||||
|
row.initial_mesh_state
|
||||||
|
.ok_or(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider mesh metadata",
|
||||||
|
})?;
|
||||||
|
let child_flags = row.current_flags;
|
||||||
|
(metadata, child_flags)
|
||||||
|
}
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
|
||||||
|
reason.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if i64::from(metadata.parent_index) != i64::from(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_key = u32::try_from(child_index)
|
||||||
|
.map_err(|_| ControlResourceAccessError::InvalidNodeStageState { key })?;
|
||||||
|
let child_flags = child_flags.ok_or(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider flags",
|
||||||
|
})?;
|
||||||
|
if child_flags & 0x140 == 0 {
|
||||||
|
let child_flags = if flags & 0xc0 == 0 {
|
||||||
|
(child_flags | if depleted_or_worsened { 0x100 } else { 0 }) & !0x80
|
||||||
|
} else {
|
||||||
|
child_flags | 0x80
|
||||||
|
};
|
||||||
|
if let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
{
|
||||||
|
rows[child_index].current_flags = Some(child_flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if depleted_or_worsened {
|
||||||
|
let child_amount = match &table.node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => {
|
||||||
|
f32::from_bits(rows[child_index].current_amount_bits)
|
||||||
|
}
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
|
||||||
|
reason.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
apply_control_node_delta(table, child_key, -child_amount)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
propagate_control_node_stage_inner(table, child_key, update_effects)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn control_stage_product(count: u32, ratio: f32) -> f32 {
|
fn control_stage_product(count: u32, ratio: f32) -> f32 {
|
||||||
if ratio == 0.0 {
|
if ratio == 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -5278,6 +5418,10 @@ mod tests {
|
|||||||
update_loaded_mission_control_node_stage(&mut engine, handle, 1, false),
|
update_loaded_mission_control_node_stage(&mut engine, handle, 1, false),
|
||||||
Err(ControlResourceAccessError::MissionUnavailable)
|
Err(ControlResourceAccessError::MissionUnavailable)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
propagate_loaded_mission_control_node_stage(&mut engine, handle, 1, false),
|
||||||
|
Err(ControlResourceAccessError::MissionUnavailable)
|
||||||
|
);
|
||||||
|
|
||||||
let stale = ObjectHandle {
|
let stale = ObjectHandle {
|
||||||
generation: handle.generation.wrapping_add(1),
|
generation: handle.generation.wrapping_add(1),
|
||||||
@@ -5321,6 +5465,12 @@ mod tests {
|
|||||||
fparkan_world::WorldError::StaleHandle
|
fparkan_world::WorldError::StaleHandle
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
propagate_loaded_mission_control_node_stage(&mut engine, stale, 1, false),
|
||||||
|
Err(ControlResourceAccessError::World(
|
||||||
|
fparkan_world::WorldError::StaleHandle
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
fparkan_world::request_delete(&mut engine.world, handle).expect("delete object");
|
fparkan_world::request_delete(&mut engine.world, handle).expect("delete object");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -5329,6 +5479,12 @@ mod tests {
|
|||||||
fparkan_world::WorldError::StaleHandle
|
fparkan_world::WorldError::StaleHandle
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
propagate_loaded_mission_control_node_stage(&mut engine, handle, 1, false),
|
||||||
|
Err(ControlResourceAccessError::World(
|
||||||
|
fparkan_world::WorldError::StaleHandle
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apply_loaded_mission_control_node_delta(&mut engine, handle, 1, -1.0),
|
apply_loaded_mission_control_node_delta(&mut engine, handle, 1, -1.0),
|
||||||
@@ -5696,6 +5852,132 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_node_stage_descendants_follow_native_walk_and_rollback() {
|
||||||
|
let make_rows = |parents: [i32; 5]| {
|
||||||
|
parents
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, parent_index)| {
|
||||||
|
let flags = if index == 0 { 0xd2 } else { 2 };
|
||||||
|
let mut row = synthetic_node_provider_row(0, index, 1.0_f32.to_bits());
|
||||||
|
row.current_flags = Some(flags);
|
||||||
|
row.initial_mesh_state = Some(MissionControlNodeInitialMeshState {
|
||||||
|
parent_index,
|
||||||
|
flags,
|
||||||
|
stage_count: 2,
|
||||||
|
});
|
||||||
|
row.mesh_state = Some(MissionControlNodeMeshState {
|
||||||
|
type1_value_bits: 1.0_f32.to_bits(),
|
||||||
|
visible_stage: 0,
|
||||||
|
});
|
||||||
|
row
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rows = make_rows([-1, 0, 0, 1, 3]);
|
||||||
|
rows[1].current_flags = Some(0x140);
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x140;
|
||||||
|
let mut table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows),
|
||||||
|
);
|
||||||
|
propagate_control_node_stage(&mut table, 0, false).unwrap();
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(rows[0].current_flags, Some(0xd2));
|
||||||
|
for index in [1, 3, 4, 2] {
|
||||||
|
assert_eq!(rows[index].current_amount_bits, 0.0_f32.to_bits());
|
||||||
|
assert_eq!(
|
||||||
|
rows[index].current_flags,
|
||||||
|
Some(if index == 1 { 0x150 } else { 0x92 })
|
||||||
|
);
|
||||||
|
assert_eq!(rows[index].processed_amount_bits, 100.0_f32.to_bits());
|
||||||
|
}
|
||||||
|
assert_eq!(table.node_provider_amount_bits, Some(100.0_f32.to_bits()));
|
||||||
|
|
||||||
|
let mut rows = make_rows([-1, 0, 0, 1, 3]);
|
||||||
|
rows[0].current_flags = Some(2);
|
||||||
|
rows[0].current_type1_value_bits = 0.0_f32.to_bits();
|
||||||
|
rows[1].current_flags = Some(0x82);
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x82;
|
||||||
|
let mut table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows),
|
||||||
|
);
|
||||||
|
propagate_control_node_stage(&mut table, 0, false).unwrap();
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(rows[0].current_stage, 2);
|
||||||
|
assert_eq!(rows[0].current_flags, Some(0x202));
|
||||||
|
for index in [1, 3, 4, 2] {
|
||||||
|
assert_eq!(rows[index].current_amount_bits, 0.0_f32.to_bits());
|
||||||
|
assert_eq!(rows[index].current_stage, 2);
|
||||||
|
assert_eq!(rows[index].current_flags, Some(0x312));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = make_rows([-1, 0, 0, 1, 3]);
|
||||||
|
rows[0].current_flags = Some(2);
|
||||||
|
rows[0].current_stage = 1;
|
||||||
|
rows[0].current_type1_value_bits = 0.5_f32.to_bits();
|
||||||
|
rows[0].initial_mesh_state.as_mut().unwrap().stage_count = 2;
|
||||||
|
rows[1].current_flags = Some(0x8a);
|
||||||
|
rows[1].current_stage = 0;
|
||||||
|
rows[1].current_type1_value_bits = 0.5_f32.to_bits();
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x8a;
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().stage_count = 2;
|
||||||
|
rows[1].mesh_state.as_mut().unwrap().type1_value_bits = 0.5_f32.to_bits();
|
||||||
|
let mut table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows),
|
||||||
|
);
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
propagate_control_node_stage(&mut table, 0, true),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedNodeEffects { key: 1 })
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
propagate_control_node_stage(&mut table, 0, false).unwrap();
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(rows[1].current_flags, Some(0x20a));
|
||||||
|
assert_eq!(rows[1].current_stage, 1);
|
||||||
|
assert_eq!(rows[1].current_amount_bits, 100.0_f32.to_bits());
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
propagate_control_node_stage(&mut table, u32::MAX, false),
|
||||||
|
Err(ControlResourceAccessError::NodeProviderKeyOutOfRange {
|
||||||
|
key: u32::MAX,
|
||||||
|
row_count: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
|
||||||
|
let mut rows = make_rows([-1, 0, 0, 1, 3]);
|
||||||
|
rows[2].mesh_state = None;
|
||||||
|
rows[4].current_flags = None;
|
||||||
|
let mut table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows),
|
||||||
|
);
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
propagate_control_node_stage(&mut table, 0, false),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider flags",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn common_factory_into_overrides_preserve_native_constructor_defaults() {
|
fn common_factory_into_overrides_preserve_native_constructor_defaults() {
|
||||||
const INTO: u32 = u32::from_le_bytes(*b"INTO");
|
const INTO: u32 = u32::from_le_bytes(*b"INTO");
|
||||||
@@ -6848,11 +7130,38 @@ mod tests {
|
|||||||
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);
|
||||||
assert_eq!(draft.unit_components[0].immediate_child_count, 5);
|
assert_eq!(draft.unit_components[0].immediate_child_count, 5);
|
||||||
let tables = loaded_mission_control_device_tables(&engine).expect("Control tables");
|
let tables = loaded_mission_control_device_tables(&engine)
|
||||||
|
.expect("Control tables")
|
||||||
|
.to_vec();
|
||||||
assert_eq!(tables.len(), 1);
|
assert_eq!(tables.len(), 1);
|
||||||
assert_eq!(tables[0].original_id, Some(OriginalObjectId(0)));
|
assert_eq!(tables[0].original_id, Some(OriginalObjectId(0)));
|
||||||
assert!(tables[0].node_provider_dirty);
|
assert!(tables[0].node_provider_dirty);
|
||||||
let control_handle = tables[0].handle;
|
let control_handle = tables[0].handle;
|
||||||
|
let node_provider_count = match &tables[0].node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => rows.len(),
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
panic!("licensed Control provider rows unavailable: {reason:?}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let before_invalid_node_key = loaded_mission_control_device_tables(&engine)
|
||||||
|
.unwrap()
|
||||||
|
.to_vec();
|
||||||
|
assert_eq!(
|
||||||
|
propagate_loaded_mission_control_node_stage(
|
||||||
|
&mut engine,
|
||||||
|
control_handle,
|
||||||
|
u32::MAX,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Err(ControlResourceAccessError::NodeProviderKeyOutOfRange {
|
||||||
|
key: u32::MAX,
|
||||||
|
row_count: node_provider_count,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded_mission_control_device_tables(&engine),
|
||||||
|
Some(before_invalid_node_key.as_slice())
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query_loaded_mission_control_resource(&engine, control_handle, 1)
|
query_loaded_mission_control_resource(&engine, control_handle, 1)
|
||||||
.expect("licensed selector 1")
|
.expect("licensed selector 1")
|
||||||
@@ -7219,13 +7528,13 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(update_loaded_mission_control_node_stage(
|
propagate_loaded_mission_control_node_stage(
|
||||||
&mut mine_engine,
|
&mut mine_engine,
|
||||||
mine_profile.handle,
|
mine_profile.handle,
|
||||||
stage_key,
|
stage_key,
|
||||||
false
|
false,
|
||||||
)
|
)
|
||||||
.unwrap());
|
.expect("licensed Control descendant stage propagation");
|
||||||
let MissionControlNodeProviderTableStatus::Available(rows) =
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
&loaded_mission_control_device_tables(&mine_engine).unwrap()[0].node_provider_status
|
&loaded_mission_control_device_tables(&mine_engine).unwrap()[0].node_provider_status
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1131,7 +1131,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
new float[] { 25.0f, 10.0f, 5.0f, 12.0f, 7.0f },
|
new float[] { 25.0f, 10.0f, 5.0f, 12.0f, 7.0f },
|
||||||
new byte[] { 0x02, 0x02, 0x02, 0x02, 0x02 });
|
new byte[] { 0x02, 0x02, 0x02, 0x02, 0x02 });
|
||||||
const int rowCount = 5;
|
const int rowCount = 5;
|
||||||
int[] parents = { -1, 0, 1, 0, 0 };
|
int[] parents = { -1, 0, 0, 1, 3 };
|
||||||
uint[] stages = { 4, 2, 3, 2, 2 };
|
uint[] stages = { 4, 2, 3, 2, 2 };
|
||||||
uint[] flags = { 0x02, 0x42, 0x02, 0x102, 0x02 };
|
uint[] flags = { 0x02, 0x42, 0x02, 0x102, 0x02 };
|
||||||
for (int index = 0; index < rowCount; index++) {
|
for (int index = 0; index < rowCount; index++) {
|
||||||
@@ -1169,14 +1169,14 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
|
|
||||||
IntPtr parent = table.Rows;
|
IntPtr parent = table.Rows;
|
||||||
IntPtr protected40 = IntPtr.Add(table.Rows, 0x2c);
|
IntPtr protected40 = IntPtr.Add(table.Rows, 0x2c);
|
||||||
IntPtr grandchild = IntPtr.Add(table.Rows, 2 * 0x2c);
|
IntPtr sibling = IntPtr.Add(table.Rows, 2 * 0x2c);
|
||||||
IntPtr protected100 = IntPtr.Add(table.Rows, 3 * 0x2c);
|
IntPtr protected100 = IntPtr.Add(table.Rows, 3 * 0x2c);
|
||||||
IntPtr sibling = IntPtr.Add(table.Rows, 4 * 0x2c);
|
IntPtr grandchild = IntPtr.Add(table.Rows, 4 * 0x2c);
|
||||||
IntPtr parentMeshRow = meshRows;
|
IntPtr parentMeshRow = meshRows;
|
||||||
IntPtr bit40MeshRow = IntPtr.Add(meshRows, 0x130);
|
IntPtr bit40MeshRow = IntPtr.Add(meshRows, 0x130);
|
||||||
IntPtr grandchildMeshRow = IntPtr.Add(meshRows, 2 * 0x130);
|
IntPtr siblingMeshRow = IntPtr.Add(meshRows, 2 * 0x130);
|
||||||
IntPtr bit100MeshRow = IntPtr.Add(meshRows, 3 * 0x130);
|
IntPtr bit100MeshRow = IntPtr.Add(meshRows, 3 * 0x130);
|
||||||
IntPtr siblingMeshRow = IntPtr.Add(meshRows, 4 * 0x130);
|
IntPtr grandchildMeshRow = IntPtr.Add(meshRows, 4 * 0x130);
|
||||||
Expect(ReadFloat(parent, 0x08) == 25.0f && ReadFloat(parent, 0x0c) == 25.0f &&
|
Expect(ReadFloat(parent, 0x08) == 25.0f && ReadFloat(parent, 0x0c) == 25.0f &&
|
||||||
unchecked((uint)Marshal.ReadInt32(parent, 0x18)) == 3,
|
unchecked((uint)Marshal.ReadInt32(parent, 0x18)) == 3,
|
||||||
"Control 0x11130 updates the parent stage and processed amount first");
|
"Control 0x11130 updates the parent stage and processed amount first");
|
||||||
@@ -1184,40 +1184,41 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
unchecked((uint)Marshal.ReadInt32(protected40, 0x18)) == 0 &&
|
unchecked((uint)Marshal.ReadInt32(protected40, 0x18)) == 0 &&
|
||||||
(unchecked((uint)Marshal.ReadInt32(protected40, 0x28)) & 0x50) == 0x50,
|
(unchecked((uint)Marshal.ReadInt32(protected40, 0x28)) & 0x50) == 0x50,
|
||||||
"Protected bit 0x40 does not stop child depletion, while its depleted-stage early return remains visible");
|
"Protected bit 0x40 does not stop child depletion, while its depleted-stage early return remains visible");
|
||||||
Expect(ReadFloat(grandchild, 0x08) == 0.0f && ReadFloat(grandchild, 0x0c) == 125.0f &&
|
|
||||||
unchecked((uint)Marshal.ReadInt32(grandchild, 0x18)) == 0 &&
|
|
||||||
(unchecked((uint)Marshal.ReadInt32(grandchild, 0x28)) & 0x90) == 0x90,
|
|
||||||
"Control 0x11130 drains a protected child's descendant but its inherited 0x80 flag preserves the depleted stage snapshot");
|
|
||||||
Expect(ReadFloat(protected100, 0x08) == 0.0f && ReadFloat(protected100, 0x0c) == 0.0f &&
|
Expect(ReadFloat(protected100, 0x08) == 0.0f && ReadFloat(protected100, 0x0c) == 0.0f &&
|
||||||
unchecked((uint)Marshal.ReadInt32(protected100, 0x18)) == 2,
|
unchecked((uint)Marshal.ReadInt32(protected100, 0x18)) == 2 &&
|
||||||
"Protected bit 0x100 does not stop child depletion or its later stage update");
|
(unchecked((uint)Marshal.ReadInt32(protected100, 0x28)) & 0x100) != 0,
|
||||||
|
"Protected bit 0x100 skips flag rewriting but does not stop drain or descendant traversal");
|
||||||
|
Expect(ReadFloat(grandchild, 0x08) == 0.0f && ReadFloat(grandchild, 0x0c) == 0.0f &&
|
||||||
|
unchecked((uint)Marshal.ReadInt32(grandchild, 0x18)) == 2 &&
|
||||||
|
(unchecked((uint)Marshal.ReadInt32(grandchild, 0x28)) & 0x100) != 0,
|
||||||
|
"The protected row's grandchild is visited before the root's next sibling");
|
||||||
Expect(ReadFloat(sibling, 0x08) == 0.0f && ReadFloat(sibling, 0x0c) == 0.0f &&
|
Expect(ReadFloat(sibling, 0x08) == 0.0f && ReadFloat(sibling, 0x0c) == 0.0f &&
|
||||||
unchecked((uint)Marshal.ReadInt32(sibling, 0x18)) == 2 &&
|
unchecked((uint)Marshal.ReadInt32(sibling, 0x18)) == 3 &&
|
||||||
(unchecked((uint)Marshal.ReadInt32(sibling, 0x28)) & 0x100) != 0,
|
(unchecked((uint)Marshal.ReadInt32(sibling, 0x28)) & 0x100) != 0,
|
||||||
"An unprotected sibling retains the native damaged flag while updating its stage");
|
"An unprotected sibling retains the native damaged flag while updating its stage");
|
||||||
Expect(unchecked((uint)Marshal.ReadInt32(parentMeshRow, 0x1c)) == 3 &&
|
Expect(unchecked((uint)Marshal.ReadInt32(parentMeshRow, 0x1c)) == 3 &&
|
||||||
unchecked((uint)Marshal.ReadInt32(bit40MeshRow, 0x1c)) == unchecked((uint)-99) &&
|
unchecked((uint)Marshal.ReadInt32(bit40MeshRow, 0x1c)) == unchecked((uint)-99) &&
|
||||||
unchecked((uint)Marshal.ReadInt32(grandchildMeshRow, 0x1c)) == unchecked((uint)-99) &&
|
unchecked((uint)Marshal.ReadInt32(siblingMeshRow, 0x1c)) == 2 &&
|
||||||
unchecked((uint)Marshal.ReadInt32(bit100MeshRow, 0x1c)) == 1 &&
|
unchecked((uint)Marshal.ReadInt32(bit100MeshRow, 0x1c)) == 1 &&
|
||||||
unchecked((uint)Marshal.ReadInt32(siblingMeshRow, 0x1c)) == 1,
|
unchecked((uint)Marshal.ReadInt32(grandchildMeshRow, 0x1c)) == 1,
|
||||||
"ANI slot 0x44 reaches native AniMesh RVA 0x5810 for changed visible stages only");
|
"ANI slot 0x44 reaches native AniMesh RVA 0x5810 for changed visible stages only");
|
||||||
Expect(controlNodeUpdateCount == 4 &&
|
Expect(controlNodeUpdateCount == 4 &&
|
||||||
String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "1,2,3,4" &&
|
String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "1,3,4,2" &&
|
||||||
String.Join(",", controlNodeUpdateFlags.ConvertAll(value => value.ToString("x")).ToArray()) == "42,82,102,102" &&
|
String.Join(",", controlNodeUpdateFlags.ConvertAll(value => value.ToString("x")).ToArray()) == "42,102,102,102" &&
|
||||||
!controlNodeUpdateOwnerMismatch,
|
!controlNodeUpdateOwnerMismatch,
|
||||||
"Control 0x11130 traverses descendants depth-first, checking callback owners and protected flags");
|
"Control 0x11130 traverses descendants depth-first, checking callback owners and protected flags");
|
||||||
Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,3:1,4:1" &&
|
Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,3:1,4:1,2:2" &&
|
||||||
aniMeshNodeLifecycleCallbacks.Count == 0 &&
|
aniMeshNodeLifecycleCallbacks.Count == 0 &&
|
||||||
!aniMeshCallbackOwnerMismatch && !nativeAniMeshStageCallbackMissing,
|
!aniMeshCallbackOwnerMismatch && !nativeAniMeshStageCallbackMissing,
|
||||||
"Control descendant update preserves the native callback order and stage bounds");
|
"Control descendant update preserves the native callback order and stage bounds");
|
||||||
Expect(String.Join(",", controlProviderCallbackTrace.ToArray()) ==
|
Expect(String.Join(",", controlProviderCallbackTrace.ToArray()) ==
|
||||||
"stage:0:3,amount:1,amount:2,amount:3,stage:3:1,amount:4,stage:4:1",
|
"stage:0:3,amount:1,amount:3,stage:3:1,amount:4,stage:4:1,amount:2,stage:2:2",
|
||||||
"Control 0x11130 keeps amount, lifecycle, and visual-stage callbacks in native depth-first order");
|
"Control 0x11130 keeps amount, lifecycle, and visual-stage callbacks in native depth-first order");
|
||||||
Expect(ReadFloat(table.Owner, 0x590) == 25.0f,
|
Expect(ReadFloat(table.Owner, 0x590) == 25.0f,
|
||||||
"Control 0x10f30 keeps the aggregate amount in sync after recursive child depletion");
|
"Control 0x10f30 keeps the aggregate amount in sync after recursive child depletion");
|
||||||
|
|
||||||
Console.WriteLine("native-control-parent-propagation: controlHash=" + ExpectedControlSha256 +
|
Console.WriteLine("native-control-parent-propagation: controlHash=" + ExpectedControlSha256 +
|
||||||
" rva=0x11130 childOrder=[1,2,3,4] protected40/100=depleted bit40-stage-return=preserved" +
|
" rva=0x11130 childOrder=[1,3,4,2] protected40/100=depleted bit40-stage-return=preserved" +
|
||||||
" callbacks=" + String.Join(",", controlProviderCallbackTrace.ToArray()));
|
" callbacks=" + String.Join(",", controlProviderCallbackTrace.ToArray()));
|
||||||
activeControlProviderRows = IntPtr.Zero;
|
activeControlProviderRows = IntPtr.Zero;
|
||||||
activeAniMeshInterface = IntPtr.Zero;
|
activeAniMeshInterface = IntPtr.Zero;
|
||||||
|
|||||||
@@ -351,11 +351,24 @@ MSH-часть проверки подтверждает начальный сн
|
|||||||
Control. Обработанное количество при ней остаётся прежним. Отдельный
|
Control. Обработанное количество при ней остаётся прежним. Отдельный
|
||||||
`update_loaded_mission_control_node_stage` реализует наследование parent ratio
|
`update_loaded_mission_control_node_stage` реализует наследование parent ratio
|
||||||
и stage, ceiling-арифметику, CPU visible stage и копию processed amount.
|
и stage, ceiling-арифметику, CPU visible stage и копию processed amount.
|
||||||
Обход потомков `0x11130` пока не перенесён. Нужные lifecycle/visibility/FX
|
Отдельная `propagate_loaded_mission_control_node_stage` переносит `0x11130`:
|
||||||
callbacks дают явные ошибки до записи; `update_effects = true` поддержан
|
после обновления строки она сканирует более поздние provider-строки и обходит
|
||||||
только без фактической смены стадии. Licensed двухстадийный узел шахты
|
найденные поддеревья в native depth-first порядке. Для родителей
|
||||||
подтверждает отдельный переход `0 → 1` при доле `0.5`, без FX и window path. Гибель root/critical узла и неизвестные flags/aggregate дают явную
|
`[-1, 0, 0, 1, 3]` порядок — `1, 3, 4, 2`. Переносит унаследованный Control
|
||||||
ошибку до записи; неподдержанные численные диапазоны тоже отклоняются.
|
ratio/stage, флаги `0x80/0x100` и drain количества; маска `0x140` пропускает
|
||||||
|
только перезапись flags, не drain и рекурсию. Ухудшение определяется ростом
|
||||||
|
стадии либо флагом истощения `0x10` уже обновлённого родителя. Отдельный
|
||||||
|
`update_loaded_mission_control_node_stage` сохраняет одиночную семантику
|
||||||
|
`0x11220`.
|
||||||
|
|
||||||
|
Пропагация клонирует Control provider-таблицу и публикует её только после
|
||||||
|
успешного обхода. Неизвестная parent-связь в просматриваемом суффиксе,
|
||||||
|
lifecycle/visibility/FX callback или неподдержанное численное значение дают
|
||||||
|
ошибку без частичных изменений. `update_effects = true` отклоняется, если
|
||||||
|
стадия любого обработанного узла меняется и требуются неисполненные FX/host
|
||||||
|
callbacks. Licensed двухстадийный узел шахты подтверждает переход `0 → 1` через
|
||||||
|
новый API при доле `0.5`, без FX и window path. Гибель root/critical узла и
|
||||||
|
неизвестные flags/aggregate также остаются явными ошибками.
|
||||||
|
|
||||||
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
||||||
текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
|
текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
|
||||||
@@ -364,7 +377,8 @@ Licensed `min_l_n1.dat` подтверждает изменение `0xe00` и B
|
|||||||
`1.0` до `0.5` после явного уменьшения количества вдвое; это ещё не
|
`1.0` до `0.5` после явного уменьшения количества вдвое; это ещё не
|
||||||
автоматический бюджетный цикл.
|
автоматический бюджетный цикл.
|
||||||
Native oracle проверил изолированные пути распределения, обновления класса `4`,
|
Native oracle проверил изолированные пути распределения, обновления класса `4`,
|
||||||
стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` и
|
стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` на
|
||||||
|
interleaved parents `[-1, 0, 0, 1, 3]` с callback-порядком `1, 3, 4, 2` и
|
||||||
visibility transition `0x11920` с исходными AniMesh callbacks. Отдельный
|
visibility transition `0x11920` с исходными AniMesh callbacks. Отдельный
|
||||||
бюджетный вектор исполняет `0x2d340` → `0x2dca0` → tag 15 `0x22bb0` →
|
бюджетный вектор исполняет `0x2d340` → `0x2dca0` → tag 15 `0x22bb0` →
|
||||||
`0x10ba0`: подтверждены коэффициенты `0.5` и `1`, точные amount/ratio и
|
`0x10ba0`: подтверждены коэффициенты `0.5` и `1`, точные amount/ratio и
|
||||||
|
|||||||
@@ -695,8 +695,8 @@ provider остаётся с заданным тестом значением `0
|
|||||||
текущего Control stage/sentinel. Неизвестный узел даёт ошибку без частичной
|
текущего Control stage/sentinel. Неизвестный узел даёт ошибку без частичной
|
||||||
записи; callback отражён в CPU-проекции, но GPU/window path ещё не подключён.
|
записи; callback отражён в CPU-проекции, но GPU/window path ещё не подключён.
|
||||||
Delta API не вызывает стадийный проход. Отдельный single-node API для
|
Delta API не вызывает стадийный проход. Отдельный single-node API для
|
||||||
`0x11220` описан в разделе о стадиях; обход `0x11130`, видимость и FX остаются
|
`0x11220` и отдельный API обхода потомков `0x11130` описаны в разделе о
|
||||||
вне этого исполнения.
|
стадиях; видимость и FX callbacks остаются вне этого исполнения.
|
||||||
Истощение корня или критического узла возвращает `UnsupportedNodeLifecycle`
|
Истощение корня или критического узла возвращает `UnsupportedNodeLifecycle`
|
||||||
до записи: состояние владельца `+0x550`, отложенная гибель и сопутствующие
|
до записи: состояние владельца `+0x550`, отложенная гибель и сопутствующие
|
||||||
callbacks ещё не реализованы. Отсутствие MSH flags, provider aggregate,
|
callbacks ещё не реализованы. Отсутствие MSH flags, provider aggregate,
|
||||||
@@ -964,15 +964,33 @@ Native oracle и Rust-тест также проверяют двухстади
|
|||||||
стадии и отсутствие такого обновления при защищённом раннем выходе. Licensed
|
стадии и отсутствие такого обновления при защищённом раннем выходе. Licensed
|
||||||
тест повторяет отдельный переход `0 → 1` на двухстадийном узле настоящего
|
тест повторяет отдельный переход `0 → 1` на двухстадийном узле настоящего
|
||||||
`min_l_n1.dat`, с `update_effects = false`. Полный игровой вызов не подменяется
|
`min_l_n1.dat`, с `update_effects = false`. Полный игровой вызов не подменяется
|
||||||
этим режимом: FX, lifecycle, видимость и обход потомков ещё требуют реализации.
|
этим режимом: FX, lifecycle и видимость всё ещё требуют реализации; обход
|
||||||
|
потомков доступен отдельным CPU API и пока не подключён к игровому циклу.
|
||||||
|
|
||||||
После перехода родительского ряда `FUN_10011130` находит более поздние строки
|
`FUN_10011130` сохраняет старую стадию, вызывает `0x11220`, затем сохраняет
|
||||||
с его индексом в поле parent. Обход идёт в глубину: сначала ребёнок и всё его
|
обновлённые flags родителя для всего прохода. В исследованной сборке
|
||||||
поддерево, затем следующий ребёнок. Рост стадии или истощение родителя снимает
|
`0x11130` вызывает `0x11220` по RVA `0x11156`; caller-ы `0x11130` находятся в
|
||||||
количество с дочерних строк; защитные флаги `0x40` и `0x100` меняют часть
|
`0x10ba0`, `0x12a40`, `0x12ce0`, `0x13180` и в самой рекурсивной функции.
|
||||||
обработки флагов, но не останавливают снятие количества и обход потомков в
|
`0x10f30` вызывает `0x11220` напрямую по RVA `0x1110f`.
|
||||||
проверенных входах. Нативный тест задал родителя с дочерними рядами, снял их
|
|
||||||
количество и проверил порядок вызовов и значение stage snapshot.
|
Затем `0x11130` сканирует provider-строки с большим индексом и выбирает строки,
|
||||||
|
у которых parent равен текущему ключу. Обход идёт в глубину: сначала ребёнок и
|
||||||
|
всё его поддерево, затем следующий ребёнок. Для parents `[-1, 0, 0, 1, 3]`
|
||||||
|
порядок callbacks равен `1, 3, 4, 2`. Если стадия родителя выросла или у него
|
||||||
|
стоит флаг истощения `0x10`, Control снимает количество с ребёнка. При флагах
|
||||||
|
родителя `0xc0` дочернему ряду ставится `0x80`; в обычной ветви ставится `0x100`
|
||||||
|
при ухудшении и очищается `0x80`. Дочерний mask `0x140` запрещает только
|
||||||
|
перезапись его flags: количество всё равно снимается, и обход продолжается.
|
||||||
|
Стадии детей пересчитываются, даже если родитель не ухудшился.
|
||||||
|
|
||||||
|
Rust API `propagate_loaded_mission_control_node_stage` выполняет этот обход
|
||||||
|
отдельно от single-node `update_loaded_mission_control_node_stage`. Он работает
|
||||||
|
с копией provider-таблицы и публикует её только после успешной обработки всех
|
||||||
|
потомков. Если в проверяемом суффиксе отсутствуют mesh metadata и поэтому нельзя
|
||||||
|
установить parent-связи, либо потомку требуются lifecycle, FX или visibility
|
||||||
|
callbacks, API возвращает ошибку и оставляет исходную таблицу без изменений.
|
||||||
|
Native oracle использует синтетические NDP/Node38-строки и исходные
|
||||||
|
Control/AniMesh callbacks; это не полное исполнение или сравнение миссии.
|
||||||
|
|
||||||
Переход между обычной стадией и sentinel `stage == count` вызывает
|
Переход между обычной стадией и sentinel `stage == count` вызывает
|
||||||
`Control.dll!FUN_10011920`, только если у строки не установлен флаг `0x02`.
|
`Control.dll!FUN_10011920`, только если у строки не установлен флаг `0x02`.
|
||||||
|
|||||||
@@ -997,22 +997,43 @@ clock Control.
|
|||||||
даёт `[14, 28, 10]`. При вычитании последовательный способ начинает с последней
|
даёт `[14, 28, 10]`. При вычитании последовательный способ начинает с последней
|
||||||
строки. Так одна таблица описывает и текущее значение, и то, как оно меняется.
|
строки. Так одна таблица описывает и текущее значение, и то, как оно меняется.
|
||||||
|
|
||||||
У строк есть и связи с родительскими узлами. Когда стадия родителя меняется,
|
У строк есть и связи с родительскими узлами. Нативный `Control.dll` сначала
|
||||||
Control пересчитывает дочерние строки в глубину: ребёнка вместе со всем его
|
обновляет запрошенную строку через `0x11220`, затем `0x11130` ищет её детей
|
||||||
поддеревом, затем следующего ребёнка. Для числа вариантов `count` внутренняя
|
среди строк с большим индексом. Каждый найденный ребёнок обновляется вместе со
|
||||||
стадия вычисляется как `count - ceil(f32(count * ratio))`, а видимое значение
|
своим поддеревом, и только потом обход продолжается со следующим соседом.
|
||||||
ограничивается диапазоном модели. Например, при трёх стадиях доли
|
Например, для родителей `[-1, 0, 0, 1, 3]` порядок строк становится
|
||||||
|
`1 → 3 → 4 → 2`: сначала весь первый участок дерева, затем его сосед.
|
||||||
|
|
||||||
|
Стадия увеличивается, когда доля ресурса уменьшается. Для числа вариантов
|
||||||
|
`count` внутренняя стадия вычисляется как `count - ceil(f32(count * ratio))`,
|
||||||
|
а видимое значение ограничивается диапазоном модели. Например, при трёх стадиях доли
|
||||||
`[0.1, 0.4, 0.5, 1, 0]` дали `[2, 1, 1, 0, 3]`; последнее значение — sentinel
|
`[0.1, 0.4, 0.5, 1, 0]` дали `[2, 1, 1, 0, 3]`; последнее значение — sentinel
|
||||||
истощения, а в модель передаётся индекс не выше `count - 1`. Нативные проверки
|
истощения, а в модель передаётся индекс не выше `count - 1`. Нативные проверки
|
||||||
подтвердили порядок обхода и передачу состояния в AniMesh.
|
подтвердили арифметику и порядок обхода.
|
||||||
|
|
||||||
|
После обновления родителя Control сравнивает его старую стадию с новой и
|
||||||
|
проверяет флаг истощения `0x10`. Если стадия выросла либо строка истощена,
|
||||||
|
количество каждого ребёнка полностью уменьшается тем же путём, что и обычная
|
||||||
|
добавка ресурса. Для ребёнка без защитной маски родительские флаги `0xc0`
|
||||||
|
задают флаг `0x80`; в обычной ветви переход ставит `0x100` при ухудшении и
|
||||||
|
очищает у ребёнка `0x80`. Маска `0x140` запрещает только изменение этих флагов:
|
||||||
|
уменьшение количества и рекурсивный вызов всё равно выполняются. Дочерние
|
||||||
|
стадии пересчитываются и тогда, когда родитель не ухудшился.
|
||||||
|
|
||||||
Выбранная стадия также задаёт, какие данные MSH попадают в пересчёт видимости.
|
Выбранная стадия также задаёт, какие данные MSH попадают в пересчёт видимости.
|
||||||
У одного синтетического узла первый слот давал `[42, 330]`, а слот другой стадии —
|
У одного синтетического узла первый слот давал `[42, 330]`, а слот другой стадии —
|
||||||
`[102, 570]`. В проверенных векторах одинаковые данные при скрытии и повторном
|
`[102, 570]`. В проверенных векторах одинаковые данные при скрытии и повторном
|
||||||
включении восстановили суммы; переходы с разными слотами — нет. Эти проверки
|
включении восстановили суммы; переходы с разными слотами — нет. Эти проверки
|
||||||
исполняют исходные DLL на тестовых строках, но не являются запуском Control на
|
исполняют исходные DLL на тестовых строках, но не являются запуском Control на
|
||||||
полной миссии. Rust выполняет отдельный стадийный шаг в подтверждённой области,
|
полной миссии.
|
||||||
но обход потомков, пересчёт видимости и остальные callbacks ещё не подключены.
|
|
||||||
|
В Rust `update_loaded_mission_control_node_stage` сохраняет отдельный шаг одной
|
||||||
|
строки, а `propagate_loaded_mission_control_node_stage` обновляет строку и её
|
||||||
|
поддерево в том же порядке, что native `0x11130`. Весь снимок provider-таблицы
|
||||||
|
публикуется только после успешной обработки всех потомков. Если позже по обходу
|
||||||
|
понадобится неизвестная связь узлов, lifecycle, FX или пересчёт видимости,
|
||||||
|
операция сообщает об этом и оставляет снимок без изменений. Эта CPU-проекция
|
||||||
|
ещё не подключена к игровому Control clock или выводу модели в окно.
|
||||||
Прямой путь изменения количества остаётся отдельной операцией.
|
Прямой путь изменения количества остаётся отдельной операцией.
|
||||||
Подробные границы приведены в [техническом приложении](../appendices/script-vm.md#родители-стадии-и-видимость-provider-узлов).
|
Подробные границы приведены в [техническом приложении](../appendices/script-vm.md#родители-стадии-и-видимость-provider-узлов).
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user