feat(runtime): support tag-26 Control resource budgets
Docs Deploy / Build and Deploy MkDocs (push) Successful in 41s

This commit is contained in:
Valentin Popov committed 2026-10-29 20:26:15 +04:00
1 parent 1d20509b05
commit ba7cb7f0f9
5 files changed
+688 -42

No files matched your search

+411 -12
View File
@@ -201,6 +201,8 @@ pub struct EffectiveControlDevice {
pub multiplier_bits: Option<u32>, pub multiplier_bits: Option<u32>,
/// Class-9-only scale used by its selector-0 getter. /// Class-9-only scale used by its selector-0 getter.
pub class9_selector0_scale_bits: Option<u32>, pub class9_selector0_scale_bits: Option<u32>,
/// Factory-26 mutable resource-rate addition at native `+0xa4`.
pub tag26_extra_rate_bits: Option<u32>,
/// Six class-9 budget values at native `+0x98..+0xac`. /// Six class-9 budget values at native `+0x98..+0xac`.
pub class9_values_bits: Option<[u32; 6]>, pub class9_values_bits: Option<[u32; 6]>,
/// Six accumulated class-9 changes at native `+0xb0..+0xc4`. /// Six accumulated class-9 changes at native `+0xb0..+0xc4`.
@@ -1973,8 +1975,9 @@ pub fn query_loaded_mission_control_resource(
/// Queries one loaded Control device through its original factory getter. /// Queries one loaded Control device through its original factory getter.
/// ///
/// Selector `0xe00` is currently modeled for current descriptor tag 26 with /// Selector `0xe00` is currently modeled for current descriptor tag 26 with
/// factory tag 26. The loaded NDP provider value is its initial native state; /// factory tag 26. The query reads the current provider, coefficient, and
/// later Control updates are not synchronized into this snapshot. /// multiplier in the CPU projection. Explicit Control update calls can change
/// those values; the mission clock does not advance them automatically.
/// ///
/// # Errors /// # Errors
/// Returns an error for an invalid handle, unavailable mission/table/provider, /// Returns an error for an invalid handle, unavailable mission/table/provider,
@@ -1999,14 +2002,46 @@ pub fn query_loaded_mission_control_device_selector(
query_control_device_table_selector(table, device_index, selector) query_control_device_table_selector(table, device_index, selector)
} }
/// Sets a supported native Control device property.
///
/// Selector `0x500` writes the factory-26 resource-rate addition at `+0xa4`.
/// A current descriptor tag other than 26 follows the native no-result path
/// and returns `Ok(false)`; successful writes return `Ok(true)`.
///
/// # Errors
/// Returns an error for an invalid handle, unavailable mission/table, an
/// out-of-range device index, an unsupported selector/factory layout, or a
/// non-finite, subnormal, or out-of-range float.
pub fn set_loaded_mission_control_device_selector(
engine: &mut Engine,
handle: ObjectHandle,
device_index: usize,
selector: u32,
value: f32,
) -> 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 })?;
set_control_device_table_selector(table, device_index, selector, value)
}
/// Queries the recovered Behavior resource selectors `0x1002` and `0x1005`. /// Queries the recovered Behavior resource selectors `0x1002` and `0x1005`.
/// ///
/// Selector `0x1005` returns the loaded profile's `Transfer_Power_Out` value. /// Selector `0x1005` returns the loaded profile's `Transfer_Power_Out` value.
/// Selector `0x1002` sums supported Control `0xe00` results in native table /// Selector `0x1002` sums supported Control `0xe00` results in native table
/// order, rounding each addition to `f32`, then multiplies by the profile's /// order, rounding each addition to `f32`, then multiplies by the profile's
/// `Transfer_Ore_OffBoard` value. Unsupported table/query paths return an error. /// `Transfer_Ore_OffBoard` value. Unsupported table/query paths return an error.
/// Profile and provider values are initial full-load snapshots; later damage, /// Profile values come from full mission loading, while `0xe00` reads the
/// Control updates, and profile changes are not synchronized into this state. /// current CPU-projected provider and device state. Explicit Control updates
/// can therefore change `0x1002`; the mission clock does not update it
/// automatically.
/// ///
/// # Errors /// # Errors
/// Returns an error for an invalid handle, unavailable mission/profile/table, /// Returns an error for an invalid handle, unavailable mission/profile/table,
@@ -2282,7 +2317,7 @@ pub fn distribute_loaded_mission_control_provider_amount(
/// Runs the supported loaded Control resource-budget pass (`Control.dll+0x2d340`). /// Runs the supported loaded Control resource-budget pass (`Control.dll+0x2d340`).
/// ///
/// The pass supports the original factory callbacks for tags 9, 15, and 19, /// The pass supports the original factory callbacks for tags 9, 15, 19, and 26,
/// plus the generic default callback pair. Current descriptor category tags are /// plus the generic default callback pair. Current descriptor category tags are
/// supported for the native map prefix `0..=34`. This is an explicit CPU update; /// supported for the native map prefix `0..=34`. This is an explicit CPU update;
/// the mission clock does not call it yet. Rate outputs at `+0x5dc/+0x5e0` are /// the mission clock does not call it yet. Rate outputs at `+0x5dc/+0x5e0` are
@@ -2929,7 +2964,7 @@ fn control_resource_budget_pass_inner(
for index in 0..device_count { for index in 0..device_count {
let device = control_budget_device(table, index)?; let device = control_budget_device(table, index)?;
control_budget_category(device.descriptor.tag)?; control_budget_category(device.descriptor.tag)?;
if !matches!(device.factory_tag, 9 | 15 | 19) if !matches!(device.factory_tag, 9 | 15 | 19 | 26)
&& !control_factory_uses_generic_budget_callbacks(device.factory_tag) && !control_factory_uses_generic_budget_callbacks(device.factory_tag)
{ {
return Err(ControlResourceAccessError::UnsupportedFactoryTag { return Err(ControlResourceAccessError::UnsupportedFactoryTag {
@@ -3289,6 +3324,22 @@ fn control_device_resource_rate(
-coefficient_cost, -coefficient_cost,
) )
} }
26 => {
let extra_rate =
control_device_float(device, device.tag26_extra_rate_bits, "tag-26 extra rate")?;
let rate = checked_control_sum(
device.factory_tag,
"tag-26 descriptor plus extra rate",
descriptor_rate,
extra_rate,
)?;
Ok(-checked_control_product(
device.factory_tag,
"tag-26 resource rate times interval",
rate,
delta_time_seconds,
)?)
}
19 => { 19 => {
let coefficient_0 = control_device_budget_coefficient(device, 0)?; let coefficient_0 = control_device_budget_coefficient(device, 0)?;
let multiplier = control_device_float(device, device.multiplier_bits, "multiplier")?; let multiplier = control_device_float(device, device.multiplier_bits, "multiplier")?;
@@ -3663,6 +3714,7 @@ fn control_device_resource_update(
)?; )?;
} }
} }
26 => set_control_device_multiplier(table, device_index, allocation)?,
factory_tag if control_factory_uses_generic_budget_callbacks(factory_tag) => { factory_tag if control_factory_uses_generic_budget_callbacks(factory_tag) => {
set_control_device_multiplier(table, device_index, allocation)?; set_control_device_multiplier(table, device_index, allocation)?;
} }
@@ -4066,6 +4118,50 @@ fn query_control_device_table_selector(
.map(Some) .map(Some)
} }
fn set_control_device_table_selector(
table: &mut MissionControlDeviceTableState,
device_index: usize,
selector: u32,
value: f32,
) -> Result<bool, ControlResourceAccessError> {
if selector != 0x500 {
return Err(ControlResourceAccessError::UnsupportedSelector(selector));
}
let devices = match &table.status {
MissionControlDeviceTableStatus::Available(devices) => devices,
MissionControlDeviceTableStatus::Unavailable(reason) => {
return Err(ControlResourceAccessError::TableUnavailable(reason.clone()))
}
};
let device =
devices
.get(device_index)
.ok_or(ControlResourceAccessError::DeviceIndexOutOfRange {
device_index,
device_count: devices.len(),
})?;
if device.descriptor.tag != 26 {
return Ok(false);
}
if device.factory_tag != 26 {
return Err(ControlResourceAccessError::UnsupportedFactoryTag {
factory_tag: device.factory_tag,
});
}
if device.tag26_extra_rate_bits.is_none() {
return Err(ControlResourceAccessError::UnsupportedField {
factory_tag: device.factory_tag,
field: "tag-26 extra rate +0xa4",
});
}
let value = checked_control_float(device.factory_tag, "tag-26 extra rate", value)?;
let MissionControlDeviceTableStatus::Available(devices) = &mut table.status else {
unreachable!("table status validated before mutation");
};
devices[device_index].tag26_extra_rate_bits = Some(value.to_bits());
Ok(true)
}
fn query_behavior_profile_ore_rate( fn query_behavior_profile_ore_rate(
table: &MissionControlDeviceTableState, table: &MissionControlDeviceTableState,
transfer_ore_off_board_bits: u32, transfer_ore_off_board_bits: u32,
@@ -4946,6 +5042,7 @@ fn append_effective_control_device(
coefficient_bits, coefficient_bits,
multiplier_bits, multiplier_bits,
class9_selector0_scale_bits: (descriptor.tag == 9).then_some(0x3f80_0000), class9_selector0_scale_bits: (descriptor.tag == 9).then_some(0x3f80_0000),
tag26_extra_rate_bits: (descriptor.tag == 26).then_some(0),
class9_values_bits: (descriptor.tag == 9).then_some([0x3f80_0000; 6]), class9_values_bits: (descriptor.tag == 9).then_some([0x3f80_0000; 6]),
class9_deltas_bits: (descriptor.tag == 9).then_some([0; 6]), class9_deltas_bits: (descriptor.tag == 9).then_some([0; 6]),
native_word_50_bits, native_word_50_bits,
@@ -4979,7 +5076,7 @@ fn control_factory_uses_generic_budget_callbacks(factory_tag: u32) -> bool {
} }
fn control_factory_override_is_supported(factory_tag: u32) -> bool { fn control_factory_override_is_supported(factory_tag: u32) -> bool {
!matches!(factory_tag, 1 | 17 | 26 | 30) !matches!(factory_tag, 1 | 17 | 30)
} }
fn native_control_factory_defaults(factory_tag: u32) -> (Option<u32>, Option<u32>) { fn native_control_factory_defaults(factory_tag: u32) -> (Option<u32>, Option<u32>) {
@@ -6259,6 +6356,7 @@ mod tests {
coefficient_bits: Some(coefficients), coefficient_bits: Some(coefficients),
multiplier_bits, multiplier_bits,
class9_selector0_scale_bits: class9_scale_bits, class9_selector0_scale_bits: class9_scale_bits,
tag26_extra_rate_bits: (factory_tag == 26).then_some(0),
class9_values_bits: (factory_tag == 9).then_some([0x3f80_0000; 6]), class9_values_bits: (factory_tag == 9).then_some([0x3f80_0000; 6]),
class9_deltas_bits: (factory_tag == 9).then_some([0; 6]), class9_deltas_bits: (factory_tag == 9).then_some([0; 6]),
native_word_50_bits, native_word_50_bits,
@@ -6679,6 +6777,262 @@ mod tests {
} }
} }
#[test]
fn control_budget_tag26_rate_matches_native_additive_cost() {
let mut device = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
device.key = 0;
device.descriptor.fixed_words[8] = 0.2_f32.to_bits();
device.tag26_extra_rate_bits = Some(0.3_f32.to_bits());
let provider = synthetic_node_provider_row(0, 0, 0.4_f32.to_bits());
let table = synthetic_control_device_table_with_providers(
MissionControlDeviceTableStatus::Available(vec![device.clone()]),
MissionControlNodeProviderTableStatus::Available(vec![provider]),
);
assert_eq!(
control_device_resource_rate(&table, &device, 0.5)
.expect("tag-26 resource rate")
.to_bits(),
(-0.25_f32).to_bits()
);
assert_eq!(
control_device_resource_rate(&table, &device, 0.7)
.expect("tag-26 fadd-before-fmul vector")
.to_bits(),
0xbeb3_3333
);
}
#[test]
fn control_budget_tag26_factory_defaults_and_into_preserve_extra_rate() {
let mut device = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
assert_eq!(device.tag26_extra_rate_bits, Some(0));
device.tag26_extra_rate_bits = Some(0.3_f32.to_bits());
let replacement = synthetic_control_record(19, 99, 0x30, None);
let control = synthetic_component_control(
1,
vec![replacement.clone()],
PreparedControlEffectSection::Parsed(Vec::new()),
);
apply_effective_control_override(&mut device, &replacement, 1, 0, &control);
assert_eq!(device.factory_tag, 26);
assert_eq!(device.descriptor.tag, 19);
assert_eq!(device.tag26_extra_rate_bits, Some(0.3_f32.to_bits()));
}
#[test]
fn control_budget_tag26_gate_returns_zero_before_provider_and_rate_reads() {
let mut device = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
device.key = 9;
let mut table = synthetic_control_device_table_with_providers(
MissionControlDeviceTableStatus::Available(vec![device.clone()]),
MissionControlNodeProviderTableStatus::Available(Vec::new()),
);
device.native_word_50_bits = Some(0);
assert_eq!(
control_device_resource_rate(&table, &device, 0.5).expect("disabled tag-26 gate"),
0.0
);
device.native_word_50_bits = Some(5);
device.key = 0;
let mut zero_provider = synthetic_node_provider_row(0, 0, 0.0_f32.to_bits());
zero_provider.current_type1_value_bits = 0.0_f32.to_bits();
table.node_provider_status =
MissionControlNodeProviderTableStatus::Available(vec![zero_provider]);
assert_eq!(
control_device_resource_rate(&table, &device, 0.5).expect("zero provider tag-26 gate"),
0.0
);
}
#[test]
fn control_device_selector_500_sets_only_matching_factory26_extra_rate() {
let device = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
let mut table =
synthetic_control_device_table(MissionControlDeviceTableStatus::Available(vec![
device,
]));
assert_eq!(
set_control_device_table_selector(&mut table, 0, 0x500, 0.25),
Ok(true)
);
let MissionControlDeviceTableStatus::Available(devices) = &table.status else {
unreachable!();
};
assert_eq!(devices[0].tag26_extra_rate_bits, Some(0.25_f32.to_bits()));
let mut other_current_tag =
synthetic_control_device_table(MissionControlDeviceTableStatus::Available(vec![
synthetic_effective_control_device(
26,
19,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
),
]));
assert_eq!(
set_control_device_table_selector(&mut other_current_tag, 0, 0x500, 0.5),
Ok(false)
);
let MissionControlDeviceTableStatus::Available(devices) = &other_current_tag.status else {
unreachable!();
};
assert_eq!(devices[0].tag26_extra_rate_bits, Some(0));
let mut wrong_factory =
synthetic_control_device_table(MissionControlDeviceTableStatus::Available(vec![
synthetic_effective_control_device(
9,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
),
]));
let wrong_factory_before = wrong_factory.clone();
assert_eq!(
set_control_device_table_selector(&mut wrong_factory, 0, 0x500, 0.5),
Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: 9 })
);
assert_eq!(wrong_factory, wrong_factory_before);
let before_bad_index = table.clone();
assert_eq!(
set_control_device_table_selector(&mut table, 1, 0x500, 0.5),
Err(ControlResourceAccessError::DeviceIndexOutOfRange {
device_index: 1,
device_count: 1,
})
);
assert_eq!(table, before_bad_index);
for (selector, value, expected) in [
(
0x501,
0.5,
ControlResourceAccessError::UnsupportedSelector(0x501),
),
(
0x500,
f32::NAN,
ControlResourceAccessError::NonFiniteValue {
factory_tag: 26,
field: "tag-26 extra rate",
},
),
(
0x500,
f32::from_bits(1),
ControlResourceAccessError::SubnormalValue {
factory_tag: 26,
field: "tag-26 extra rate",
},
),
] {
let before = table.clone();
assert_eq!(
set_control_device_table_selector(&mut table, 0, selector, value),
Err(expected)
);
assert_eq!(table, before);
}
}
#[test]
fn control_budget_tag26_update_uses_native_allocation_multiplier() {
let mut device = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
device.key = 0;
let provider = synthetic_node_provider_row(0, 0, 0.4_f32.to_bits());
let mut table = synthetic_control_device_table_with_providers(
MissionControlDeviceTableStatus::Available(vec![device]),
MissionControlNodeProviderTableStatus::Available(vec![provider]),
);
control_device_resource_update(&mut table, 0, 0.5, 0.375)
.expect("tag-26 allocation update");
let MissionControlDeviceTableStatus::Available(devices) = &table.status else {
unreachable!();
};
assert_eq!(devices[0].multiplier_bits, Some(0x3ec0_0000));
}
#[test]
fn control_budget_dispatch_matches_tag26_supply_and_demand() {
let mut producer = synthetic_effective_control_device(
19,
19,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
producer.key = 0;
producer.descriptor.fixed_words[8] = 0.625_f32.to_bits();
let mut consumer = synthetic_effective_control_device(
26,
26,
1.0_f32.to_bits(),
Some(1.0_f32.to_bits()),
None,
);
consumer.key = 0;
consumer.descriptor.fixed_words[8] = 0.2_f32.to_bits();
consumer.tag26_extra_rate_bits = Some(0.3_f32.to_bits());
let provider = synthetic_node_provider_row(0, 0, 0.4_f32.to_bits());
let mut table = synthetic_control_device_table_with_providers(
MissionControlDeviceTableStatus::Available(vec![producer, consumer]),
MissionControlNodeProviderTableStatus::Available(vec![provider]),
);
control_resource_budget_pass(&mut table, 0.5)
.expect("tag-26 demand is covered by the tag-19 supply");
let MissionControlDeviceTableStatus::Available(devices) = &table.status else {
unreachable!();
};
assert_eq!(devices[0].multiplier_bits, Some(0x3f30_0000));
assert_eq!(devices[1].multiplier_bits, Some(0x3f00_0000));
assert_eq!(table.current_output_rate_5dc_bits, 0x8000_0000);
assert_eq!(table.current_output_rate_5e0_bits, 0x3e80_0000);
}
#[test] #[test]
fn control_budget_current_tag_categories_match_native_prefix() { fn control_budget_current_tag_categories_match_native_prefix() {
let categories = [ let categories = [
@@ -7259,6 +7613,10 @@ mod tests {
query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00), query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00),
Err(ControlResourceAccessError::MissionUnavailable) Err(ControlResourceAccessError::MissionUnavailable)
); );
assert_eq!(
set_loaded_mission_control_device_selector(&mut engine, handle, 0, 0x500, 0.25),
Err(ControlResourceAccessError::MissionUnavailable)
);
assert_eq!( assert_eq!(
query_loaded_mission_behavior_resource(&engine, handle, 0x1005), query_loaded_mission_behavior_resource(&engine, handle, 0x1005),
Err(ControlResourceAccessError::MissionUnavailable) Err(ControlResourceAccessError::MissionUnavailable)
@@ -7318,6 +7676,12 @@ mod tests {
fparkan_world::WorldError::InvalidHandle fparkan_world::WorldError::InvalidHandle
)) ))
); );
assert_eq!(
set_loaded_mission_control_device_selector(&mut engine, invalid, 0, 0x500, 0.25),
Err(ControlResourceAccessError::World(
fparkan_world::WorldError::InvalidHandle
))
);
assert_eq!( assert_eq!(
query_loaded_mission_control_resource(&engine, stale, 1), query_loaded_mission_control_resource(&engine, stale, 1),
Err(ControlResourceAccessError::World( Err(ControlResourceAccessError::World(
@@ -7336,6 +7700,12 @@ mod tests {
fparkan_world::WorldError::StaleHandle fparkan_world::WorldError::StaleHandle
)) ))
); );
assert_eq!(
set_loaded_mission_control_device_selector(&mut engine, stale, 0, 0x500, 0.25),
Err(ControlResourceAccessError::World(
fparkan_world::WorldError::StaleHandle
))
);
assert_eq!( assert_eq!(
query_loaded_mission_behavior_resource(&engine, stale, 0x1005), query_loaded_mission_behavior_resource(&engine, stale, 0x1005),
Err(ControlResourceAccessError::World( Err(ControlResourceAccessError::World(
@@ -8253,6 +8623,7 @@ mod tests {
(13, 5), (13, 5),
(21, 5), (21, 5),
(25, 5), (25, 5),
(26, 5),
(27, 0), (27, 0),
] { ] {
let mut root = synthetic_control_record(factory_tag, 2, 0x20, None); let mut root = synthetic_control_record(factory_tag, 2, 0x20, None);
@@ -8290,12 +8661,15 @@ mod tests {
assert_eq!(device.multiplier_bits, Some(1.0_f32.to_bits())); assert_eq!(device.multiplier_bits, Some(1.0_f32.to_bits()));
assert_eq!(device.native_word_50_bits, Some(word_50)); assert_eq!(device.native_word_50_bits, Some(word_50));
assert_eq!(device.coefficient_bits.unwrap()[0], 0x30); assert_eq!(device.coefficient_bits.unwrap()[0], 0x30);
if factory_tag == 26 {
assert_eq!(device.tag26_extra_rate_bits, Some(0));
}
if factory_tag == 9 { if factory_tag == 9 {
assert_eq!(device.class9_values_bits, Some([0x3f80_0000; 6])); assert_eq!(device.class9_values_bits, Some([0x3f80_0000; 6]));
assert_eq!(device.class9_deltas_bits, Some([0; 6])); assert_eq!(device.class9_deltas_bits, Some([0; 6]));
} }
} }
for factory_tag in [1, 17, 26, 30] { for factory_tag in [1, 17, 30] {
assert!(!control_factory_override_is_supported(factory_tag)); assert!(!control_factory_override_is_supported(factory_tag));
} }
} }
@@ -9441,6 +9815,7 @@ mod tests {
Some([0x3f80_0000; 6]) Some([0x3f80_0000; 6])
); );
assert_eq!(mine_control_devices[2].class9_deltas_bits, Some([0; 6])); assert_eq!(mine_control_devices[2].class9_deltas_bits, Some([0; 6]));
assert_eq!(mine_control_devices[8].tag26_extra_rate_bits, Some(0));
assert_eq!( assert_eq!(
advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, f32::NAN), advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, f32::NAN),
Err(ControlResourceAccessError::NonFiniteValue { Err(ControlResourceAccessError::NonFiniteValue {
@@ -9448,14 +9823,38 @@ mod tests {
field: "Control resource interval", field: "Control resource interval",
}) })
); );
assert_eq!(
advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, 0.5),
Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: 26 })
);
assert_eq!( assert_eq!(
loaded_mission_control_device_tables(&mine_engine), loaded_mission_control_device_tables(&mine_engine),
Some(mine_control_before.as_slice()) Some(mine_control_before.as_slice())
); );
advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, 0.5)
.expect("whole licensed mine table passes supported Control budget callbacks");
let mine_control_after = loaded_mission_control_device_tables(&mine_engine)
.expect("licensed mine Control table after budget pass");
let mine_table_after = &mine_control_after[0];
let MissionControlDeviceTableStatus::Available(mine_devices_after) =
&mine_table_after.status
else {
panic!("licensed mine table became unavailable after budget pass");
};
let mine_multipliers = mine_devices_after
.iter()
.map(|device| device.multiplier_bits.map(|bits| format!("{bits:08x}")))
.collect::<Vec<_>>();
println!(
"licensed mine full Control budget: devices={} outputs={:08x},{:08x} multipliers={mine_multipliers:?} tag26Extra={:?}",
mine_devices_after.len(),
mine_table_after.current_output_rate_5dc_bits,
mine_table_after.current_output_rate_5e0_bits,
mine_devices_after[8].tag26_extra_rate_bits,
);
assert_eq!(mine_devices_after.len(), 11);
assert_eq!(mine_devices_after[8].tag26_extra_rate_bits, Some(0));
assert_eq!(mine_table_after.current_output_rate_5dc_bits, 0x3c23_d70a);
assert_eq!(mine_table_after.current_output_rate_5e0_bits, 0x3c23_d70a);
assert_eq!(mine_devices_after[0].multiplier_bits, Some(0x3f7f_d70a));
assert_eq!(mine_devices_after[1].multiplier_bits, Some(0x3f7f_d70a));
assert_ne!(mine_control_after, mine_control_before.as_slice());
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);
@@ -53,6 +53,10 @@ internal static class BehaviorDistributorTickOracle {
private const int ControlResourceCommonSetterRva = 0x21d50; private const int ControlResourceCommonSetterRva = 0x21d50;
private const int ControlResourceTag9ConstructorRva = 0x25460; private const int ControlResourceTag9ConstructorRva = 0x25460;
private const int ControlResourceTag15ConstructorRva = 0x22ae0; private const int ControlResourceTag15ConstructorRva = 0x22ae0;
private const int ControlResourceTag26ConstructorRva = 0x2d4b0;
private const int ControlResourceTag26SetterRva = 0x2c2b0;
private const int ControlResourceTag26RateRva = 0x2e540;
private const int ControlResourceTag26UpdateRva = 0x21890;
private const int ControlResourceTag25ConstructorRva = 0x20800; private const int ControlResourceTag25ConstructorRva = 0x20800;
private const int ControlResourceTag9RateRva = 0x25700; private const int ControlResourceTag9RateRva = 0x25700;
private const int ControlResourceTag9UpdateRva = 0x257b0; private const int ControlResourceTag9UpdateRva = 0x257b0;
@@ -68,6 +72,7 @@ internal static class BehaviorDistributorTickOracle {
private const int ControlResourceTag15GateRva = 0x21820; private const int ControlResourceTag15GateRva = 0x21820;
private const int ControlResourceTag9VtableRva = 0x3c938; private const int ControlResourceTag9VtableRva = 0x3c938;
private const int ControlResourceTag15VtableRva = 0x3c648; private const int ControlResourceTag15VtableRva = 0x3c648;
private const int ControlResourceTag26VtableRva = 0x3ce38;
private const int ControlResourceTag25VtableRva = 0x3c448; private const int ControlResourceTag25VtableRva = 0x3c448;
private const int ControlResourceTag9StringPairRva = 0x25600; private const int ControlResourceTag9StringPairRva = 0x25600;
private const int ControlResourceNoOpStringPairRva = 0x0bcd0; private const int ControlResourceNoOpStringPairRva = 0x0bcd0;
@@ -125,6 +130,8 @@ internal static class BehaviorDistributorTickOracle {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceBudgetUpdate(IntPtr self, float deltaSeconds); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceBudgetUpdate(IntPtr self, float deltaSeconds);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceTag9Update(IntPtr self, float deltaSeconds, float allocation); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceTag9Update(IntPtr self, float deltaSeconds, float allocation);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceDeviceUpdate(IntPtr self, float deltaSeconds, float allocation);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate byte NativeControlDeviceSelectorSetter(IntPtr ownerInterface, uint deviceIndex, uint selector, IntPtr value);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlDeviceFactory(IntPtr self, uint componentId, uint keyOffset, IntPtr record); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlDeviceFactory(IntPtr self, uint componentId, uint keyOffset, IntPtr record);
@@ -712,6 +719,7 @@ internal static class BehaviorDistributorTickOracle {
RunNativeControlTimedUpdateBaselineCases(controlModule); RunNativeControlTimedUpdateBaselineCases(controlModule);
RunNativeControlAggregateResourceCases(controlModule); RunNativeControlAggregateResourceCases(controlModule);
RunNativeControlResourceBudgetTag15Case(controlModule); RunNativeControlResourceBudgetTag15Case(controlModule);
RunNativeControlResourceBudgetTag26Case(controlModule);
RunNativeControlResourceBudgetClass9Case(controlModule); RunNativeControlResourceBudgetClass9Case(controlModule);
RunNativeControlGenericDefaultBudgetCase(controlModule); RunNativeControlGenericDefaultBudgetCase(controlModule);
RunNativeControlActivationCase(controlModule); RunNativeControlActivationCase(controlModule);
@@ -2362,6 +2370,168 @@ internal static class BehaviorDistributorTickOracle {
GC.KeepAlive(update); GC.KeepAlive(update);
} }
private static void RunNativeControlResourceBudgetTag26Case(IntPtr module) {
NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlOwnerConstructorRva), typeof(NativeControlOwnerConstructor));
NativeControlProviderRowConstructor constructProviderRow = (NativeControlProviderRowConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva), typeof(NativeControlProviderRowConstructor));
NativeControlDeviceFactory createDevice = (NativeControlDeviceFactory)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag26ConstructorRva), typeof(NativeControlDeviceFactory));
NativeControlResourceDeviceConstructor constructTag19 = (NativeControlResourceDeviceConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceDeviceConstructorRva), typeof(NativeControlResourceDeviceConstructor));
NativeControlResourceDeviceRate rate19 = (NativeControlResourceDeviceRate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, 0x229a0), typeof(NativeControlResourceDeviceRate));
NativeControlResourceComponentOverride applyInto = (NativeControlResourceComponentOverride)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceIntoRva), typeof(NativeControlResourceComponentOverride));
NativeControlResourceDeviceRate rate = (NativeControlResourceDeviceRate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag26RateRva), typeof(NativeControlResourceDeviceRate));
NativeControlResourceDeviceGate gate = (NativeControlResourceDeviceGate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag15GateRva), typeof(NativeControlResourceDeviceGate));
NativeControlResourceDeviceUpdate update = (NativeControlResourceDeviceUpdate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag26UpdateRva), typeof(NativeControlResourceDeviceUpdate));
NativeControlResourceBudgetUpdate budgetStep = (NativeControlResourceBudgetUpdate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceBudgetUpdateRva), typeof(NativeControlResourceBudgetUpdate));
NativeControlDeviceSelectorSetter setSelector = (NativeControlDeviceSelectorSetter)
Marshal.GetDelegateForFunctionPointer(
WriteStdCallFourArgsToFastcallThunk(0x380, IntPtr.Add(module, ControlResourceTag26SetterRva)),
typeof(NativeControlDeviceSelectorSetter));
using (Fixture fixture = NewFixture()) {
IntPtr owner = fixture.Allocate(0x664);
Expect(constructOwner(owner, IntPtr.Zero, 4) == owner, "tag-26 budget owner constructor");
IntPtr providerRow = fixture.Allocate(0x2c);
IntPtr ndpDescriptor = fixture.Allocate(0x4c);
Marshal.WriteInt32(ndpDescriptor, 4, FloatBits(100.0f));
constructProviderRow(providerRow, ndpDescriptor);
Marshal.WriteInt32(providerRow, 4, -1);
Marshal.WriteInt32(providerRow, 0x24, -1);
Marshal.WriteInt32(providerRow, 8, FloatBits(40.0f));
Marshal.WriteInt32(providerRow, 0x0c, FloatBits(40.0f));
Marshal.WriteInt32(providerRow, 0x14, FloatBits(0.4f));
Marshal.WriteIntPtr(owner, 0x55c, providerRow);
Marshal.WriteInt32(owner, 0x560, 1);
Marshal.WriteInt32(owner, 0x65c, FloatBits(1.0f));
Marshal.WriteInt32(owner, 0x58c, FloatBits(100.0f));
Marshal.WriteInt32(owner, 0x590, FloatBits(40.0f));
IntPtr devices = fixture.Allocate(8);
Marshal.WriteIntPtr(owner, 0x5bc, devices);
Marshal.WriteInt32(owner, 0x5c4, 2); // Factory device vector capacity.
IntPtr record = fixture.Allocate(0xb4);
Marshal.WriteInt32(record, 0, 26);
Marshal.WriteInt32(record, 0x18, -1); // Keep factory enable word +0x50.
Marshal.WriteInt32(record, 0x20, FloatBits(0.2f));
Expect(createDevice(owner, 3, 0, record) == IntPtr.Add(record, 0xb4),
"tag-26 native factory consumes one descriptor");
IntPtr device = Marshal.ReadIntPtr(devices);
Marshal.WriteInt32(owner, 0x5c0, 1);
Expect(device != IntPtr.Zero, "tag-26 factory publishes its allocated device pointer");
IntPtr vtable = Marshal.ReadIntPtr(device);
Expect(vtable == IntPtr.Add(module, ControlResourceTag26VtableRva) &&
Marshal.ReadIntPtr(vtable, 0x14) == IntPtr.Add(module, ControlResourceTag26RateRva) &&
Marshal.ReadIntPtr(vtable, 0x18) == IntPtr.Add(module, ControlResourceTag26UpdateRva),
"tag-26 factory keeps its native rate and update methods");
Expect(Marshal.ReadIntPtr(vtable, 0x1c) == IntPtr.Add(module, ControlResourceCommonSetterRva),
"tag-26 vtable uses the original common reader 0x21d50");
Expect(Marshal.ReadInt32(device, 0xa4) == 0, "tag-26 constructor initializes +0xa4 to zero");
// The full owner build path is outside this bounded callback fixture.
Marshal.WriteIntPtr(device, 0x3c, owner);
Marshal.WriteIntPtr(device, 0x44, IntPtr.Add(owner, 4));
Expect(Marshal.ReadInt32(device, 0x50) == 5, "tag-26 common setter preserves enabled default");
Expect(gate(device) == 0, "tag-26 provider gate is active");
IntPtr extra = fixture.Allocate(4);
Marshal.WriteInt32(extra, 0, FloatBits(0.3f));
byte setterResult = setSelector(IntPtr.Add(owner, 0x0c), 0, 0x500, extra);
Expect(setterResult == 1 && Marshal.ReadInt32(device, 0xa4) == FloatBits(0.3f),
"native selector 0x500 stores tag-26 extra rate");
float firstRate = rate(device, 0.5f);
Expect(FloatBits(firstRate) == FloatBits(-0.25f), "tag-26 rate adds +0xa4 before multiplying dt");
float orderingRate = rate(device, 0.7f);
Expect(unchecked((uint)FloatBits(orderingRate)) == 0xbeb33333u,
"native tag-26 f32 order rounds the sum before multiplying dt");
update(device, 0.5f, 0.375f);
Expect(Marshal.ReadInt32(device, 0x4c) == FloatBits(0.375f) &&
Marshal.ReadInt32(device, 0xa4) == FloatBits(0.3f),
"tag-26 update writes allocation multiplier and preserves extra rate");
IntPtr intoRecord = fixture.Allocate(0xb4);
Marshal.WriteInt32(intoRecord, 0, 26);
Marshal.WriteInt32(intoRecord, 0x18, -1);
Marshal.WriteInt32(intoRecord, 0x20, FloatBits(0.1f));
Expect(applyInto(owner, 0, intoRecord) == IntPtr.Add(intoRecord, 0xb4), "tag-26 INTO descriptor update");
Expect(Marshal.ReadInt32(device, 0xa4) == FloatBits(0.3f) &&
FloatBits(rate(device, 0.5f)) == FloatBits(-0.2f),
"common INTO setter preserves tag-26 +0xa4 across descriptor replacement");
Console.WriteLine("native-control-tag26-callbacks: controlHash=" + ExpectedControlSha256 +
" ctorExtra=0 setter500=1 extra=" + Hex(unchecked((uint)Marshal.ReadInt32(device, 0xa4))) +
" rateDt0.5=" + Hex(unchecked((uint)FloatBits(firstRate))) +
" rateDt0.7=" + Hex(unchecked((uint)FloatBits(orderingRate))) +
" updateMultiplier=" + Hex(unchecked((uint)Marshal.ReadInt32(device, 0x4c))) +
" intoRate=" + Hex(unchecked((uint)FloatBits(rate(device, 0.5f)))) +
" intoExtra=" + Hex(unchecked((uint)Marshal.ReadInt32(device, 0xa4))));
IntPtr otherTagRecord = fixture.Allocate(0xb4);
Marshal.WriteInt32(otherTagRecord, 0, 19);
Marshal.WriteInt32(otherTagRecord, 0x18, -1);
Marshal.WriteInt32(otherTagRecord, 0x20, FloatBits(0.2f));
applyInto(owner, 0, otherTagRecord);
byte otherTagSetter = setSelector(IntPtr.Add(owner, 0x0c), 0, 0x500, extra);
Expect(otherTagSetter == 0 &&
Marshal.ReadInt32(device, 0xa4) == FloatBits(0.3f),
"selector 0x500 returns no-result for a different current descriptor tag");
Console.WriteLine("native-control-tag26-setter: selector=0x500 tag26=" + setterResult +
" otherCurrentTag=" + otherTagSetter + " preservedExtra=" +
Hex(unchecked((uint)Marshal.ReadInt32(device, 0xa4))));
IntPtr budgetRecord = fixture.Allocate(0xb4);
Marshal.WriteInt32(budgetRecord, 0, 26);
Marshal.WriteInt32(budgetRecord, 0x18, -1);
Marshal.WriteInt32(budgetRecord, 0x20, FloatBits(0.2f));
applyInto(owner, 0, budgetRecord);
// The original factory-19 callbacks supply +0.125 against the
// original factory-26 demand -0.25; native dispatch allocates one
// half to tag26 and updates the generator reserve to 0.6875.
IntPtr producer = fixture.Allocate(0x94);
Expect(constructTag19(producer) == producer, "native tag-19 supplier constructor");
IntPtr producerVtable = Marshal.ReadIntPtr(producer);
Expect(producerVtable == IntPtr.Add(module, ControlResourceTag19VtableRva) &&
Marshal.ReadIntPtr(producerVtable, 0x14) == IntPtr.Add(module, 0x229a0) &&
Marshal.ReadIntPtr(producerVtable, 0x18) == IntPtr.Add(module, 0x22a60),
"native tag-19 supplier keeps its original rate and update callbacks");
Marshal.WriteIntPtr(producer, 0x3c, owner);
Marshal.WriteIntPtr(producer, 0x44, IntPtr.Add(owner, 4));
IntPtr producerRecord = fixture.Allocate(0xb4);
Marshal.WriteInt32(producerRecord, 0, 19);
Marshal.WriteInt32(producerRecord, 0x18, -1);
Marshal.WriteInt32(producerRecord, 0x20, FloatBits(0.625f));
Marshal.WriteInt32(producerRecord, 0x2c, FloatBits(1.0f));
Marshal.WriteIntPtr(devices, 0, producer);
Marshal.WriteIntPtr(devices, 4, device);
Marshal.WriteInt32(owner, 0x5c0, 2);
Expect(applyInto(owner, 0, producerRecord) == IntPtr.Add(producerRecord, 0xb4),
"native tag-19 supplier descriptor install");
Expect(gate(producer) == 0 && FloatBits(rate19(producer, 0.5f)) == FloatBits(0.125f),
"original tag-19 provider-backed supply is 0.125");
budgetStep(owner, 0.5f);
uint outputA = unchecked((uint)Marshal.ReadInt32(owner, 0x5dc));
uint outputB = unchecked((uint)Marshal.ReadInt32(owner, 0x5e0));
uint consumerScale = unchecked((uint)Marshal.ReadInt32(device, 0x4c));
uint producerScale = unchecked((uint)Marshal.ReadInt32(producer, 0x4c));
Console.WriteLine("native-control-tag26-budget: controlHash=" + ExpectedControlSha256 +
" factory=0x2d4b0 case26Ctor=0x20800 setter=0x2c2b0 rate=0x2e540 update=0x21890 budget=0x2d340" +
" provider=0.4 descriptorRate=0.2 extra=0.3 dt=0.5 producerSupply=0.125" +
" consumerAllocation=" + Float(ReadFloat(device, 0x4c)) +
" producerMultiplier=" + Float(ReadFloat(producer, 0x4c)) +
" outputs=" + Hex(outputA) + "," + Hex(outputB));
Expect(consumerScale == FloatBits(0.5f) && producerScale == FloatBits(0.6875f),
"native budget dispatcher allocates tag-26 demand against a category-1 supplier");
Expect(outputA == 0x80000000u && outputB == FloatBits(0.25f),
"native tag-26 mixed budget outputs use the scaled factory-26 rate");
GC.KeepAlive(setSelector);
}
}
private static void RunNativeControlActivationCase(IntPtr module) { private static void RunNativeControlActivationCase(IntPtr module) {
NativeControlOwnerConstructor construct = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer( NativeControlOwnerConstructor construct = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer(
IntPtr.Add(module, 0x31490), typeof(NativeControlOwnerConstructor)); IntPtr.Add(module, 0x31490), typeof(NativeControlOwnerConstructor));
@@ -3692,6 +3862,17 @@ internal static class BehaviorDistributorTickOracle {
Marshal.Copy(code, 0, thunk, code.Length); Marshal.Copy(code, 0, thunk, code.Length);
return thunk; return thunk;
} }
private static IntPtr WriteStdCallFourArgsToFastcallThunk(int offset, IntPtr fastcallTarget) {
// Adapt stdcall(owner, index, selector, value) to ECX=owner, EDX=index,
// stack(selector, value) for the native fastcall property setter.
byte[] code = { 0xff,0x74,0x24,0x10, 0xff,0x74,0x24,0x10,
0x8b,0x54,0x24,0x10, 0x8b,0x4c,0x24,0x0c,
0xb8,0,0,0,0, 0xff,0xd0, 0xc2,0x10,0x00 };
WriteImm32(code, 17, fastcallTarget.ToInt32());
IntPtr thunk = IntPtr.Add(fastcallThunks, offset);
Marshal.Copy(code, 0, thunk, code.Length);
return thunk;
}
private static IntPtr WriteX87ControlWordReader(int offset) { private static IntPtr WriteX87ControlWordReader(int offset) {
byte[] code = { 0xd9, 0x3d, 0, 0, 0, 0, 0xc3 }; // fnstcw [absolute address]; ret byte[] code = { 0xd9, 0x3d, 0, 0, 0, 0, 0xc3 }; // fnstcw [absolute address]; ret
WriteImm32(code, 2, x87ControlWordCell.ToInt32()); WriteImm32(code, 2, x87ControlWordCell.ToInt32());
+18 -8
View File
@@ -361,7 +361,7 @@ API `distribute_loaded_mission_control_provider_amount` переносит
Три API работают только с CPU-проекцией и не подключены к GPU/window path или Три API работают только с CPU-проекцией и не подключены к GPU/window path или
игровому Control clock. Отдельный игровому Control clock. Отдельный
`advance_loaded_mission_control_budget` переносит `0x2d340` для фабричных `advance_loaded_mission_control_budget` переносит `0x2d340` для фабричных
callbacks `9`, `15`, `19` и общей default-пары; category lookup использует callbacks `9`, `15`, `19`, `26` и общей default-пары; category lookup использует
текущий descriptor tag из подтверждённого префикса `0..=34`. Ставки сохраняются текущий descriptor tag из подтверждённого префикса `0..=34`. Ставки сохраняются
в `+0x5dc/+0x5e0`. Шаг выполняется транзакционно, а неподдержанный в `+0x5dc/+0x5e0`. Шаг выполняется транзакционно, а неподдержанный
специализированный callback, provider или диапазон числа возвращает ошибку без частичного специализированный callback, provider или диапазон числа возвращает ошибку без частичного
@@ -417,20 +417,30 @@ visibility transition `0x11920` с исходными AniMesh callbacks. Отд
в этом векторе подставлен mock-ом. Отдельный вектор с исходным генератором в этом векторе подставлен mock-ом. Отдельный вектор с исходным генератором
tag `19` (`0x22980`, `0x229a0`, `0x22a60`) проверяет полную и ограниченную tag `19` (`0x22980`, `0x229a0`, `0x22a60`) проверяет полную и ограниченную
rate, изменение provider и собственного множителя генератора; отрицательные rate, изменение provider и собственного множителя генератора; отрицательные
параметры и нулевой знаменатель не проверены. Native-векторы бюджетного шага параметры и нулевой знаменатель не проверены. Новый оригинальный вектор
`0x2d340` смешивает tag `19` и tag `26`: supply `0.125` покрывает половину
demand `0.25`, итоговые множители равны `0.6875` и `0.5`, а выходы —
`0x80000000` и `0x3e800000`. Тот же oracle подтверждает tag-26 selector `0x500`,
общий reader `0x21d50` и сохранение поля `+0xa4` при `INTO`. Rust-тест
повторяет бюджетную пару и разрешает tag `26` в projection INTO guard, оставляя
теги `1`, `17` и `30` явными границами. Native-векторы бюджетного шага
подтверждают текущие ставки `+0x5dc/+0x5e0`, включая значения `0.2`/`0.4` у подтверждают текущие ставки `+0x5dc/+0x5e0`, включая значения `0.2`/`0.4` у
потребителя tag 15, пустой результат `-0.0` и невырожденную пару исходных потребителя tag 15, пустой результат `-0.0` и невырожденную пару исходных
устройств tag 19/tag 15 при `dt = 0.375`; Rust-тест повторяет поля этой пары устройств tag 19/tag 15 при `dt = 0.375`; Rust-тест повторяет поля этой пары
на синтетической таблице. Rust переносит class-9 rate/update и шестизначную на синтетической таблице. Rust переносит class-9 rate/update и шестизначную
adjustment `0x25a90`, общую default callback-пару `0x21860/0x21890`, а также adjustment `0x25a90`, tag-26 rate/update и setter selector `0x500`, общую
category map только для current descriptor tags `0..=34`. Специализированные default callback-пару `0x21860/0x21890`, а также category map только для current
factory callbacks вне этого набора остаются явными ошибками. descriptor tags `0..=34`. Остальные ещё не описанные специализированные
callbacks остаются явными ошибками.
Лицензированный `min_l_n1.dat` содержит исходные factory/current tags Лицензированный `min_l_n1.dat` содержит исходные factory/current tags
`[19, 19, 9, 15, 12, 12, 13, 13, 26, 25, 21]`. Публичный API оставляет всю `[19, 19, 9, 15, 12, 12, 13, 13, 26, 25, 21]`. Публичный API оставляет всю
таблицу без изменений при не конечном интервале. Для конечного интервала таблицу без изменений при не конечном интервале. Для конечного интервала
предварительная проверка распознаёт категории, но находит неподдержанный ignored Rust-тест проходит budget по всем 11 строкам без фильтра. При `dt = 0.5`
factory tag `26` и отклоняет вызов до вычисления ставок и обновлений; полный обе output rates равны `0x3c23d70a`, первые два множителя tag `19` равны
бюджетный проход для этой реальной таблицы поэтому не проверен. Успешные бюджетные native-векторы используют синтетические `0x3f7fd70a`, остальные множители остаются `1.0`, а добавка tag `26` остаётся
нулевой. Эта проверка подтверждает только CPU-проекцию реальной таблицы; она
не запускает native полный mission tick и не подключает Control clock.
Успешные бюджетные native-векторы используют синтетические
NDP/Node38-строки и подтверждают локальные переходы и порядок вызовов, а не NDP/Node38-строки и подтверждают локальные переходы и порядок вызовов, а не
полный Control tick на миссии. Прямые oracle-вызовы `0x11220` для одиночной полный Control tick на миссии. Прямые oracle-вызовы `0x11220` для одиночной
строки используют `updateEffects = false`, что исключает ветвь Control FX и строки используют `updateEffects = false`, что исключает ветвь Control FX и
+53 -16
View File
@@ -792,23 +792,19 @@ provider getter и изменение количества не подменен
Rust предоставляет явный CPU-вызов Rust предоставляет явный CPU-вызов
`advance_loaded_mission_control_budget(engine, handle, dt)`. Он переносит `advance_loaded_mission_control_budget(engine, handle, dt)`. Он переносит
исходные callbacks factory tags `9`, `15`, `19` и общий default callback, а исходные callbacks factory tags `9`, `15`, `19`, `26` и общий default callback,
категории выбирает по текущему descriptor tag. Native map подтверждён только а категории выбирает по текущему descriptor tag. Native map подтверждён только
для префикса тегов `0..=34`; неизвестная категория, специализированный для префикса тегов `0..=34`; неизвестная категория, неподдержанный
callback, provider или численный диапазон возвращает ошибку без публикации специализированный callback, provider или численный диапазон возвращает ошибку
частичных изменений. Ставки `+0x5dc/+0x5e0` сохраняются отдельно, а сам вызов без публикации частичных изменений. Ставки `+0x5dc/+0x5e0` сохраняются отдельно,
ещё не включён в Control clock. а сам вызов ещё не включён в Control clock.
Нативный oracle проверяет пустую таблицу с отрицательным нулём в обеих выходных Нативный oracle проверяет пустую таблицу с отрицательным нулём в обеих выходных
ставках, невырожденную пару исходных устройств tag 19/tag 15 при `dt = 0.375` ставках, невырожденную пару исходных устройств tag 19/tag 15 при `dt = 0.375`
и tag 9 с шестью значениями и шестью накопителями изменений. Rust-тесты и tag 9 с шестью значениями и шестью накопителями изменений. Rust-тесты
повторяют их по битам, включая положительное и отрицательное обновления tag 9. повторяют их по битам, включая положительное и отрицательное обновления tag 9.
В лицензированном `min_l_n1.dat` предварительная проверка распознаёт категории, Rust также отклоняет нулевой знаменатель обновления tag 19; тест вызывает эту
но находит неподдержанный factory tag `26` и отклоняет бюджет до расчёта ставок ошибку после изменения provider потребителем и подтверждает откат всей таблицы.
или вызова устройств; транзакционный тест подтверждает, что исходная таблица
не меняется. Полный бюджетный проход на реальной миссии пока не выполнен. Rust также отклоняет нулевой знаменатель
обновления tag 19; тест вызывает эту ошибку после изменения provider
потребителем и подтверждает откат всей таблицы.
#### Генератор tag 19 в том же бюджетном проходе #### Генератор tag 19 в том же бюджетном проходе
@@ -911,10 +907,51 @@ Descriptor category lookup `0x2c3a0` возвращает следующие з
Именно текущий descriptor выбирает категорию; callback остаётся у исходного Именно текущий descriptor выбирает категорию; callback остаётся у исходного
factory-класса. В Rust неизвестный текущий тег за концом префикса и factory-класса. В Rust неизвестный текущий тег за концом префикса и
специализированный factory callback не подменяются общей формулой. На неподдержанный специализированный factory callback не подменяются общей
лицензированном Unit DAT полный проход доходит до factory `26` и возвращает формулой. Tag `26` поддержан отдельной парой callbacks, описанной ниже. Ни этот
ошибку без частичного изменения таблицы. Ни этот CPU-вызов, ни локальные CPU-вызов, ни локальные векторы не означают включения бюджетного шага в Control
векторы не означают включения бюджетного шага в Control clock. clock.
#### Фабричный tag 26: изменяемая добавка
В этой сборке `Control.dll` SHA-256
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` фабричный
tag `26` обрабатывается в функции-фабрике RVA `0x2d4b0`: её ветка вызывает общий
конструктор `0x20800`, обнуляет поле устройства `+0xa4` и устанавливает vtable
`0x3ce38`. Rate
callback RVA `0x2e540` сначала проходит проверку доступности provider, затем
считает `-((descriptor[+0x20] + device[+0xa4]) * dt)`; прибавление до умножения
важно для битового результата. Update slot `+0x18` ведёт на RVA `0x21890` и
сохраняет выделенную долю как обычный multiplier `+0x4c`, не трогая `+0xa4`.
Vtable slot `+0x1c` ведёт на общий reader `0x21d50`. Native override RVA
`0x2d890` через этот reader заменяет descriptor и коэффициенты, сохраняя
изменяемую добавку. Oracle проверяет сам slot и повторный rate после `INTO`.
Rust теперь включает factory tag `26` в projection guard для `INTO` по этой
проверке; границы для фабричных тегов `1`, `17` и `30` остаются закрытыми.
Отдельный Control setter RVA `0x2c2b0` поддерживает selector `0x500`: только
при текущем descriptor tag `26` он записывает переданные биты в `+0xa4` и
возвращает успех. Behavior.dll с SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99` вызывает
его через RVA `0x19880`. Callers `0x29910` и `0x2f400` передают ноль, а
`0x29ba0` передаёт поле `+0x64` рядом с кодом `NewfOreCost`; смысл последнего
значения не установлен, поэтому Rust пока не запускает этот Behavior путь.
При `B = 0.2`, добавке `0.3` и `dt = 0.7` исходная rate имеет биты
`0xbeb33333`. Смешанная проверка использует оригинальный tag-19 генератор
(`B = 0.625`, `C0 = 1`, provider `0.4`) и tag-26 потребитель (`B = 0.2`,
добавка `0.3`) при `dt = 0.5`: первый даёт `0.125`, второй запрашивает `0.25`.
Native dispatcher выделяет tag `26` половину, сохраняет множители
`0x3f300000`/`0x3f000000`, а ставки `+0x5dc/+0x5e0` получают
`0x80000000`/`0x3e800000`. Постоянный x86 oracle вызывает исходные callbacks и
Rust-тест повторяет этот вектор.
Ignored Rust-тест также проходит полный неотфильтрованный Control budget по
одиннадцати исходным устройствам `min_l_n1.dat` при `dt = 0.5`; выходы равны
`0x3c23d70a`/`0x3c23d70a`, первые два множителя tag `19` — `0x3f7fd70a`.
Это проверка CPU-проекции реальной таблицы, а не сравнение целой миссии с
оригиналом.
#### Активация и начальные часы #### Активация и начальные часы
+25 -6
View File
@@ -1082,12 +1082,31 @@ Rust переносит этот порядок, сохраняет native на
классы, которые исходный диспетчер направляет в фабрику по умолчанию. Текущая классы, которые исходный диспетчер направляет в фабрику по умолчанию. Текущая
категория читается из descriptor: её native map подтверждён только для тегов категория читается из descriptor: её native map подтверждён только для тегов
`0..=34`. Неподдержанный специализированный callback, недоступный provider или `0..=34`. Неподдержанный специализированный callback, недоступный provider или
неподдержанное число останавливают шаг без частичных изменений. В неподдержанное число останавливают шаг без частичных изменений.
лицензированном `min_l_n1.dat` предварительная проверка распознаёт текущие
категории и выявляет неподдержанный factory tag `26`; она отклоняет бюджетный У фабричного tag `26` ставка складывается из базового значения устройства и
вызов до расчёта ставок и обновлений, сохраняя таблицу целиком. Полный изменяемой добавки. Затем сумма умножается на прошедшее время:
бюджетный проход на реальной миссии и подключение к игровым часам пока не
подтверждены. ```text
rate = -((B + extra) * dt)
```
Например, при `B = 0.2`, `extra = 0.3` и `dt = 0.5` устройство запрашивает
`0.25`. Если доступный бюджет равен `0.125`, оно получает половину запроса;
устройство сохраняет долю распределения, а сама добавка переживает замену
descriptor через `INTO`. Так разные части Control могут менять базовую запись и
текущее состояние независимо.
Эту добавку записывает исходный selector `0x500`, когда текущий descriptor
тоже относится к tag `26`. Мы восстановили его точку вызова в Behavior, но
смысл передаваемого там значения пока не установлен, поэтому этот Behavior путь
ещё не исполняется. Подтверждённые callbacks tag `26`, их native адреса и
точные проверочные векторы приведены в [техническом приложении](../appendices/script-vm.md#фабричный-tag-26-изменяемая-добавка).
Полный явный CPU-проход по исходной таблице из одиннадцати устройств
`min_l_n1.dat` тоже завершился. Этот результат проверяет вычисление для одной
загруженной таблицы; обновление пока не подключено к игровым часам и не
заменяет полный Control tick.
Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и
передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`, передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,