diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 6b037aa..44b6a67 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -201,6 +201,10 @@ pub struct EffectiveControlDevice { pub multiplier_bits: Option, /// Class-9-only scale used by its selector-0 getter. pub class9_selector0_scale_bits: Option, + /// Six class-9 budget values at native `+0x98..+0xac`. + pub class9_values_bits: Option<[u32; 6]>, + /// Six accumulated class-9 changes at native `+0xb0..+0xc4`. + pub class9_deltas_bits: Option<[u32; 6]>, /// Current native word at `+0x50` when it is known. pub native_word_50_bits: Option, /// Current native tag-15 byte at `+0x94`; constructors currently set zero. @@ -419,7 +423,7 @@ pub enum MissionControlDeviceTableUnavailableReason { unit_component_index: usize, target_index: u32, }, - /// The receiver tag does not have a verified common override method. + /// The receiver's override behavior is not supported. UnsupportedOverrideReceiver { factory_tag: u32 }, /// The EXTO key needs a global model-node mapping that is not available. UnsupportedExToNodeMapping { unit_component_index: usize }, @@ -2278,8 +2282,9 @@ pub fn distribute_loaded_mission_control_provider_amount( /// Runs the supported loaded Control resource-budget pass (`Control.dll+0x2d340`). /// -/// The pass currently supports original factory callbacks for tags 15 and 19, -/// with current descriptor tags 15 and 19 (native categories 0 and 1). It is an explicit CPU update; +/// The pass supports the original factory callbacks for tags 9, 15, and 19, +/// 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; /// the mission clock does not call it yet. Rate outputs at `+0x5dc/+0x5e0` are /// stored on the table after a successful pass. /// @@ -2924,7 +2929,9 @@ fn control_resource_budget_pass_inner( for index in 0..device_count { let device = control_budget_device(table, index)?; control_budget_category(device.descriptor.tag)?; - if !matches!(device.factory_tag, 15 | 19) { + if !matches!(device.factory_tag, 9 | 15 | 19) + && !control_factory_uses_generic_budget_callbacks(device.factory_tag) + { return Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: device.factory_tag, }); @@ -3019,11 +3026,14 @@ fn control_resource_budget_pass_inner( } fn control_budget_category(tag: u32) -> Result { - match tag { - 15 => Ok(0), - 19 => Ok(1), - _ => Err(ControlResourceAccessError::UnsupportedCurrentTag { tag }), - } + const NATIVE_CATEGORY_PREFIX: [usize; 35] = [ + 0, 4, 4, 0, 2, 3, 0, 0, 2, 5, 5, 0, 0, 0, 1, 0, 3, 2, 0, 1, 3, 5, 4, 0, 4, 0, 3, 5, 0, 0, + 4, 0, 1, 2, 4, + ]; + NATIVE_CATEGORY_PREFIX + .get(tag as usize) + .copied() + .ok_or(ControlResourceAccessError::UnsupportedCurrentTag { tag }) } fn control_budget_device( @@ -3192,12 +3202,95 @@ fn control_device_resource_rate( let Some(provider_value) = control_device_budget_active_provider_value(table, device)? else { return Ok(0.0); }; - let coefficient_0 = control_device_budget_coefficient(device, 0)?; let descriptor_rate = control_device_budget_descriptor_float(device, 8, "descriptor resource rate +0x20")?; match device.factory_tag { + 9 => { + let coefficient_0 = control_device_budget_coefficient(device, 0)?; + let coefficient_1 = control_device_budget_coefficient(device, 1)?; + let coefficient_2 = control_device_budget_coefficient(device, 2)?; + let class9_scale = control_device_float( + device, + device.class9_selector0_scale_bits, + "class-9 selector-0 scale", + )?; + let capacity_scale = checked_control_product( + device.factory_tag, + "class-9 scaled capacity coefficient", + coefficient_0, + class9_scale, + )?; + let values = + device + .class9_values_bits + .ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: device.factory_tag, + field: "class-9 six values +0x98", + })?; + let mut value_sum = 0.0_f32; + for bits in values { + value_sum = checked_control_sum( + device.factory_tag, + "class-9 value sum", + value_sum, + checked_control_float( + device.factory_tag, + "class-9 value", + f32::from_bits(bits), + )?, + )?; + } + let free_values = checked_control_sum( + device.factory_tag, + "class-9 free value capacity", + 6.0, + -value_sum, + )?; + let capacity = checked_control_product( + device.factory_tag, + "class-9 available capacity", + free_values, + capacity_scale, + )?; + let query = checked_control_product( + device.factory_tag, + "class-9 provider coefficient", + coefficient_1, + provider_value, + )?; + let query_interval = checked_control_product( + device.factory_tag, + "class-9 provider rate times interval", + query, + delta_time_seconds, + )?; + let interval_rate = checked_control_product( + device.factory_tag, + "class-9 interval descriptor rate", + delta_time_seconds, + descriptor_rate, + )?; + let selected = if capacity < query_interval { + capacity + } else { + query_interval + }; + let coefficient_cost = checked_control_product( + device.factory_tag, + "class-9 coefficient cost", + coefficient_2, + selected, + )?; + checked_control_sum( + device.factory_tag, + "class-9 resource rate", + -interval_rate, + -coefficient_cost, + ) + } 19 => { + let coefficient_0 = control_device_budget_coefficient(device, 0)?; let multiplier = control_device_float(device, device.multiplier_bits, "multiplier")?; let mut raw = checked_control_product( device.factory_tag, @@ -3236,6 +3329,7 @@ fn control_device_resource_rate( } } 15 => { + let coefficient_0 = control_device_budget_coefficient(device, 0)?; let coefficient_1 = control_device_budget_coefficient(device, 1)?; let interval_rate = checked_control_product( device.factory_tag, @@ -3284,6 +3378,15 @@ fn control_device_resource_rate( -coefficient_cost, ) } + factory_tag if control_factory_uses_generic_budget_callbacks(factory_tag) => { + let interval_rate = checked_control_product( + device.factory_tag, + "generic Control interval descriptor rate", + delta_time_seconds, + descriptor_rate, + )?; + Ok(-interval_rate) + } factory_tag => Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag }), } } @@ -3354,6 +3457,82 @@ fn control_device_resource_update( return Ok(()); }; match device.factory_tag { + 9 => { + set_control_device_multiplier(table, device_index, allocation)?; + let device = control_budget_device(table, device_index)?; + let rate = control_device_resource_rate(table, device, delta_time_seconds)?; + let allocated_rate = checked_control_product( + device.factory_tag, + "class-9 allocated rate", + rate, + allocation, + )?; + let interval_rate = checked_control_product( + device.factory_tag, + "class-9 interval descriptor rate", + delta_time_seconds, + control_device_budget_descriptor_float( + device, + 8, + "descriptor resource rate +0x20", + )?, + )?; + let remaining = checked_control_sum( + device.factory_tag, + "class-9 remaining adjustment", + -allocated_rate, + -interval_rate, + )?; + if remaining == 0.0 { + return Ok(()); + } + let coefficient_2 = control_device_budget_coefficient(device, 2)?; + let raw_change = if coefficient_2 > 0.0 { + checked_control_ratio("class-9 adjustment coefficient", remaining, coefficient_2)? + } else { + let coefficient_1 = control_device_budget_coefficient(device, 1)?; + let query = checked_control_product( + device.factory_tag, + "class-9 provider coefficient", + coefficient_1, + provider_value, + )?; + let query_interval = checked_control_product( + device.factory_tag, + "class-9 provider rate times interval", + query, + delta_time_seconds, + )?; + if remaining > 0.0 { + query_interval + } else { + -query_interval + } + }; + let class9_scale = control_device_float( + device, + device.class9_selector0_scale_bits, + "class-9 selector-0 scale", + )?; + let coefficient_0 = control_device_budget_coefficient(device, 0)?; + let capacity_scale = checked_control_product( + device.factory_tag, + "class-9 scaled capacity coefficient", + coefficient_0, + class9_scale, + )?; + if !table.node_provider_distribution_suspended && capacity_scale > 0.0 { + let divisor = checked_control_product( + device.factory_tag, + "class-9 six-value scale", + capacity_scale, + 6.0, + )?; + let change = + checked_control_ratio("class-9 value adjustment", raw_change, divisor)?; + adjust_control_class9_values(table, device_index, change)?; + } + } 19 => { let coefficient_0 = control_device_budget_coefficient(&device, 0)?; if coefficient_0 < 0.0 { @@ -3484,6 +3663,9 @@ fn control_device_resource_update( )?; } } + factory_tag if control_factory_uses_generic_budget_callbacks(factory_tag) => { + set_control_device_multiplier(table, device_index, allocation)?; + } factory_tag => { return Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag }) } @@ -3491,6 +3673,157 @@ fn control_device_resource_update( Ok(()) } +fn set_control_device_multiplier( + table: &mut MissionControlDeviceTableState, + device_index: usize, + value: f32, +) -> Result<(), ControlResourceAccessError> { + let device = control_budget_device(table, device_index)?; + let factory_tag = device.factory_tag; + let value = checked_control_float(factory_tag, "multiplier", value)?; + match &mut table.status { + MissionControlDeviceTableStatus::Available(devices) => { + let Some(device) = devices.get_mut(device_index) else { + return Err(ControlResourceAccessError::DeviceIndexOutOfRange { + device_index, + device_count: devices.len(), + }); + }; + device.multiplier_bits = Some(value.to_bits()); + } + MissionControlDeviceTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::TableUnavailable(reason.clone())) + } + } + Ok(()) +} + +fn adjust_control_class9_values( + table: &mut MissionControlDeviceTableState, + device_index: usize, + change: f32, +) -> Result<(), ControlResourceAccessError> { + const CLASS9_AVERAGE_FACTOR: f32 = f32::from_bits(0x3e2a_aaab); + + let device = control_budget_device(table, device_index)?; + let values = device + .class9_values_bits + .ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: device.factory_tag, + field: "class-9 six values +0x98", + })?; + let deltas = device + .class9_deltas_bits + .ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: device.factory_tag, + field: "class-9 six deltas +0xb0", + })?; + let factory_tag = device.factory_tag; + let mut sum = 0.0_f32; + for bits in values { + sum = checked_control_sum( + factory_tag, + "class-9 value sum", + sum, + checked_control_float(factory_tag, "class-9 value", f32::from_bits(bits))?, + )?; + } + let average = checked_control_product( + factory_tag, + "class-9 value average", + sum, + CLASS9_AVERAGE_FACTOR, + )?; + let mut updated_values = values; + let mut updated_deltas = deltas; + if change > 0.0 && average != 1.0 { + let remaining = + checked_control_sum(factory_tag, "class-9 remaining average", 1.0, -average)?; + let factor = + checked_control_ratio("class-9 positive adjustment factor", change, remaining)?; + let factor = if factor > 1.0 { 1.0 } else { factor }; + for index in 0..6 { + let value = + checked_control_float(factory_tag, "class-9 value", f32::from_bits(values[index]))?; + let delta = checked_control_float( + factory_tag, + "class-9 accumulated delta", + f32::from_bits(deltas[index]), + )?; + let room = checked_control_sum(factory_tag, "class-9 value room", 1.0, -value)?; + let adjustment = checked_control_product( + factory_tag, + "class-9 positive value adjustment", + room, + factor, + )?; + updated_values[index] = checked_control_sum( + factory_tag, + "class-9 value after positive adjustment", + value, + adjustment, + )? + .to_bits(); + updated_deltas[index] = checked_control_sum( + factory_tag, + "class-9 delta after positive adjustment", + delta, + adjustment, + )? + .to_bits(); + } + } else if change < 0.0 && average != 0.0 { + let factor = checked_control_ratio("class-9 negative adjustment factor", -change, average)?; + let factor = if factor > 1.0 { 1.0 } else { factor }; + for index in 0..6 { + let value = + checked_control_float(factory_tag, "class-9 value", f32::from_bits(values[index]))?; + let delta = checked_control_float( + factory_tag, + "class-9 accumulated delta", + f32::from_bits(deltas[index]), + )?; + let removed = checked_control_product( + factory_tag, + "class-9 negative value adjustment", + factor, + value, + )?; + updated_values[index] = checked_control_sum( + factory_tag, + "class-9 value after negative adjustment", + value, + -removed, + )? + .to_bits(); + updated_deltas[index] = checked_control_sum( + factory_tag, + "class-9 delta after negative adjustment", + delta, + -removed, + )? + .to_bits(); + } + } + + match &mut table.status { + MissionControlDeviceTableStatus::Available(devices) => { + let Some(device) = devices.get_mut(device_index) else { + return Err(ControlResourceAccessError::DeviceIndexOutOfRange { + device_index, + device_count: devices.len(), + }); + }; + device.class9_values_bits = Some(updated_values); + device.class9_deltas_bits = Some(updated_deltas); + } + MissionControlDeviceTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::TableUnavailable(reason.clone())) + } + } + Ok(()) +} + fn apply_control_node_delta( table: &mut MissionControlDeviceTableState, key: u32, @@ -4598,8 +4931,7 @@ fn append_effective_control_device( let provenance = control_device_record_provenance(component_index, record_index, control); let mut coefficient_words = [0; 16]; coefficient_words.copy_from_slice(&descriptor.fixed_words[11..27]); - let coefficient_bits = - control_factory_uses_common_setter(descriptor.tag).then_some(coefficient_words); + let coefficient_bits = Some(coefficient_words); let (multiplier_bits, default_word_50) = native_control_factory_defaults(descriptor.tag); let native_byte_94 = native_control_factory_byte_94(descriptor.tag); let native_word_50_bits = if descriptor.fixed_words[6] == u32::MAX { @@ -4614,6 +4946,8 @@ fn append_effective_control_device( coefficient_bits, multiplier_bits, class9_selector0_scale_bits: (descriptor.tag == 9).then_some(0x3f80_0000), + class9_values_bits: (descriptor.tag == 9).then_some([0x3f80_0000; 6]), + class9_deltas_bits: (descriptor.tag == 9).then_some([0; 6]), native_word_50_bits, native_byte_94, descriptor: descriptor.clone(), @@ -4640,23 +4974,24 @@ fn apply_effective_control_override( control_device_record_provenance(component_index, record_index, control); } -fn control_factory_uses_common_setter(factory_tag: u32) -> bool { - matches!( - factory_tag, - 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 17 | 19 | 21 | 25 | 26 | 27 | 30 - ) +fn control_factory_uses_generic_budget_callbacks(factory_tag: u32) -> bool { + !matches!(factory_tag, 1 | 2 | 5 | 9 | 10 | 15 | 17 | 19 | 26 | 30) +} + +fn control_factory_override_is_supported(factory_tag: u32) -> bool { + !matches!(factory_tag, 1 | 17 | 26 | 30) } fn native_control_factory_defaults(factory_tag: u32) -> (Option, Option) { match factory_tag { 1 | 4 => (Some(0x3f80_0000), Some(0x200)), - 3 | 5 | 8 | 21 | 26 => (Some(0x3f80_0000), Some(0x5)), + 3 | 5 | 8 | 21 | 25 | 26 => (Some(0x3f80_0000), Some(0x5)), 10 => (Some(0x3f80_0000), Some(0x2000)), 17 | 19 => (Some(0x3f80_0000), Some(0x20)), 2 | 27 => (Some(0x3f80_0000), Some(0)), - 9 | 25 => (Some(0x3f80_0000), None), + 9 => (Some(0x3f80_0000), Some(0x20)), 15 => (Some(0x3f80_0000), Some(0)), - _ => (None, None), + _ => (Some(0x3f80_0000), Some(0x5)), } } @@ -5029,10 +5364,7 @@ fn build_control_device_projection( target_index, }, )?; - if !matches!( - device.factory_tag, - 2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 19 | 21 | 25 | 27 - ) { + if !control_factory_override_is_supported(device.factory_tag) { return Err(Unavailable::UnsupportedOverrideReceiver { factory_tag: device.factory_tag, }); @@ -5927,6 +6259,8 @@ mod tests { coefficient_bits: Some(coefficients), multiplier_bits, class9_selector0_scale_bits: class9_scale_bits, + class9_values_bits: (factory_tag == 9).then_some([0x3f80_0000; 6]), + class9_deltas_bits: (factory_tag == 9).then_some([0; 6]), native_word_50_bits, native_byte_94: native_control_factory_byte_94(factory_tag), descriptor, @@ -6079,6 +6413,201 @@ mod tests { } } + #[test] + fn control_budget_class9_device_matches_native_rate_and_update_vectors() { + let mut device = synthetic_effective_control_device( + 9, + 9, + 0.75_f32.to_bits(), + Some(0.25_f32.to_bits()), + Some(1.25_f32.to_bits()), + ); + device.key = 0; + device.native_word_50_bits = Some(0x20); + device.descriptor.fixed_words[8] = 0.2_f32.to_bits(); + let coefficients = device.coefficient_bits.as_mut().expect("coefficient table"); + coefficients[1] = 0.6_f32.to_bits(); + coefficients[2] = 2.0_f32.to_bits(); + device.class9_values_bits = Some([ + 0.0_f32.to_bits(), + 0.25_f32.to_bits(), + 0.5_f32.to_bits(), + 0.75_f32.to_bits(), + 1.0_f32.to_bits(), + 0.9_f32.to_bits(), + ]); + device.class9_deltas_bits = Some([ + 0.1_f32.to_bits(), + (-0.2_f32).to_bits(), + 0.0_f32.to_bits(), + 0.3_f32.to_bits(), + (-0.4_f32).to_bits(), + 0.5_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![device]), + MissionControlNodeProviderTableStatus::Available(vec![provider]), + ); + + let rate = { + let device = control_budget_device(&table, 0).expect("class-9 device"); + control_device_resource_rate(&table, device, 0.5).expect("class-9 rate") + }; + assert_eq!(rate.to_bits(), 0xbeae_147b); + control_device_resource_update(&mut table, 0, 0.5, 1.0).expect("class-9 update"); + + let MissionControlDeviceTableStatus::Available(devices) = &table.status else { + unreachable!(); + }; + assert_eq!(devices[0].multiplier_bits, Some(0x3f80_0000)); + assert_eq!( + devices[0].class9_values_bits, + Some([ + 0x3d49_a637, + 0x3e92_e795, + 0x3f06_4d32, + 0x3f43_2699, + 0x3f80_0000, + 0x3f67_a90a, + ]) + ); + assert_eq!( + devices[0].class9_deltas_bits, + Some([ + 0x3e18_cff4, + 0xbe26_fda3, + 0x3cc9_a637, + 0x3e9f_e6cc, + 0xbecc_cccd, + 0x3f01_42a4, + ]) + ); + + let mut decreasing = synthetic_effective_control_device( + 9, + 9, + 10.0_f32.to_bits(), + Some(1.0_f32.to_bits()), + Some(1.0_f32.to_bits()), + ); + decreasing.key = 0; + decreasing.native_word_50_bits = Some(0x20); + decreasing.descriptor.fixed_words[8] = 0.2_f32.to_bits(); + let coefficients = decreasing + .coefficient_bits + .as_mut() + .expect("coefficient table"); + coefficients[1] = 0.6_f32.to_bits(); + coefficients[2] = 2.0_f32.to_bits(); + decreasing.class9_values_bits = Some([0x3f80_0000; 6]); + decreasing.class9_deltas_bits = Some([0; 6]); + 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![decreasing]), + MissionControlNodeProviderTableStatus::Available(vec![provider]), + ); + let rate = { + let device = control_budget_device(&table, 0).expect("class-9 device"); + control_device_resource_rate(&table, device, 0.5).expect("class-9 rate") + }; + assert_eq!(rate.to_bits(), 0xbdcc_cccd); + control_device_resource_update(&mut table, 0, 0.5, 0.5).expect("class-9 negative update"); + let MissionControlDeviceTableStatus::Available(devices) = &table.status else { + unreachable!(); + }; + assert_eq!(devices[0].multiplier_bits, Some(0x3f00_0000)); + assert_eq!(devices[0].class9_values_bits, Some([0x3f7f_e4b1; 6])); + assert_eq!(devices[0].class9_deltas_bits, Some([0xb9da_740e; 6])); + } + + #[test] + fn control_class9_adjustment_matches_native_stored_average_factor() { + let mut device = synthetic_effective_control_device( + 9, + 9, + 1.0_f32.to_bits(), + Some(1.0_f32.to_bits()), + Some(1.0_f32.to_bits()), + ); + device.class9_values_bits = Some([ + 0, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000, + ]); + device.class9_deltas_bits = Some([0; 6]); + let mut table = + synthetic_control_device_table(MissionControlDeviceTableStatus::Available(vec![ + device, + ])); + + adjust_control_class9_values(&mut table, 0, 0.1).expect("class-9 adjustment"); + + let MissionControlDeviceTableStatus::Available(devices) = &table.status else { + unreachable!(); + }; + assert_eq!( + devices[0].class9_values_bits, + Some([ + 0x3f19_999c, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000, + 0x3f80_0000 + ]) + ); + assert_eq!( + devices[0].class9_deltas_bits, + Some([0x3f19_999c, 0, 0, 0, 0, 0]) + ); + } + + #[test] + fn class9_into_preserves_runtime_values_and_deltas() { + let mut device = synthetic_effective_control_device( + 9, + 9, + 1.0_f32.to_bits(), + Some(1.0_f32.to_bits()), + Some(1.0_f32.to_bits()), + ); + let values = [ + 0x3d49_a637, + 0x3e92_e795, + 0x3f06_4d32, + 0x3f43_2699, + 0x3f80_0000, + 0x3f67_a90a, + ]; + let deltas = [ + 0x3e18_cff4, + 0xbe26_fda3, + 0x3cc9_a637, + 0x3e9f_e6cc, + 0xbecc_cccd, + 0x3f01_42a4, + ]; + device.class9_values_bits = Some(values); + device.class9_deltas_bits = Some(deltas); + 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, 9); + assert_eq!(device.descriptor.tag, 19); + assert_eq!(device.class9_values_bits, Some(values)); + assert_eq!(device.class9_deltas_bits, Some(deltas)); + } + #[test] fn control_budget_dispatch_matches_non_degenerate_native_pair() { let mut table = control_budget_test_table(0.25); @@ -6110,6 +6639,65 @@ mod tests { assert_eq!(rows[0].current_accumulated_delta_bits, 0x3d2e_1400); } + #[test] + fn control_budget_generic_default_callbacks_match_native_rate_and_update() { + for factory_tag in [3, 4, 6, 8, 12, 13, 21, 25, 27, 34] { + let mut device = synthetic_effective_control_device( + factory_tag, + factory_tag, + 1.0_f32.to_bits(), + Some(1.0_f32.to_bits()), + None, + ); + device.key = 0; + device.native_word_50_bits = Some(5); + device.descriptor.fixed_words[8] = 0.2_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![device]), + MissionControlNodeProviderTableStatus::Available(vec![provider]), + ); + + let rate = { + let device = control_budget_device(&table, 0).expect("generic device"); + control_device_resource_rate(&table, device, 0.5).expect("generic base rate") + }; + assert_eq!( + rate.to_bits(), + (-0.1_f32).to_bits(), + "factory {factory_tag}" + ); + control_device_resource_update(&mut table, 0, 0.5, 0.375).expect("generic base update"); + let MissionControlDeviceTableStatus::Available(devices) = &table.status else { + unreachable!(); + }; + assert_eq!( + devices[0].multiplier_bits, + Some(0.375_f32.to_bits()), + "factory {factory_tag}" + ); + } + } + + #[test] + fn control_budget_current_tag_categories_match_native_prefix() { + let categories = [ + 0, 4, 4, 0, 2, 3, 0, 0, 2, 5, 5, 0, 0, 0, 1, 0, 3, 2, 0, 1, 3, 5, 4, 0, 4, 0, 3, 5, 0, + 0, 4, 0, 1, 2, 4, + ]; + for (tag, category) in categories.into_iter().enumerate() { + assert_eq!( + control_budget_category(tag as u32), + Ok(category), + "current descriptor tag {tag}" + ); + } + assert_eq!( + control_budget_category(35), + Err(ControlResourceAccessError::UnsupportedCurrentTag { tag: 35 }) + ); + } + #[test] fn control_budget_dispatch_keeps_callbacks_but_honors_owner_suspension() { let mut table = control_budget_test_table(1.0); @@ -6162,7 +6750,7 @@ mod tests { let mut table = control_budget_test_table(1.0); let unsupported = synthetic_effective_control_device( 26, - 26, + 35, 1.0_f32.to_bits(), Some(1.0_f32.to_bits()), None, @@ -6175,7 +6763,7 @@ mod tests { assert_eq!( control_resource_budget_pass(&mut table, 0.5), - Err(ControlResourceAccessError::UnsupportedCurrentTag { tag: 26 }) + Err(ControlResourceAccessError::UnsupportedCurrentTag { tag: 35 }) ); assert_eq!(table, before); } @@ -7657,9 +8245,14 @@ mod tests { (3, 5), (4, 0x200), (5, 5), + (6, 5), (8, 5), + (9, 0x20), (10, 0x2000), + (12, 5), + (13, 5), (21, 5), + (25, 5), (27, 0), ] { let mut root = synthetic_control_record(factory_tag, 2, 0x20, None); @@ -7697,15 +8290,22 @@ mod tests { assert_eq!(device.multiplier_bits, Some(1.0_f32.to_bits())); assert_eq!(device.native_word_50_bits, Some(word_50)); assert_eq!(device.coefficient_bits.unwrap()[0], 0x30); + if factory_tag == 9 { + assert_eq!(device.class9_values_bits, Some([0x3f80_0000; 6])); + assert_eq!(device.class9_deltas_bits, Some([0; 6])); + } + } + for factory_tag in [1, 17, 26, 30] { + assert!(!control_factory_override_is_supported(factory_tag)); } } #[test] - fn control_factory_multiplier_defaults_preserve_unknown_word_50() { - for factory_tag in [9, 25] { + fn control_factory_defaults_match_native_class_construction() { + for (factory_tag, word_50) in [(9, 0x20), (12, 5), (13, 5), (25, 5), (34, 5)] { assert_eq!( native_control_factory_defaults(factory_tag), - (Some(0x3f80_0000), None) + (Some(0x3f80_0000), Some(word_50)) ); } assert_eq!( @@ -8831,6 +9431,16 @@ mod tests { (21, 21), ] ); + assert_eq!(mine_control_devices[2].native_word_50_bits, Some(0x20)); + assert_eq!( + mine_control_devices[2].class9_selector0_scale_bits, + Some(0x3f80_0000) + ); + assert_eq!( + mine_control_devices[2].class9_values_bits, + Some([0x3f80_0000; 6]) + ); + assert_eq!(mine_control_devices[2].class9_deltas_bits, Some([0; 6])); assert_eq!( advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, f32::NAN), Err(ControlResourceAccessError::NonFiniteValue { @@ -8840,7 +9450,7 @@ mod tests { ); assert_eq!( advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, 0.5), - Err(ControlResourceAccessError::UnsupportedCurrentTag { tag: 9 }) + Err(ControlResourceAccessError::UnsupportedFactoryTag { factory_tag: 26 }) ); assert_eq!( loaded_mission_control_device_tables(&mine_engine), diff --git a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs index 457a6d6..55acd6f 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -54,6 +54,11 @@ internal static class BehaviorDistributorTickOracle { private const int ControlResourceTag9ConstructorRva = 0x25460; private const int ControlResourceTag15ConstructorRva = 0x22ae0; private const int ControlResourceTag25ConstructorRva = 0x20800; + private const int ControlResourceTag9RateRva = 0x25700; + private const int ControlResourceTag9UpdateRva = 0x257b0; + private const int ControlResourceTag9AdjustRva = 0x25a90; + private const int ControlResourceDefaultRateRva = 0x21860; + private const int ControlResourceDefaultUpdateRva = 0x21890; private const int ControlResourceTag15RateRva = 0x22b20; private const int ControlResourceTag15UpdateRva = 0x22bb0; private const int ControlResourceBudgetUpdateRva = 0x2d340; @@ -119,6 +124,7 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceDeviceConstructor(IntPtr self); [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 void NativeControlResourceTag9Update(IntPtr self, float deltaSeconds, float allocation); [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 IntPtr NativeControlDeviceFactory(IntPtr self, uint componentId, uint keyOffset, IntPtr record); @@ -706,6 +712,8 @@ internal static class BehaviorDistributorTickOracle { RunNativeControlTimedUpdateBaselineCases(controlModule); RunNativeControlAggregateResourceCases(controlModule); RunNativeControlResourceBudgetTag15Case(controlModule); + RunNativeControlResourceBudgetClass9Case(controlModule); + RunNativeControlGenericDefaultBudgetCase(controlModule); RunNativeControlActivationCase(controlModule); RunNativeBehaviorProfileMapCases(module); RunNativeBehaviorVarSyntheticCase(module); @@ -1790,6 +1798,198 @@ internal static class BehaviorDistributorTickOracle { return device; } + private static void RunNativeControlResourceBudgetClass9Case(IntPtr module) { + NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlOwnerConstructorRva), + typeof(NativeControlOwnerConstructor)); + NativeControlResourceDeviceConstructor constructDevice = (NativeControlResourceDeviceConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag9ConstructorRva), + typeof(NativeControlResourceDeviceConstructor)); + NativeControlProviderRowConstructor constructProvider = (NativeControlProviderRowConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva), + typeof(NativeControlProviderRowConstructor)); + NativeControlResourceDeviceRate rate = (NativeControlResourceDeviceRate) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag9RateRva), + typeof(NativeControlResourceDeviceRate)); + NativeControlResourceTag9Update update = (NativeControlResourceTag9Update) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag9UpdateRva), + typeof(NativeControlResourceTag9Update)); + NativeControlContactRateUpdate adjust = (NativeControlContactRateUpdate) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag9AdjustRva), + typeof(NativeControlContactRateUpdate)); + + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x664); + Expect(constructOwner(owner, IntPtr.Zero, 4) == owner, "native class-9 budget owner constructor"); + IntPtr ndp = fixture.Allocate(0x4c); + Marshal.WriteInt32(ndp, 4, FloatBits(100.0f)); + IntPtr provider = fixture.Allocate(0x2c); + constructProvider(provider, ndp); + Marshal.WriteInt32(provider, 0x14, FloatBits(0.4f)); + Marshal.WriteIntPtr(owner, 0x55c, provider); + Marshal.WriteInt32(owner, 0x560, 1); + + IntPtr device = fixture.Allocate(0x120); + Expect(constructDevice(device) == device && + Marshal.ReadIntPtr(device) == IntPtr.Add(module, ControlResourceTag9VtableRva), + "native class-9 constructor installs its original vtable"); + Expect(Marshal.ReadInt32(device, 0x50) == 0x20, + "native class-9 constructor enables its budget callback"); + for (int i = 0; i < 6; i++) { + Expect(Marshal.ReadInt32(device, 0x98 + i * 4) == FloatBits(1.0f) && + Marshal.ReadInt32(device, 0xb0 + i * 4) == 0, + "native class-9 constructor initializes value/delta row " + i); + } + IntPtr descriptor = fixture.Allocate(0xb4); + Marshal.WriteInt32(descriptor, 0, 9); + Marshal.WriteInt32(descriptor, 0x20, FloatBits(0.2f)); + Marshal.WriteIntPtr(device, 0x3c, owner); + Marshal.WriteIntPtr(device, 0x44, IntPtr.Add(owner, 4)); + Marshal.WriteIntPtr(device, 0x48, descriptor); + Marshal.WriteInt32(device, 0x54, FloatBits(0.75f)); + Marshal.WriteInt32(device, 0x58, FloatBits(0.6f)); + Marshal.WriteInt32(device, 0x5c, FloatBits(2.0f)); + Marshal.WriteInt32(device, 0x11c, FloatBits(1.25f)); + float[] values = { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f, 0.9f }; + float[] deltas = { 0.1f, -0.2f, 0.0f, 0.3f, -0.4f, 0.5f }; + for (int i = 0; i < 6; i++) { + Marshal.WriteInt32(device, 0x98 + i * 4, FloatBits(values[i])); + Marshal.WriteInt32(device, 0xb0 + i * 4, FloatBits(deltas[i])); + } + + Expect(FloatBits(rate(device, 0.5f)) == unchecked((int)0xbeae147b), + "native class-9 rate uses six values and selector-0 scale"); + update(device, 0.5f, 1.0f); + int[] expectedValues = { + unchecked((int)0x3d49a637), unchecked((int)0x3e92e795), + unchecked((int)0x3f064d32), unchecked((int)0x3f432699), + unchecked((int)0x3f800000), unchecked((int)0x3f67a90a), + }; + int[] expectedDeltas = { + unchecked((int)0x3e18cff4), unchecked((int)0xbe26fda3), + unchecked((int)0x3cc9a637), unchecked((int)0x3e9fe6cc), + unchecked((int)0xbecccccd), unchecked((int)0x3f0142a4), + }; + Expect(Marshal.ReadInt32(device, 0x4c) == FloatBits(1.0f), + "native class-9 update stores its allocation multiplier"); + for (int i = 0; i < 6; i++) { + Expect(Marshal.ReadInt32(device, 0x98 + i * 4) == expectedValues[i] && + Marshal.ReadInt32(device, 0xb0 + i * 4) == expectedDeltas[i], + "native class-9 exact value/delta vector row " + i); + } + Console.WriteLine("native-control-class9-budget: controlHash=" + ExpectedControlSha256 + + " rateRva=0x25700 updateRva=0x257b0 adjustRva=0x25a90 dt=0.5" + + " rate=0xbeae147b values=" + String.Join(",", Array.ConvertAll(expectedValues, + value => Hex(unchecked((uint)value)))) + + " deltas=" + String.Join(",", Array.ConvertAll(expectedDeltas, + value => Hex(unchecked((uint)value))))); + + IntPtr decreasingDevice = fixture.Allocate(0x120); + Expect(constructDevice(decreasingDevice) == decreasingDevice, + "native class-9 negative-update constructor"); + IntPtr decreasingDescriptor = fixture.Allocate(0xb4); + Marshal.WriteInt32(decreasingDescriptor, 0, 9); + Marshal.WriteInt32(decreasingDescriptor, 0x20, FloatBits(0.2f)); + Marshal.WriteIntPtr(decreasingDevice, 0x3c, owner); + Marshal.WriteIntPtr(decreasingDevice, 0x44, IntPtr.Add(owner, 4)); + Marshal.WriteIntPtr(decreasingDevice, 0x48, decreasingDescriptor); + Marshal.WriteInt32(decreasingDevice, 0x54, FloatBits(10.0f)); + Marshal.WriteInt32(decreasingDevice, 0x58, FloatBits(0.6f)); + Marshal.WriteInt32(decreasingDevice, 0x5c, FloatBits(2.0f)); + Marshal.WriteInt32(decreasingDevice, 0x11c, FloatBits(1.0f)); + Expect(FloatBits(rate(decreasingDevice, 0.5f)) == unchecked((int)0xbdcccccd), + "native class-9 negative case rate"); + update(decreasingDevice, 0.5f, 0.5f); + Expect(Marshal.ReadInt32(decreasingDevice, 0x4c) == FloatBits(0.5f), + "native class-9 negative case allocation multiplier"); + for (int i = 0; i < 6; i++) { + Expect(Marshal.ReadInt32(decreasingDevice, 0x98 + i * 4) == unchecked((int)0x3f7fe4b1) && + Marshal.ReadInt32(decreasingDevice, 0xb0 + i * 4) == unchecked((int)0xb9da740e), + "native class-9 negative value/delta row " + i); + } + Console.WriteLine("native-control-class9-negative: rate=0xbdcccccd M=0x3f000000" + + " values=0x3f7fe4b1x6 deltas=0xb9da740ex6"); + + IntPtr averageDevice = fixture.Allocate(0x120); + Expect(constructDevice(averageDevice) == averageDevice, "native class-9 average constructor"); + for (int i = 0; i < 6; i++) { + Marshal.WriteInt32(averageDevice, 0x98 + i * 4, FloatBits(i == 0 ? 0.0f : 1.0f)); + Marshal.WriteInt32(averageDevice, 0xb0 + i * 4, 0); + } + adjust(averageDevice, 0.1f); + Expect(Marshal.ReadInt32(averageDevice, 0x98) == unchecked((int)0x3f19999c) && + Marshal.ReadInt32(averageDevice, 0xb0) == unchecked((int)0x3f19999c), + "native class-9 adjustment multiplies by the stored 1/6 constant"); + for (int i = 1; i < 6; i++) { + Expect(Marshal.ReadInt32(averageDevice, 0x98 + i * 4) == FloatBits(1.0f) && + Marshal.ReadInt32(averageDevice, 0xb0 + i * 4) == 0, + "native class-9 constant-average leaves full rows unchanged"); + } + Console.WriteLine("native-control-class9-average: first=0x3f19999c delta=0x3f19999c"); + } + GC.KeepAlive(rate); + GC.KeepAlive(update); + GC.KeepAlive(adjust); + } + + private static void RunNativeControlGenericDefaultBudgetCase(IntPtr module) { + NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlOwnerConstructorRva), + typeof(NativeControlOwnerConstructor)); + NativeControlResourceDeviceConstructor constructDefault = (NativeControlResourceDeviceConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag25ConstructorRva), + typeof(NativeControlResourceDeviceConstructor)); + NativeControlResourceDeviceRate rate = (NativeControlResourceDeviceRate) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceDefaultRateRva), + typeof(NativeControlResourceDeviceRate)); + NativeControlResourceTag9Update update = (NativeControlResourceTag9Update) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceDefaultUpdateRva), + typeof(NativeControlResourceTag9Update)); + NativeControlProviderRowConstructor constructProvider = (NativeControlProviderRowConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva), + typeof(NativeControlProviderRowConstructor)); + + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x664); + Expect(constructOwner(owner, IntPtr.Zero, 4) == owner, + "native generic-budget owner constructor"); + IntPtr ndp = fixture.Allocate(0x4c); + Marshal.WriteInt32(ndp, 4, FloatBits(100.0f)); + IntPtr provider = fixture.Allocate(0x2c); + constructProvider(provider, ndp); + Marshal.WriteInt32(provider, 0x14, FloatBits(0.4f)); + Marshal.WriteIntPtr(owner, 0x55c, provider); + Marshal.WriteInt32(owner, 0x560, 1); + + IntPtr device = fixture.Allocate(0xa4); + Expect(constructDefault(device) == device, "native generic default-family constructor"); + IntPtr vtable = Marshal.ReadIntPtr(device); + Expect(vtable == IntPtr.Add(module, ControlResourceTag25VtableRva) && + Marshal.ReadIntPtr(vtable, 0x14) == IntPtr.Add(module, ControlResourceDefaultRateRva) && + Marshal.ReadIntPtr(vtable, 0x18) == IntPtr.Add(module, ControlResourceDefaultUpdateRva), + "generic default-family vtable installs native rate/update callbacks"); + Expect(Marshal.ReadInt32(device, 0x50) == 5 && + Marshal.ReadInt32(device, 0x4c) == FloatBits(1.0f), + "generic default-family constructor defaults"); + IntPtr descriptor = fixture.Allocate(0xb4); + Marshal.WriteInt32(descriptor, 0, 12); + Marshal.WriteInt32(descriptor, 0x20, FloatBits(0.2f)); + Marshal.WriteIntPtr(device, 0x3c, owner); + Marshal.WriteIntPtr(device, 0x44, IntPtr.Add(owner, 4)); + Marshal.WriteIntPtr(device, 0x48, descriptor); + Expect(FloatBits(rate(device, 0.5f)) == FloatBits(-0.1f), + "generic native callback rate is negative descriptor rate times dt"); + update(device, 0.5f, 0.375f); + Expect(Marshal.ReadInt32(device, 0x4c) == FloatBits(0.375f), + "generic native callback stores allocation multiplier"); + Console.WriteLine("native-control-generic-default-budget: controlHash=" + ExpectedControlSha256 + + " constructorRva=0x20800 vtable=0x3c448 rateRva=0x21860 updateRva=0x21890" + + " descriptor=12 B=0.2 dt=0.5 rate=-0.1 allocation=0.375"); + } + GC.KeepAlive(rate); + GC.KeepAlive(update); + } + private static void RunNativeControlResourceBudgetTag15Case(IntPtr module) { RunNativeControlResourceBudgetEmptyCases(module); NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer( @@ -2533,6 +2733,10 @@ internal static class BehaviorDistributorTickOracle { 0x3cbd8, 0x29940, applyOverride); RunNativeControlOverrideReceiverCase(module, 3, 0xa4, 0x20800, 0x3c448, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 12, 0xa4, 0x20800, + 0x3c448, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 13, 0xa4, 0x20800, + 0x3c448, ControlResourceNoOpStringPairRva, applyOverride); GC.KeepAlive(applyOverride); } @@ -2546,14 +2750,21 @@ internal static class BehaviorDistributorTickOracle { "tag-" + tag + " vtable reader resolves to its native implementation"); Expect(Marshal.ReadIntPtr(expectedVtable, 0x24) == IntPtr.Add(module, stringPairRva), "tag-" + tag + " vtable string-pair slot matches its native callback"); + if (vtableRva == 0x3c448 || vtableRva == 0x3c720 || + vtableRva == 0x3c800 || vtableRva == 0x3c528) { + Expect(Marshal.ReadIntPtr(expectedVtable, 0x14) == IntPtr.Add(module, ControlResourceDefaultRateRva) && + Marshal.ReadIntPtr(expectedVtable, 0x18) == IntPtr.Add(module, ControlResourceDefaultUpdateRva), + "tag-" + tag + " vtable uses the shared default budget callbacks"); + } using (Fixture fixture = NewFixture()) { IntPtr owner = fixture.Allocate(0x5c8); IntPtr deviceArray = fixture.Allocate(2 * IntPtr.Size); - IntPtr targetDevice = (tag == 3 || tag == 5) ? IntPtr.Zero : fixture.Allocate(objectSize); + bool defaultFactory = tag == 3 || tag == 5 || tag == 12 || tag == 13; + IntPtr targetDevice = defaultFactory ? IntPtr.Zero : fixture.Allocate(objectSize); bool variableTail = tag == 2 || tag == 3 || tag == 4 || tag == 5 || tag == 8 || tag == 10 || tag == 21 || tag == 27; IntPtr typedRecord = fixture.Allocate(variableTail ? 0xc0 : 0xb4); - if (tag == 3 || tag == 5) { - // Tag 3 follows the default factory; case 5 also assigns a derived vtable. + if (defaultFactory) { + // Tags 3/12/13 follow the default factory; case 5 assigns a derived vtable. Marshal.WriteIntPtr(owner, 0x5bc, deviceArray); Marshal.WriteInt32(owner, 0x5c4, 2); Marshal.WriteInt32(typedRecord, 0, tag); @@ -2573,6 +2784,10 @@ internal static class BehaviorDistributorTickOracle { Marshal.ReadInt32(targetDevice, 0x4c) == FloatBits(1.0f), "native extended factory constructor defaults"); } + if (tag == 12 || tag == 13) + Expect(Marshal.ReadInt32(targetDevice, 0x50) == 5 && + Marshal.ReadInt32(targetDevice, 0x4c) == FloatBits(1.0f), + "native default-factory tag " + tag + " constructor defaults"); Marshal.WriteIntPtr(deviceArray, 0, targetDevice); Marshal.WriteIntPtr(owner, 0x5bc, deviceArray); Marshal.WriteInt32(owner, 0x5c0, 1); diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index dcc14fe..626469f 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -360,13 +360,16 @@ API `distribute_loaded_mission_control_provider_amount` переносит Три API работают только с CPU-проекцией и не подключены к GPU/window path или игровому Control clock. Отдельный -`advance_loaded_mission_control_budget` переносит `0x2d340` для factory -callbacks `15/19` и current descriptor tags `15/19` (категории `0/1`); ставки сохраняются -в текущих полях `+0x5dc/+0x5e0`. Он выполняется транзакционно, а неизвестный -callback, provider или диапазон числа возвращает ошибку без частичного +`advance_loaded_mission_control_budget` переносит `0x2d340` для фабричных +callbacks `9`, `15`, `19` и общей default-пары; category lookup использует +текущий descriptor tag из подтверждённого префикса `0..=34`. Ставки сохраняются +в `+0x5dc/+0x5e0`. Шаг выполняется транзакционно, а неподдержанный +специализированный callback, provider или диапазон числа возвращает ошибку без частичного изменения. Тест с нулевым знаменателем генератора подтверждает откат после уже выполненного изменения потребителем. Пустая таблица всё равно записывает в оба поля отрицательный ноль; -неактивный callback не требует provider lookup. Gate `+0x618` изначально +неактивный callback не требует provider lookup. Class-9 constructor начинает +шесть значений `+0x98..+0xac` с `1.0` и шесть дельт `+0xb0..+0xc4` с нуля; +`INTO` сохраняет эти runtime-массивы. Gate `+0x618` изначально выключен; событие `7` задаёт его по правилу `gate = (arg == 2)` и вызывает `0x2dc20`, только если gate изменился. События Control в Rust пока не исполняются, поэтому ресурсный шаг вызывается явно и не подключён к часам. @@ -418,13 +421,16 @@ rate, изменение provider и собственного множителя подтверждают текущие ставки `+0x5dc/+0x5e0`, включая значения `0.2`/`0.4` у потребителя tag 15, пустой результат `-0.0` и невырожденную пару исходных устройств tag 19/tag 15 при `dt = 0.375`; Rust-тест повторяет поля этой пары -на синтетической таблице. Реализация принимает только описанные factory и -category tags; остальные callbacks и состояния остаются явными ошибками. +на синтетической таблице. Rust переносит class-9 rate/update и шестизначную +adjustment `0x25a90`, общую default callback-пару `0x21860/0x21890`, а также +category map только для current descriptor tags `0..=34`. Специализированные +factory callbacks вне этого набора остаются явными ошибками. Лицензированный `min_l_n1.dat` содержит исходные factory/current tags `[19, 19, 9, 15, 12, 12, 13, 13, 26, 25, 21]`. Публичный API оставляет всю -таблицу без изменений при не конечном интервале и при первом неподдержанном -current tag `9`; полный бюджетный проход для этой реальной таблицы поэтому не -проверен. Успешные бюджетные native-векторы используют синтетические +таблицу без изменений при не конечном интервале. Для конечного интервала +предварительная проверка распознаёт категории, но находит неподдержанный +factory tag `26` и отклоняет вызов до вычисления ставок и обновлений; полный +бюджетный проход для этой реальной таблицы поэтому не проверен. Успешные бюджетные native-векторы используют синтетические NDP/Node38-строки и подтверждают локальные переходы и порядок вызовов, а не полный Control tick на миссии. Прямые oracle-вызовы `0x11220` для одиночной строки используют `updateEffects = false`, что исключает ветвь Control FX и diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index a362b73..5fb0cd0 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -792,21 +792,23 @@ provider getter и изменение количества не подменен Rust предоставляет явный CPU-вызов `advance_loaded_mission_control_budget(engine, handle, dt)`. Он переносит -диспетчер и callbacks исходных factory tags `15` и `19`, если текущий -descriptor tag равен `15` или `19` (native categories `0` или `1`). Rates `+0x5dc/+0x5e0` -сохраняются отдельно. Вызов работает на копии полного состояния: неизвестный -descriptor, factory callback, provider или численный диапазон возвращает -ошибку без публикации частичных изменений. Вызов ещё не включён в Control clock; -нативный oracle также проверяет пустую таблицу с отрицательным нулём в обеих -выходных ставках и невырожденную пару исходных устройств tag 19/tag 15 при -`dt = 0.375`. Этот парный вектор проверяет изменения amount, processed amount, -накопителя, доли, обоих множителей и ставок `+0x5dc/+0x5e0` по битам. Rust-тест -повторяет его на синтетической provider-таблице. Для лицензированного -`min_l_n1.dat` отдельный тест подтверждает состав factory/current tags и то, что -первый неподдержанный current tag `9` отклоняет весь шаг без изменений; полного -успешного бюджетного прохода для реальной миссионной таблицы пока нет. -Rust также отклоняет нулевой знаменатель обновления tag 19; тест вызывает эту -ошибку после изменения provider потребителем и подтверждает откат всей таблицы. +исходные callbacks factory tags `9`, `15`, `19` и общий default callback, а +категории выбирает по текущему descriptor tag. Native map подтверждён только +для префикса тегов `0..=34`; неизвестная категория, специализированный +callback, provider или численный диапазон возвращает ошибку без публикации +частичных изменений. Ставки `+0x5dc/+0x5e0` сохраняются отдельно, а сам вызов +ещё не включён в Control clock. + +Нативный oracle проверяет пустую таблицу с отрицательным нулём в обеих выходных +ставках, невырожденную пару исходных устройств tag 19/tag 15 при `dt = 0.375` +и tag 9 с шестью значениями и шестью накопителями изменений. Rust-тесты +повторяют их по битам, включая положительное и отрицательное обновления tag 9. +В лицензированном `min_l_n1.dat` предварительная проверка распознаёт категории, +но находит неподдержанный factory tag `26` и отклоняет бюджет до расчёта ставок +или вызова устройств; транзакционный тест подтверждает, что исходная таблица +не меняется. Полный бюджетный проход на реальной миссии пока не выполнен. Rust также отклоняет нулевой знаменатель +обновления tag 19; тест вызывает эту ошибку после изменения provider +потребителем и подтверждает откат всей таблицы. #### Генератор tag 19 в том же бюджетном проходе @@ -846,6 +848,74 @@ tag `15`: смысл изменения этого поля зависит от Поддержанные Rust-векторы не означают, что любой Control descriptor или полный миссионный цикл уже работает. +#### Класс 9: ставка и шесть изменяемых значений + +У `Control.dll` SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +класс `9` использует constructor RVA `0x25460`, vtable `0x3c938`, rate +`0x25700`, update `0x257b0` и adjustment `0x25a90`. Constructor начинает шесть +значений в `+0x98..+0xac` с `1.0`, а шесть накопителей в `+0xb0..+0xc4` — с +нуля. Текущий `INTO` заменяет descriptor и общие коэффициенты, сохраняя эти +runtime-массивы. + +Для rate `q` — текущее значение provider, `B` — descriptor `+0x20`, `C0..C2` — +коэффициенты, а `scale` — class-9 поле `+0x11c`. Сначала отдельно округляются +`G0 = C0 * scale`, сумма шести значений, и свободная величина `6 - sum`; затем +вычисляются доступная часть `(6 - sum) * G0` и запрос `(C1 * q) * dt`. Rate +равна `-(dt * B) - C2 * min(доступная часть, запрос)`. В update множитель +`+0x4c` сначала получает allocation, затем вычисляется +`S = -(rate(dt) * allocation) - (dt * B)`. При `C2 > 0` изменение равно +`S / C2`; иначе его знак выбирает `±((C1 * q) * dt)`. Если `+0x618` не +приостанавливает обновление, а `G0 > 0`, изменение делится на `G0 * 6` и +передаётся `0x25a90`. + +Adjustment умножает упорядоченную сумму шести значений на единственную +binary32-константу `0x3e2aaaab` — приблизительное `1/6`. Дополнительного деления +на шесть нет. При положительном изменении native ограничивает фактор сверху +единицей и приближает каждое значение к `1`; при отрицательном — уменьшает +каждое пропорционально ему. Каждый фактический шаг также прибавляется к своему +накопителю. Oracle запускает исходные entry points на оригинальном Control.dll +при x87 control word `0x007f`: + +| Вектор | Вход | Rate / multiplier | Шесть выходных значений | Шесть выходных дельт | +|---|---|---|---|---| +| Уменьшение | `q=.4, dt=.5, B=.2, C0=10, scale=1, C1=.6, C2=2`, allocation `.5`; все значения `1`, дельты `0` | `0xbdcccccd`, `0x3f000000` | `0x3f7fe4b1` во всех строках | `0xb9da740e` во всех строках | +| Смешанный набор | `q=.4, dt=.5, B=.2, C0=.75, scale=1.25, C1=.6, C2=2`, allocation `1`; значения `[0,.25,.5,.75,1,.9]`, дельты `[.1,-.2,0,.3,-.4,.5]` | `0xbeae147b`, `0x3f800000` | `[3d49a637,3e92e795,3f064d32,3f432699,3f800000,3f67a90a]` | `[3e18cff4,be26fda3,3cc9a637,3e9fe6cc,becccccd,3f0142a4]` | + +Отдельный прямой вызов `0x25a90` для значения `[0,1,1,1,1,1]`, нулевых +накопителей и добавки `.1` записывает `0x3f19999c` в первую пару value/delta; +остальные пять строк остаются неизменными. Этот vector фиксирует умножение на +сохранённую константу, а не вычисление `sum / 6`. Rust хранит оба массива +отдельно и сохраняет их при descriptor override; ошибки численного диапазона +не публикуют частичный результат. + +#### Общие default budget callbacks и категории + +Ветка фабрики Control RVA `0x2d4b0` направляет обычные классы в constructor +`0x20800`; он устанавливает vtable `0x3c448` и word `+0x50 = 5`. Rate и update +этого семейства — исходные `0x21860` и `0x21890`: первая возвращает +`-(dt * descriptor[+0x20])`, второй записывает allocation в `+0x4c` после +общей проверки активности provider. Фабричные теги `12`, `13` и `25` идут по +ветке default; tag `21` явно выбирает ту же пару. У tags `4`, `8` и `27` свои +vtable/constructor, но их rate/update entries указывают на ту же пару. Native +oracle проверяет factory routing тегов `12/13`, vtable pointers и прямой +вызов rate/update общей пары. Rust повторяет формулу и сохраняет исходные +word-50 defaults, в частности `0x200` у тега `4` и ноль у тега `27`. + +Descriptor category lookup `0x2c3a0` возвращает следующие значения для +проверенного префикса `0..=34`: + +```text +[0,4,4,0,2,3,0,0,2,5,5,0,0,0,1,0,3,2,0,1,3,5,4,0,4,0,3,5,0,0,4,0,1,2,4] +``` + +Именно текущий descriptor выбирает категорию; callback остаётся у исходного +factory-класса. В Rust неизвестный текущий тег за концом префикса и +специализированный factory callback не подменяются общей формулой. На +лицензированном Unit DAT полный проход доходит до factory `26` и возвращает +ошибку без частичного изменения таблицы. Ни этот CPU-вызов, ни локальные +векторы не означают включения бюджетного шага в Control clock. + #### Активация и начальные часы Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490` diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 6406307..4e71ed9 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -725,6 +725,14 @@ component ID и multiplier `2.5` при копировании новых коэ и дополнительная строка `fx` возвращают cursor `record + 0xbf`. Слово CTLD `+0x18 == -1` сохраняет фабричное `+0x50` и при INTO-замене. +Ветвь фабрики `0x2d4b0` направляет factory tags без отдельного случая в +конструктор `0x20800`; его vtable использует общий reader `0x21d50` в slot +`+0x1c`. Поэтому common-field override применяется и к этой default-семье, +включая tag `6`; Rust-тест проверяет его вместе с тегами `12` и `13`. Tags +`1`, `17`, `26` и `30` остаются явными границами: фабрика выбирает для них +отдельные классы, а проверки текущей реализации ещё не покрывают их полное +setter-поведение. + У tag `2` slot `+0x1c` указывает на обёртку `0x29650`. Она вызывает тот же reader, затем сохраняет selector `1` в `+0x120`, округляет selector `0` через x87 `FISTP` в `+0x124` и заново обнуляет массив `+0x12c` по накопленному числу @@ -1061,15 +1069,25 @@ Rust переносит этот порядок, сохраняет native на в массиве, его обновление перечитывает уже изменённую долю после обслуживания категории tag 15. Этот явный вызов хранит также две выходные ставки `+0x5dc` и `+0x5e0`, но сам по себе ещё не запускается игровыми часами. -Поддержаны factory callbacks `15/19` и текущие descriptor tags `15/19` -(категории `0/1`); неизвестное устройство, -недоступные данные provider или нулевой знаменатель останавливают весь шаг без -частичных изменений. -Другие категории и полный миссионный цикл пока не подтверждены. В лицензированном -`min_l_n1.dat` рядом с тегами `19` и `15` есть также factory tags `9`, `12`, -`13`, `21`, `25` и `26`; поэтому его полный проход пока явно отклоняется на -первой неподдержанной категории `9`. Проверка сохраняет всю таблицу без -изменений и не доказывает успешный бюджетный шаг на реальной миссии. +Помимо класса `15` и генератора `19` поддержан класс `9`: он считает ставку +через шесть собственных значений и при распределении обновляет их вместе с +накопленными изменениями. Например, для доли provider `0.4`, интервала `0.5`, +`B = 0.2`, коэффициентов `C0 = 0.75`, `C1 = 0.6`, `C2 = 2` и масштаба +`1.25` исходная ставка имеет биты `0xbeae147b`; после полного выделения +обновлённые шесть значений совпадают с native побитно. + +Несколько фабричных классов используют общий callback: ставка равна +`-(dt * B)`, а обновление записывает долю выделения в множитель. К этому +семейству относятся фабричные теги `12`, `13`, `25` и `21`, а также другие +классы, которые исходный диспетчер направляет в фабрику по умолчанию. Текущая +категория читается из descriptor: её native map подтверждён только для тегов +`0..=34`. Неподдержанный специализированный callback, недоступный provider или +неподдержанное число останавливают шаг без частичных изменений. В +лицензированном `min_l_n1.dat` предварительная проверка распознаёт текущие +категории и выявляет неподдержанный factory tag `26`; она отклоняет бюджетный +вызов до расчёта ставок и обновлений, сохраняя таблицу целиком. Полный +бюджетный проход на реальной миссии и подключение к игровым часам пока не +подтверждены. Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,