feat(runtime): update Control node stages
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
+497
-18
No files matched your search
@@ -263,6 +263,8 @@ pub struct MissionControlNodeProviderRow {
|
|||||||
pub current_type1_value_bits: u32,
|
pub current_type1_value_bits: u32,
|
||||||
/// Current native `+0x28` flags, when the initial mesh metadata is known.
|
/// Current native `+0x28` flags, when the initial mesh metadata is known.
|
||||||
pub current_flags: Option<u32>,
|
pub current_flags: Option<u32>,
|
||||||
|
/// Native Control stage at `+0x18`; `stage_count` is the exhaustion sentinel.
|
||||||
|
pub current_stage: u32,
|
||||||
/// MSH state exposed at the same global ordinal, if the mesh projection exists.
|
/// MSH state exposed at the same global ordinal, if the mesh projection exists.
|
||||||
pub initial_mesh_state: Option<MissionControlNodeInitialMeshState>,
|
pub initial_mesh_state: Option<MissionControlNodeInitialMeshState>,
|
||||||
/// Current model-node fields, when its global ordinal was resolved.
|
/// Current model-node fields, when its global ordinal was resolved.
|
||||||
@@ -341,6 +343,18 @@ pub enum ControlResourceAccessError {
|
|||||||
InvalidNodeProviderAmount { key: u32 },
|
InvalidNodeProviderAmount { key: u32 },
|
||||||
/// Depleting a root or critical node needs the unimplemented object lifecycle.
|
/// Depleting a root or critical node needs the unimplemented object lifecycle.
|
||||||
UnsupportedNodeLifecycle { key: u32 },
|
UnsupportedNodeLifecycle { key: u32 },
|
||||||
|
/// A provider row references a parent outside the loaded table.
|
||||||
|
NodeProviderParentOutOfRange {
|
||||||
|
key: u32,
|
||||||
|
parent_index: i32,
|
||||||
|
row_count: usize,
|
||||||
|
},
|
||||||
|
/// The stage count or ratio is outside the confirmed stage domain.
|
||||||
|
InvalidNodeStageState { key: u32 },
|
||||||
|
/// A stage transition needs the unimplemented geometry/visibility update.
|
||||||
|
UnsupportedNodeVisibility { key: u32 },
|
||||||
|
/// A stage transition requests unimplemented FX and host notifications.
|
||||||
|
UnsupportedNodeEffects { key: u32 },
|
||||||
/// A native tag has an e00 callback that has not been implemented here.
|
/// A native tag has an e00 callback that has not been implemented here.
|
||||||
UnsupportedCurrentTag { tag: u32 },
|
UnsupportedCurrentTag { tag: u32 },
|
||||||
/// A selected row lacks a value needed by the native getter or setter.
|
/// A selected row lacks a value needed by the native getter or setter.
|
||||||
@@ -2196,6 +2210,154 @@ pub fn apply_loaded_mission_control_node_delta(
|
|||||||
apply_control_node_delta(table, key, delta)
|
apply_control_node_delta(table, key, delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the confirmed single-node stage path of native Control `0x11220`.
|
||||||
|
///
|
||||||
|
/// Copies parent ratio/stage when requested, updates the processed amount and
|
||||||
|
/// CPU visible stage, and returns whether the Control stage changed. It does not
|
||||||
|
/// traverse descendants (`0x11130`) or run the separate Control clock.
|
||||||
|
/// `update_effects` is the original native argument, not a compatibility fallback.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Rejects unavailable state, invalid parent/count/ratio, and transitions needing
|
||||||
|
/// unsupported lifecycle, visibility, FX, or host callbacks. No partial state is
|
||||||
|
/// published on error. Depleted protected nodes retain the native early return.
|
||||||
|
pub fn update_loaded_mission_control_node_stage(
|
||||||
|
engine: &mut Engine,
|
||||||
|
handle: ObjectHandle,
|
||||||
|
key: u32,
|
||||||
|
update_effects: bool,
|
||||||
|
) -> Result<bool, 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 })?;
|
||||||
|
update_control_node_stage(table, key, update_effects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_control_node_stage(
|
||||||
|
table: &mut MissionControlDeviceTableState,
|
||||||
|
key: u32,
|
||||||
|
update_effects: bool,
|
||||||
|
) -> Result<bool, ControlResourceAccessError> {
|
||||||
|
let rows = match &mut table.node_provider_status {
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows) => rows,
|
||||||
|
MissionControlNodeProviderTableStatus::Unavailable(reason) => {
|
||||||
|
return Err(ControlResourceAccessError::NodeProviderTableUnavailable(
|
||||||
|
reason.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let row_count = rows.len();
|
||||||
|
let index = usize::try_from(key)
|
||||||
|
.ok()
|
||||||
|
.filter(|index| *index < row_count)
|
||||||
|
.ok_or(ControlResourceAccessError::NodeProviderKeyOutOfRange { key, row_count })?;
|
||||||
|
let row = &rows[index];
|
||||||
|
let flags = row
|
||||||
|
.current_flags
|
||||||
|
.ok_or(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider flags",
|
||||||
|
})?;
|
||||||
|
if flags & 0x1c0 != 0 {
|
||||||
|
if flags & 0x10 == 0 {
|
||||||
|
return Err(ControlResourceAccessError::UnsupportedNodeLifecycle { key });
|
||||||
|
}
|
||||||
|
if flags & 0xc0 != 0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let metadata = row
|
||||||
|
.initial_mesh_state
|
||||||
|
.ok_or(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "provider mesh metadata",
|
||||||
|
})?;
|
||||||
|
let count = metadata.stage_count;
|
||||||
|
if count == 0 {
|
||||||
|
return Err(ControlResourceAccessError::InvalidNodeStageState { key });
|
||||||
|
}
|
||||||
|
let parent = if flags & 0x0c != 0 {
|
||||||
|
Some(
|
||||||
|
usize::try_from(metadata.parent_index)
|
||||||
|
.ok()
|
||||||
|
.and_then(|parent| rows.get(parent))
|
||||||
|
.ok_or(ControlResourceAccessError::NodeProviderParentOutOfRange {
|
||||||
|
key,
|
||||||
|
parent_index: metadata.parent_index,
|
||||||
|
row_count,
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let ratio_bits = if flags & 4 != 0 {
|
||||||
|
parent
|
||||||
|
.expect("parent validated for inheritance")
|
||||||
|
.current_type1_value_bits
|
||||||
|
} else {
|
||||||
|
row.current_type1_value_bits
|
||||||
|
};
|
||||||
|
let ratio = checked_control_float(0, "provider stage ratio", f32::from_bits(ratio_bits))?;
|
||||||
|
if !(0.0..=1.0).contains(&ratio) {
|
||||||
|
return Err(ControlResourceAccessError::InvalidNodeStageState { key });
|
||||||
|
}
|
||||||
|
let stage = if flags & 8 != 0 {
|
||||||
|
parent
|
||||||
|
.expect("parent validated for inheritance")
|
||||||
|
.current_stage
|
||||||
|
.min(count)
|
||||||
|
} else {
|
||||||
|
let product = control_stage_product(count, ratio);
|
||||||
|
count.wrapping_sub(product.ceil() as u64 as u32).min(count)
|
||||||
|
};
|
||||||
|
let changed = stage != row.current_stage;
|
||||||
|
if changed {
|
||||||
|
if update_effects {
|
||||||
|
return Err(ControlResourceAccessError::UnsupportedNodeEffects { key });
|
||||||
|
}
|
||||||
|
if flags & 2 == 0 && (stage == count || row.current_stage == count) {
|
||||||
|
return Err(ControlResourceAccessError::UnsupportedNodeVisibility { key });
|
||||||
|
}
|
||||||
|
if row.mesh_state.is_none() {
|
||||||
|
return Err(ControlResourceAccessError::UnsupportedField {
|
||||||
|
factory_tag: 0,
|
||||||
|
field: "model node state",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let row = &mut rows[index];
|
||||||
|
row.current_type1_value_bits = ratio_bits;
|
||||||
|
row.processed_amount_bits = row.current_amount_bits;
|
||||||
|
if changed {
|
||||||
|
row.current_flags = Some(flags | 0x200);
|
||||||
|
row.current_stage = stage;
|
||||||
|
row.mesh_state
|
||||||
|
.as_mut()
|
||||||
|
.expect("model state validated")
|
||||||
|
.visible_stage = stage.min(count - 1);
|
||||||
|
}
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_stage_product(count: u32, ratio: f32) -> f32 {
|
||||||
|
if ratio == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
// FILD keeps the integer exact; round the mantissa product once, then rescale exactly.
|
||||||
|
let bits = ratio.to_bits();
|
||||||
|
let significand = (bits & 0x007f_ffff) | 0x0080_0000;
|
||||||
|
let exponent = ((bits >> 23) & 0xff) as i32 - 150;
|
||||||
|
let product = u64::from(count) * u64::from(significand);
|
||||||
|
(f64::from(product as f32) * 2.0_f64.powi(exponent)) as f32
|
||||||
|
}
|
||||||
|
|
||||||
fn initial_node_provider_amount(status: &MissionControlNodeProviderTableStatus) -> Option<u32> {
|
fn initial_node_provider_amount(status: &MissionControlNodeProviderTableStatus) -> Option<u32> {
|
||||||
let MissionControlNodeProviderTableStatus::Available(rows) = status else {
|
let MissionControlNodeProviderTableStatus::Available(rows) = status else {
|
||||||
return None;
|
return None;
|
||||||
@@ -3773,6 +3935,7 @@ fn append_control_node_provider_rows(
|
|||||||
processed_amount_bits: source_row.max_volume.to_bits(),
|
processed_amount_bits: source_row.max_volume.to_bits(),
|
||||||
current_type1_value_bits: 0x3f80_0000,
|
current_type1_value_bits: 0x3f80_0000,
|
||||||
current_flags: None,
|
current_flags: None,
|
||||||
|
current_stage: 0,
|
||||||
initial_mesh_state: None,
|
initial_mesh_state: None,
|
||||||
mesh_state: None,
|
mesh_state: None,
|
||||||
});
|
});
|
||||||
@@ -4344,6 +4507,7 @@ mod tests {
|
|||||||
processed_amount_bits: 100.0_f32.to_bits(),
|
processed_amount_bits: 100.0_f32.to_bits(),
|
||||||
current_type1_value_bits: initial_type1_value_bits,
|
current_type1_value_bits: initial_type1_value_bits,
|
||||||
current_flags: None,
|
current_flags: None,
|
||||||
|
current_stage: 0,
|
||||||
initial_mesh_state: None,
|
initial_mesh_state: None,
|
||||||
mesh_state: None,
|
mesh_state: None,
|
||||||
}
|
}
|
||||||
@@ -5110,6 +5274,11 @@ mod tests {
|
|||||||
Err(ControlResourceAccessError::MissionUnavailable)
|
Err(ControlResourceAccessError::MissionUnavailable)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
update_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),
|
||||||
slot: handle.slot,
|
slot: handle.slot,
|
||||||
@@ -5146,7 +5315,21 @@ mod tests {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
update_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!(
|
||||||
|
update_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),
|
||||||
Err(ControlResourceAccessError::World(
|
Err(ControlResourceAccessError::World(
|
||||||
@@ -5348,6 +5531,171 @@ mod tests {
|
|||||||
assert_eq!(table.node_provider_amount_bits, total);
|
assert_eq!(table.node_provider_amount_bits, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_node_stage_matches_native_inheritance_and_visible_sentinel() {
|
||||||
|
let descriptors = [
|
||||||
|
(-1, 0.25_f32, 4, 0x02),
|
||||||
|
(0, 0.875, 5, 0x06),
|
||||||
|
(0, 0.9, 2, 0x0a),
|
||||||
|
(1, 0.1, 6, 0x0e),
|
||||||
|
(-1, 0.1, 3, 0x02),
|
||||||
|
(-1, 0.4, 3, 0x02),
|
||||||
|
(-1, 0.5, 3, 0x02),
|
||||||
|
(-1, 1.0, 3, 0x102),
|
||||||
|
(-1, 0.0, 3, 0x02),
|
||||||
|
(-1, 0.5, 2, 0x02),
|
||||||
|
(-1, 0.75, 16_777_217, 0x02),
|
||||||
|
];
|
||||||
|
let rows = descriptors
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, (parent_index, ratio, stage_count, flags))| {
|
||||||
|
let mut row = synthetic_node_provider_row(0, index, ratio.to_bits());
|
||||||
|
row.current_amount_bits = (index as f32 + 1.0).to_bits();
|
||||||
|
row.processed_amount_bits = (90.0 + index as f32).to_bits();
|
||||||
|
row.current_flags = Some(flags);
|
||||||
|
row.current_stage = u32::from(index == 7);
|
||||||
|
row.initial_mesh_state = Some(MissionControlNodeInitialMeshState {
|
||||||
|
parent_index,
|
||||||
|
flags,
|
||||||
|
stage_count,
|
||||||
|
});
|
||||||
|
row.mesh_state = Some(MissionControlNodeMeshState {
|
||||||
|
type1_value_bits: 0,
|
||||||
|
visible_stage: (-99_i32) as u32,
|
||||||
|
});
|
||||||
|
row
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut table = synthetic_control_device_table_with_providers(
|
||||||
|
MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||||
|
MissionControlNodeProviderTableStatus::Available(rows),
|
||||||
|
);
|
||||||
|
for key in [0_u32, 1, 2, 3, 4, 5, 6, 8, 9, 10] {
|
||||||
|
assert!(update_control_node_stage(&mut table, key, false).unwrap());
|
||||||
|
}
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
for (index, expected) in [3, 3, 2, 3, 2, 1, 1, 1, 3, 1, 4_194_304]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
assert_eq!(rows[index].current_stage, expected);
|
||||||
|
if index == 7 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
rows[index].processed_amount_bits,
|
||||||
|
rows[index].current_amount_bits
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rows[index].mesh_state.unwrap().visible_stage,
|
||||||
|
expected.min(descriptors[index].2 - 1)
|
||||||
|
);
|
||||||
|
assert_eq!(rows[index].mesh_state.unwrap().type1_value_bits, 0);
|
||||||
|
assert_ne!(rows[index].current_flags.unwrap() & 0x200, 0);
|
||||||
|
}
|
||||||
|
assert_eq!(rows[1].current_type1_value_bits, 0.25_f32.to_bits());
|
||||||
|
assert_eq!(rows[3].current_type1_value_bits, 0.25_f32.to_bits());
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
update_control_node_stage(&mut table, 7, false),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedNodeLifecycle { key: 7 })
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[6].processed_amount_bits = 999.0_f32.to_bits();
|
||||||
|
rows[6].mesh_state.as_mut().unwrap().visible_stage = 77;
|
||||||
|
assert!(!update_control_node_stage(&mut table, 6, true).unwrap());
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(rows[6].processed_amount_bits, rows[6].current_amount_bits);
|
||||||
|
assert_eq!(rows[6].mesh_state.unwrap().visible_stage, 77);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[8].current_stage = 0;
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
update_control_node_stage(&mut table, 8, true),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedNodeEffects { key: 8 })
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[8].current_flags = Some(0);
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
update_control_node_stage(&mut table, 8, false),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedNodeVisibility { key: 8 })
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().parent_index = -1;
|
||||||
|
let before = table.clone();
|
||||||
|
assert_eq!(
|
||||||
|
update_control_node_stage(&mut table, 1, false),
|
||||||
|
Err(ControlResourceAccessError::NodeProviderParentOutOfRange {
|
||||||
|
key: 1,
|
||||||
|
parent_index: -1,
|
||||||
|
row_count: 11
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(table, before);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[1].initial_mesh_state.as_mut().unwrap().parent_index = 0;
|
||||||
|
rows[1].current_stage = 0;
|
||||||
|
rows[1].current_type1_value_bits = 0.875_f32.to_bits();
|
||||||
|
rows[1].mesh_state = None;
|
||||||
|
let before = table.clone();
|
||||||
|
assert!(matches!(
|
||||||
|
update_control_node_stage(&mut table, 1, false),
|
||||||
|
Err(ControlResourceAccessError::UnsupportedField {
|
||||||
|
field: "model node state",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
table, before,
|
||||||
|
"failed inherited-stage update must not copy the parent ratio"
|
||||||
|
);
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&mut table.node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
rows[6].current_flags = Some(0x52);
|
||||||
|
rows[6].initial_mesh_state = None;
|
||||||
|
rows[6].processed_amount_bits = 999.0_f32.to_bits();
|
||||||
|
let before = table.clone();
|
||||||
|
assert!(!update_control_node_stage(&mut table, 6, false).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
table, before,
|
||||||
|
"depleted protected node returns before reading metadata or processing amount"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[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");
|
||||||
@@ -6858,6 +7206,48 @@ mod tests {
|
|||||||
visible_stage: 0,
|
visible_stage: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
let stage_key = 1;
|
||||||
|
let stage_row = &mine_rows[stage_key as usize];
|
||||||
|
assert_eq!(stage_row.initial_mesh_state.unwrap().stage_count, 2);
|
||||||
|
assert_eq!(stage_row.current_flags, Some(2));
|
||||||
|
let stage_initial = stage_row.initial_amount_bits;
|
||||||
|
let _publication = apply_loaded_mission_control_node_delta(
|
||||||
|
&mut mine_engine,
|
||||||
|
mine_profile.handle,
|
||||||
|
stage_key,
|
||||||
|
-f32::from_bits(stage_initial) * 0.5,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(update_loaded_mission_control_node_stage(
|
||||||
|
&mut mine_engine,
|
||||||
|
mine_profile.handle,
|
||||||
|
stage_key,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
let MissionControlNodeProviderTableStatus::Available(rows) =
|
||||||
|
&loaded_mission_control_device_tables(&mine_engine).unwrap()[0].node_provider_status
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(rows[stage_key as usize].current_stage, 1);
|
||||||
|
assert_eq!(
|
||||||
|
rows[stage_key as usize].mesh_state.unwrap().visible_stage,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rows[stage_key as usize]
|
||||||
|
.mesh_state
|
||||||
|
.unwrap()
|
||||||
|
.type1_value_bits,
|
||||||
|
0.5_f32.to_bits()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rows[stage_key as usize].processed_amount_bits,
|
||||||
|
rows[stage_key as usize].current_amount_bits
|
||||||
|
);
|
||||||
|
assert_eq!(rows[stage_key as usize].initial_amount_bits, stage_initial);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(aniMeshModule, 0x05500),
|
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(aniMeshModule, 0x05500),
|
||||||
typeof(NativeAniMeshNodeLifecycleCallback));
|
typeof(NativeAniMeshNodeLifecycleCallback));
|
||||||
using (Fixture fixture = new Fixture()) {
|
using (Fixture fixture = new Fixture()) {
|
||||||
const int rowCount = 9;
|
const int rowCount = 11;
|
||||||
IntPtr owner = fixture.Allocate(0x664);
|
IntPtr owner = fixture.Allocate(0x664);
|
||||||
IntPtr rows = fixture.Allocate(rowCount * 0x2c);
|
IntPtr rows = fixture.Allocate(rowCount * 0x2c);
|
||||||
IntPtr aniInterface = fixture.Allocate(0x1b0);
|
IntPtr aniInterface = fixture.Allocate(0x1b0);
|
||||||
@@ -1043,6 +1043,8 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
WriteProviderStageRow(rows, 6, -1, 4.0f, 94.0f, 0.5f, 0, 3, 0x02);
|
WriteProviderStageRow(rows, 6, -1, 4.0f, 94.0f, 0.5f, 0, 3, 0x02);
|
||||||
WriteProviderStageRow(rows, 7, -1, 5.0f, 95.0f, 1.0f, 1, 3, 0x102);
|
WriteProviderStageRow(rows, 7, -1, 5.0f, 95.0f, 1.0f, 1, 3, 0x102);
|
||||||
WriteProviderStageRow(rows, 8, -1, 6.0f, 96.0f, 0.0f, 0, 3, 0x02);
|
WriteProviderStageRow(rows, 8, -1, 6.0f, 96.0f, 0.0f, 0, 3, 0x02);
|
||||||
|
WriteProviderStageRow(rows, 9, -1, 10.0f, 90.0f, 0.5f, 0, 2, 0x02);
|
||||||
|
WriteProviderStageRow(rows, 10, -1, 11.0f, 90.0f, 0.75f, 0, 16777217, 0x02);
|
||||||
|
|
||||||
aniMeshStageTransitions.Clear();
|
aniMeshStageTransitions.Clear();
|
||||||
aniMeshNodeLifecycleCallbacks.Clear();
|
aniMeshNodeLifecycleCallbacks.Clear();
|
||||||
@@ -1057,10 +1059,10 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
updateStage(owner, 3, 0);
|
updateStage(owner, 3, 0);
|
||||||
for (int index = 4; index < rowCount; index++) updateStage(owner, index, 0);
|
for (int index = 4; index < rowCount; index++) updateStage(owner, index, 0);
|
||||||
|
|
||||||
uint[] expectedStages = { 3, 3, 2, 3, 2, 1, 1, 0, 3 };
|
uint[] expectedStages = { 3, 3, 2, 3, 2, 1, 1, 0, 3, 1, 4194304 };
|
||||||
uint[] expectedVisibleStages = { 3, 3, 1, 3, 2, 1, 1, 0, 2 };
|
uint[] expectedVisibleStages = { 3, 3, 1, 3, 2, 1, 1, 0, 2, 1, 4194304 };
|
||||||
uint[] expectedAmounts = { 0x42200000, 0x41400000, 0x40e00000, 0x40400000,
|
uint[] expectedAmounts = { 0x42200000, 0x41400000, 0x40e00000, 0x40400000,
|
||||||
0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000 };
|
0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x41200000, 0x41300000 };
|
||||||
for (int index = 0; index < rowCount; index++) {
|
for (int index = 0; index < rowCount; index++) {
|
||||||
IntPtr row = IntPtr.Add(rows, index * 0x2c);
|
IntPtr row = IntPtr.Add(rows, index * 0x2c);
|
||||||
IntPtr meshRow = IntPtr.Add(meshRows, index * 0x130);
|
IntPtr meshRow = IntPtr.Add(meshRows, index * 0x130);
|
||||||
@@ -1080,17 +1082,33 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
"Control flag 4 copies the parent type-1 ratio before stage calculation");
|
"Control flag 4 copies the parent type-1 ratio before stage calculation");
|
||||||
Expect(ReadFloat(meshRows, 0x130 + 0x124) == 0.0f && ReadFloat(rows, 0x2c + 0x14) == 0.25f,
|
Expect(ReadFloat(meshRows, 0x130 + 0x124) == 0.0f && ReadFloat(rows, 0x2c + 0x14) == 0.25f,
|
||||||
"inheriting a parent provider ratio does not rewrite the separate AniMesh node value");
|
"inheriting a parent provider ratio does not rewrite the separate AniMesh node value");
|
||||||
Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,1:3,2:1,3:3,4:2,5:1,6:1,7:0,8:2" &&
|
Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,1:3,2:1,3:3,4:2,5:1,6:1,7:0,8:2,9:1,10:4194304" &&
|
||||||
String.Join(",", aniMeshNodeLifecycleCallbacks.ToArray()) == "7:101:4" && !aniMeshCallbackOwnerMismatch &&
|
String.Join(",", aniMeshNodeLifecycleCallbacks.ToArray()) == "7:101:4" && !aniMeshCallbackOwnerMismatch &&
|
||||||
!controlNodeUpdateOwnerMismatch && !nativeAniMeshStageCallbackMissing,
|
!controlNodeUpdateOwnerMismatch && !nativeAniMeshStageCallbackMissing,
|
||||||
"Control stage and slot-0x20 callbacks reach the original AniMesh RVA 0x5810 in direct update order");
|
"Control stage and slot-0x20 callbacks reach the original AniMesh RVA 0x5810 in direct update order");
|
||||||
Expect(String.Join(",", controlProviderCallbackTrace.ToArray()) ==
|
Expect(String.Join(",", controlProviderCallbackTrace.ToArray()) ==
|
||||||
"stage:0:3,stage:1:3,stage:2:1,stage:3:3,stage:4:2,stage:5:1,stage:6:1,lifecycle:7:101:4,stage:7:0,stage:8:2",
|
"stage:0:3,stage:1:3,stage:2:1,stage:3:3,stage:4:2,stage:5:1,stage:6:1,lifecycle:7:101:4,stage:7:0,stage:8:2,stage:9:1,stage:10:4194304",
|
||||||
"AniMesh slot 0x20 receives its native node, selector, and mask arguments before stage publication");
|
"AniMesh slot 0x20 receives its native node, selector, and mask arguments before stage publication");
|
||||||
|
|
||||||
|
int transitionsBeforeNoOp = aniMeshStageTransitions.Count;
|
||||||
|
IntPtr unchanged = IntPtr.Add(rows, 6 * 0x2c);
|
||||||
|
Marshal.WriteInt32(unchanged, 0x0c, FloatBits(999.0f));
|
||||||
|
Marshal.WriteInt32(meshRows, 6 * 0x130 + 0x1c, 77);
|
||||||
|
updateStage(owner, 6, 1);
|
||||||
|
Expect(ReadFloat(unchanged, 0x0c) == ReadFloat(unchanged, 8) &&
|
||||||
|
Marshal.ReadInt32(meshRows, 6 * 0x130 + 0x1c) == 77 &&
|
||||||
|
aniMeshStageTransitions.Count == transitionsBeforeNoOp,
|
||||||
|
"unchanged stage refreshes processed amount without stage or effects callbacks");
|
||||||
|
Marshal.WriteInt32(unchanged, 0x28, 0x52);
|
||||||
|
Marshal.WriteInt32(unchanged, 0x0c, FloatBits(999.0f));
|
||||||
|
updateStage(owner, 6, 0);
|
||||||
|
Expect(ReadFloat(unchanged, 0x0c) == 999.0f &&
|
||||||
|
aniMeshStageTransitions.Count == transitionsBeforeNoOp,
|
||||||
|
"depleted protected node returns without processing its amount");
|
||||||
|
|
||||||
Console.WriteLine("native-control-parent-stage-arithmetic: controlHash=" + ExpectedControlSha256 +
|
Console.WriteLine("native-control-parent-stage-arithmetic: controlHash=" + ExpectedControlSha256 +
|
||||||
" rva=0x11220 bit4=ratio bit8=parent-stage ceil=[0.1,0.4,0.5,1,0]=>[2,1,1,0,3]" +
|
" rva=0x11220 bit4=ratio bit8=parent-stage ceil=[0.1,0.4,0.5,1,0]=>[2,1,1,0,3]" +
|
||||||
" callbacks=[0:3,1:3,2:1,3:3,4:2,5:1,6:1,lifecycle7:101:4,7:0,8:2]");
|
" callbacks=[0:3,1:3,2:1,3:3,4:2,5:1,6:1,lifecycle7:101:4,7:0,8:2,9:1,10:4194304]");
|
||||||
activeAniMeshInterface = IntPtr.Zero;
|
activeAniMeshInterface = IntPtr.Zero;
|
||||||
activeNativeAniMeshStageTransition = null;
|
activeNativeAniMeshStageTransition = null;
|
||||||
activeNativeAniMeshNodeLifecycle = null;
|
activeNativeAniMeshNodeLifecycle = null;
|
||||||
|
|||||||
@@ -347,8 +347,14 @@ MSH-часть проверки подтверждает начальный сн
|
|||||||
соответствующего узла модели. Исходный `AniMesh.dll+0x5750` подтверждён в
|
соответствующего узла модели. Исходный `AniMesh.dll+0x5750` подтверждён в
|
||||||
связке с `Control+0x10f30`: меняется только поле узла `+0x124`, а стадия и
|
связке с `Control+0x10f30`: меняется только поле узла `+0x124`, а стадия и
|
||||||
соседние узлы сохраняются. Эта проекция ещё не подключена к GPU/window path
|
соседние узлы сохраняются. Эта проекция ещё не подключена к GPU/window path
|
||||||
и не выполняет пересчёт стадий, видимости, FX или время Control. Поэтому обработанное количество остаётся
|
и сама delta-операция не выполняет пересчёт стадий, видимости, FX или время
|
||||||
прежним. Гибель root/critical узла и неизвестные flags/aggregate дают явную
|
Control. Обработанное количество при ней остаётся прежним. Отдельный
|
||||||
|
`update_loaded_mission_control_node_stage` реализует наследование parent ratio
|
||||||
|
и stage, ceiling-арифметику, CPU visible stage и копию processed amount.
|
||||||
|
Обход потомков `0x11130` пока не перенесён. Нужные lifecycle/visibility/FX
|
||||||
|
callbacks дают явные ошибки до записи; `update_effects = true` поддержан
|
||||||
|
только без фактической смены стадии. Licensed двухстадийный узел шахты
|
||||||
|
подтверждает отдельный переход `0 → 1` при доле `0.5`, без FX и window path. Гибель root/critical узла и неизвестные flags/aggregate дают явную
|
||||||
ошибку до записи; неподдержанные численные диапазоны тоже отклоняются.
|
ошибку до записи; неподдержанные численные диапазоны тоже отклоняются.
|
||||||
|
|
||||||
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
||||||
|
|||||||
@@ -692,9 +692,11 @@ provider остаётся с заданным тестом значением `0
|
|||||||
стадийное наследование доли родителя тоже не переписывает `+0x124` модели.
|
стадийное наследование доли родителя тоже не переписывает `+0x124` модели.
|
||||||
Поэтому Rust хранит `mesh_state.type1_value_bits` отдельно от
|
Поэтому Rust хранит `mesh_state.type1_value_bits` отдельно от
|
||||||
`current_type1_value_bits` provider, а `mesh_state.visible_stage` — отдельно от
|
`current_type1_value_bits` provider, а `mesh_state.visible_stage` — отдельно от
|
||||||
будущего Control stage/sentinel. Неизвестный узел даёт ошибку без частичной
|
текущего Control stage/sentinel. Неизвестный узел даёт ошибку без частичной
|
||||||
записи; callback отражён в CPU-проекции, но GPU/window path ещё не подключён.
|
записи; callback отражён в CPU-проекции, но GPU/window path ещё не подключён.
|
||||||
Полный пересчёт стадии `0x11130`/`0x11220`, видимости и FX также не вызывается.
|
Delta API не вызывает стадийный проход. Отдельный single-node API для
|
||||||
|
`0x11220` описан в разделе о стадиях; обход `0x11130`, видимость и FX остаются
|
||||||
|
вне этого исполнения.
|
||||||
Истощение корня или критического узла возвращает `UnsupportedNodeLifecycle`
|
Истощение корня или критического узла возвращает `UnsupportedNodeLifecycle`
|
||||||
до записи: состояние владельца `+0x550`, отложенная гибель и сопутствующие
|
до записи: состояние владельца `+0x550`, отложенная гибель и сопутствующие
|
||||||
callbacks ещё не реализованы. Отсутствие MSH flags, provider aggregate,
|
callbacks ещё не реализованы. Отсутствие MSH flags, provider aggregate,
|
||||||
@@ -928,6 +930,42 @@ provider-состояния в реальной миссии. Ограничен
|
|||||||
`AniMesh.dll!FUN_10005810` передаётся только видимое значение
|
`AniMesh.dll!FUN_10005810` передаётся только видимое значение
|
||||||
`min(stage, count - 1)`.
|
`min(stage, count - 1)`.
|
||||||
|
|
||||||
|
Rust API `update_loaded_mission_control_node_stage(engine, handle, key,
|
||||||
|
update_effects)` переносит отдельный вызов `0x11220` в пределах подтверждённой
|
||||||
|
арифметики и CPU-модели. Он читает текущие flags, count и parent, копирует долю
|
||||||
|
родителя по `4` и его Control stage по `8`, записывает изменённую видимую стадию
|
||||||
|
по правилу исходного `AniMesh+0x5810`, ставит flag `0x200` при переходе и
|
||||||
|
сохраняет текущее количество в `processed_amount_bits`. Начальные снимки не
|
||||||
|
меняются. Возвращаемый bool означает только смену Control stage: при неизменной
|
||||||
|
стадии обработанное количество всё равно обновляется. Модельный `+0x124` при
|
||||||
|
наследовании доли не переписывается.
|
||||||
|
|
||||||
|
Flags `0x1c0` вместе с `0x10` и `0xc0` дают нативный ранний выход до чтения
|
||||||
|
mesh metadata и обновления `+0x0c`. Когда эти же flags требуют lifecycle
|
||||||
|
callback, API возвращает `UnsupportedNodeLifecycle`. Переход при
|
||||||
|
`update_effects = true` требует не только создания FX, но и host notifications
|
||||||
|
через `0x11af0`; пока он возвращает `UnsupportedNodeEffects`. Если стадия не
|
||||||
|
меняется, нативный код пропускает эту ветвь и такой вызов поддержан.
|
||||||
|
Переход через sentinel при снятом flag `2` требует `0x11920` и возвращает
|
||||||
|
`UnsupportedNodeVisibility`. Неверные parent/count, неизвестное состояние
|
||||||
|
модели и неподдержанные доли также отклоняются. Все эти ошибки сохраняют
|
||||||
|
прежнее состояние, включая ещё не скопированную долю родителя.
|
||||||
|
|
||||||
|
Здесь поддержаны конечные нормальные доли `0..1` и ноль. У инструкции `FILD`
|
||||||
|
целый count остаётся точным до `FMUL`; предварительный cast count в `f32`
|
||||||
|
может изменить ответ. Native вектор `count = 16777217`, `ratio = 0.75` получает
|
||||||
|
Control/visible stage `4194304`; ранний cast давал бы `4194305`. Rust умножает
|
||||||
|
целый count на 24-битную мантиссу доли в `u64`, округляет произведение один раз
|
||||||
|
и точно масштабирует его степенью двойки. Это проверка арифметики с
|
||||||
|
искусственным большим count, а не число стадий в игровых ресурсах.
|
||||||
|
|
||||||
|
Native oracle и Rust-тест также проверяют двухстадийный случай `ratio = 0.5`
|
||||||
|
со стадией `1`, обновление только обработанного количества при неизменной
|
||||||
|
стадии и отсутствие такого обновления при защищённом раннем выходе. Licensed
|
||||||
|
тест повторяет отдельный переход `0 → 1` на двухстадийном узле настоящего
|
||||||
|
`min_l_n1.dat`, с `update_effects = false`. Полный игровой вызов не подменяется
|
||||||
|
этим режимом: FX, lifecycle, видимость и обход потомков ещё требуют реализации.
|
||||||
|
|
||||||
После перехода родительского ряда `FUN_10011130` находит более поздние строки
|
После перехода родительского ряда `FUN_10011130` находит более поздние строки
|
||||||
с его индексом в поле parent. Обход идёт в глубину: сначала ребёнок и всё его
|
с его индексом в поле parent. Обход идёт в глубину: сначала ребёнок и всё его
|
||||||
поддерево, затем следующий ребёнок. Рост стадии или истощение родителя снимает
|
поддерево, затем следующий ребёнок. Рост стадии или истощение родителя снимает
|
||||||
@@ -1061,7 +1099,8 @@ SHA-256 `65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`,
|
|||||||
RVA `0x68a0`) читает три DLL scale globals, которые после LoadLibrary в oracle
|
RVA `0x68a0`) читает три DLL scale globals, которые после LoadLibrary в oracle
|
||||||
равны `1.0`. В Rust `initial_amount_bits` сохраняет это общее начальное
|
равны `1.0`. В Rust `initial_amount_bits` сохраняет это общее начальное
|
||||||
количество отдельно от текущего и последнего обработанного. Прямой delta API
|
количество отдельно от текущего и последнего обработанного. Прямой delta API
|
||||||
меняет только текущее количество; стадийный update пока не перенесён.
|
меняет только текущее количество; отдельный стадийный API обновляет
|
||||||
|
обработанное и текущую стадию в подтверждённой области.
|
||||||
|
|
||||||
Licensed full-load regression `full_load_builds_effective_control_device_table_for_mine_unit_dat`
|
Licensed full-load regression `full_load_builds_effective_control_device_table_for_mine_unit_dat`
|
||||||
проверяет значения настоящего `min_l_n1.dat`: корневой provider row `0` даёт
|
проверяет значения настоящего `min_l_n1.dat`: корневой provider row `0` даёт
|
||||||
@@ -1070,7 +1109,9 @@ Licensed full-load regression `full_load_builds_effective_control_device_table_f
|
|||||||
строки tag-26 устройства вдвое. Rust `0xe00` и `0x1002` дают `0.5`, и обычный
|
строки tag-26 устройства вдвое. Rust `0xe00` и `0x1002` дают `0.5`, и обычный
|
||||||
шаг runtime сохраняет эту долю. Это вызов прямого API, а не полный живой
|
шаг runtime сохраняет эту долю. Это вызов прямого API, а не полный живой
|
||||||
Control tick. Проекция поля модели `+0x124` также получает `0.5`, а её видимая
|
Control tick. Проекция поля модели `+0x124` также получает `0.5`, а её видимая
|
||||||
стадия остаётся `0`; GPU/window path этим тестом не исполняется.
|
стадия этого ресурсного узла остаётся `0`. Затем другой, двухстадийный узел
|
||||||
|
получает долю `0.5` и отдельный stage API переводит его из `0` в `1`.
|
||||||
|
GPU/window path этим тестом не исполняется.
|
||||||
|
|
||||||
Native-часть этих ABI проверок воспроизводит
|
Native-часть этих ABI проверок воспроизводит
|
||||||
`behavior_distributor_tick_native_oracle.cs` на синтетически выделенной памяти
|
`behavior_distributor_tick_native_oracle.cs` на синтетически выделенной памяти
|
||||||
|
|||||||
@@ -362,8 +362,10 @@ CollObject, Control и PhysicalModel, пропуская отсутствующ
|
|||||||
[техническом разборе](../appendices/script-vm.md#доставка-игровых-часов-в-control).
|
[техническом разборе](../appendices/script-vm.md#доставка-игровых-часов-в-control).
|
||||||
Статическая цепочка ещё не доказывает исполнение активного Control update на
|
Статическая цепочка ещё не доказывает исполнение активного Control update на
|
||||||
миссии. Rust сохраняет исходные и текущие provider-поля отдельно. Прямой delta
|
миссии. Rust сохраняет исходные и текущие provider-поля отдельно. Прямой delta
|
||||||
API меняет строку и значение соответствующего CPU-узла модели, но описанный
|
API меняет строку и значение соответствующего CPU-узла модели; отдельный
|
||||||
clock/budget-путь и вывод этого состояния в окно пока не подключены.
|
стадийный API обновляет его видимую стадию и обработанное количество в
|
||||||
|
подтверждённой области. Описанный clock/budget-путь и вывод этого состояния
|
||||||
|
в окно пока не подключены.
|
||||||
|
|
||||||
В текущем runtime `step_headless` и `frame` получают время очереди явным
|
В текущем runtime `step_headless` и `frame` получают время очереди явным
|
||||||
аргументом `u32`. По нему запускается представленный ресурсный планировщик
|
аргументом `u32`. По нему запускается представленный ресурсный планировщик
|
||||||
|
|||||||
@@ -915,8 +915,9 @@ initial_mesh_state: Option<MissionControlNodeInitialMeshState>
|
|||||||
|
|
||||||
Внутри снимка хранятся родительский индекс, переведённые флаги Control и число
|
Внутри снимка хранятся родительский индекс, переведённые флаги Control и число
|
||||||
начальных стадий. Узлы корневой модели добавляются все, а присоединённая `EXTO`
|
начальных стадий. Узлы корневой модели добавляются все, а присоединённая `EXTO`
|
||||||
начинает с локального узла `1`. Это описывает исходную настройку: код пока не
|
начинает с локального узла `1`. Это описывает исходную настройку. Отдельный
|
||||||
переключает стадии в ходе игры и не исполняет полный Control tick.
|
API уже пересчитывает стадию одного узла, но полный игровой Control tick
|
||||||
|
пока не исполняется.
|
||||||
|
|
||||||
На той же начальной стадии Control вычисляет количество строки по NDP
|
На той же начальной стадии Control вычисляет количество строки по NDP
|
||||||
`max_volume`. Для загрузки Unit DAT проверенные множители равны `1.0`, поэтому
|
`max_volume`. Для загрузки Unit DAT проверенные множители равны `1.0`, поэтому
|
||||||
@@ -964,6 +965,26 @@ Native oracle соединяет исходные `Control+0x10f30` и `AniMesh+
|
|||||||
с критическим флагом `1` пока возвращает ошибку без изменения состояния:
|
с критическим флагом `1` пока возвращает ошибку без изменения состояния:
|
||||||
эта ветвь требует полного lifecycle объекта, а не только нового количества.
|
эта ветвь требует полного lifecycle объекта, а не только нового количества.
|
||||||
|
|
||||||
|
Стадия пересчитывается отдельным вызовом
|
||||||
|
`update_loaded_mission_control_node_stage`. Для трёх состояний доли `1`, `0.5`
|
||||||
|
и `0.1` выбирают стадии `0`, `1` и `2`. Нулевая доля даёт служебную стадию
|
||||||
|
Control `3`, но в модель записывается последняя видимая стадия `2`. Поэтому
|
||||||
|
`current_stage` и `mesh_state.visible_stage` хранятся отдельно. Флаги `4` и `8`
|
||||||
|
могут брать соответственно долю или стадию родителя; доля самой модели этим
|
||||||
|
пересчётом не переписывается.
|
||||||
|
|
||||||
|
После успешного вызова обработанное количество становится равным текущему,
|
||||||
|
даже если стадия осталась прежней. Истощённые защищённые узлы сохраняют
|
||||||
|
нативный ранний выход и не обновляют этот снимок. Неизвестные parent/count,
|
||||||
|
неподдержанные lifecycle-вызовы, FX и пересчёт геометрии на границе истощения
|
||||||
|
возвращают ошибку до записи. Аргумент `update_effects` сохраняет нативный смысл:
|
||||||
|
его нельзя молча отключать при включении полноценного игрового цикла.
|
||||||
|
|
||||||
|
На настоящем двухстадийном узле `min_l_n1.dat` тест уменьшает количество вдвое,
|
||||||
|
явно вызывает стадийный API без FX и получает Control stage `1`, видимую стадию
|
||||||
|
модели `1` и обновлённое обработанное количество. Это проверка отдельной
|
||||||
|
операции на ресурсах, а не переключение кадра в игровом окне.
|
||||||
|
|
||||||
На исходном `min_l_n1.dat` прямое уменьшение количества строки ресурсного
|
На исходном `min_l_n1.dat` прямое уменьшение количества строки ресурсного
|
||||||
устройства вдвое меняет ответы `0xe00` и Behavior `0x1002` с `1.0` на `0.5`.
|
устройства вдвое меняет ответы `0xe00` и Behavior `0x1002` с `1.0` на `0.5`.
|
||||||
Обычный последующий шаг runtime сохраняет это состояние. Это воспроизводимый
|
Обычный последующий шаг runtime сохраняет это состояние. Это воспроизводимый
|
||||||
@@ -990,7 +1011,8 @@ Control пересчитывает дочерние строки в глубин
|
|||||||
`[102, 570]`. В проверенных векторах одинаковые данные при скрытии и повторном
|
`[102, 570]`. В проверенных векторах одинаковые данные при скрытии и повторном
|
||||||
включении восстановили суммы; переходы с разными слотами — нет. Эти проверки
|
включении восстановили суммы; переходы с разными слотами — нет. Эти проверки
|
||||||
исполняют исходные DLL на тестовых строках, но не являются запуском Control на
|
исполняют исходные DLL на тестовых строках, но не являются запуском Control на
|
||||||
полной миссии; Rust пока не выполняет эти переходы стадий и видимости.
|
полной миссии. Rust выполняет отдельный стадийный шаг в подтверждённой области,
|
||||||
|
но обход потомков, пересчёт видимости и остальные callbacks ещё не подключены.
|
||||||
Прямой путь изменения количества остаётся отдельной операцией.
|
Прямой путь изменения количества остаётся отдельной операцией.
|
||||||
Подробные границы приведены в [техническом приложении](../appendices/script-vm.md#родители-стадии-и-видимость-provider-узлов).
|
Подробные границы приведены в [техническом приложении](../appendices/script-vm.md#родители-стадии-и-видимость-provider-узлов).
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user