diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 44b6a67..d3ab573 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -201,6 +201,8 @@ pub struct EffectiveControlDevice { pub multiplier_bits: Option, /// Class-9-only scale used by its selector-0 getter. pub class9_selector0_scale_bits: Option, + /// Factory-26 mutable resource-rate addition at native `+0xa4`. + pub tag26_extra_rate_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`. @@ -1973,8 +1975,9 @@ pub fn query_loaded_mission_control_resource( /// Queries one loaded Control device through its original factory getter. /// /// Selector `0xe00` is currently modeled for current descriptor tag 26 with -/// factory tag 26. The loaded NDP provider value is its initial native state; -/// later Control updates are not synchronized into this snapshot. +/// factory tag 26. The query reads the current provider, coefficient, and +/// multiplier in the CPU projection. Explicit Control update calls can change +/// those values; the mission clock does not advance them automatically. /// /// # Errors /// 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) } +/// 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 { + 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`. /// /// Selector `0x1005` returns the loaded profile's `Transfer_Power_Out` value. /// Selector `0x1002` sums supported Control `0xe00` results in native table /// order, rounding each addition to `f32`, then multiplies by the profile's /// `Transfer_Ore_OffBoard` value. Unsupported table/query paths return an error. -/// Profile and provider values are initial full-load snapshots; later damage, -/// Control updates, and profile changes are not synchronized into this state. +/// Profile values come from full mission loading, while `0xe00` reads the +/// current CPU-projected provider and device state. Explicit Control updates +/// can therefore change `0x1002`; the mission clock does not update it +/// automatically. /// /// # Errors /// 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`). /// -/// 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 /// 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 @@ -2929,7 +2964,7 @@ 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, 9 | 15 | 19) + if !matches!(device.factory_tag, 9 | 15 | 19 | 26) && !control_factory_uses_generic_budget_callbacks(device.factory_tag) { return Err(ControlResourceAccessError::UnsupportedFactoryTag { @@ -3289,6 +3324,22 @@ fn control_device_resource_rate( -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 => { let coefficient_0 = control_device_budget_coefficient(device, 0)?; 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) => { set_control_device_multiplier(table, device_index, allocation)?; } @@ -4066,6 +4118,50 @@ fn query_control_device_table_selector( .map(Some) } +fn set_control_device_table_selector( + table: &mut MissionControlDeviceTableState, + device_index: usize, + selector: u32, + value: f32, +) -> Result { + 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( table: &MissionControlDeviceTableState, transfer_ore_off_board_bits: u32, @@ -4946,6 +5042,7 @@ fn append_effective_control_device( coefficient_bits, multiplier_bits, 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_deltas_bits: (descriptor.tag == 9).then_some([0; 6]), 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 { - !matches!(factory_tag, 1 | 17 | 26 | 30) + !matches!(factory_tag, 1 | 17 | 30) } fn native_control_factory_defaults(factory_tag: u32) -> (Option, Option) { @@ -6259,6 +6356,7 @@ mod tests { coefficient_bits: Some(coefficients), multiplier_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_deltas_bits: (factory_tag == 9).then_some([0; 6]), 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] fn control_budget_current_tag_categories_match_native_prefix() { let categories = [ @@ -7259,6 +7613,10 @@ mod tests { query_loaded_mission_control_device_selector(&engine, handle, 0, 0xe00), Err(ControlResourceAccessError::MissionUnavailable) ); + assert_eq!( + set_loaded_mission_control_device_selector(&mut engine, handle, 0, 0x500, 0.25), + Err(ControlResourceAccessError::MissionUnavailable) + ); assert_eq!( query_loaded_mission_behavior_resource(&engine, handle, 0x1005), Err(ControlResourceAccessError::MissionUnavailable) @@ -7318,6 +7676,12 @@ mod tests { 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!( query_loaded_mission_control_resource(&engine, stale, 1), Err(ControlResourceAccessError::World( @@ -7336,6 +7700,12 @@ mod tests { 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!( query_loaded_mission_behavior_resource(&engine, stale, 0x1005), Err(ControlResourceAccessError::World( @@ -8253,6 +8623,7 @@ mod tests { (13, 5), (21, 5), (25, 5), + (26, 5), (27, 0), ] { 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.native_word_50_bits, Some(word_50)); 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 { 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] { + for factory_tag in [1, 17, 30] { assert!(!control_factory_override_is_supported(factory_tag)); } } @@ -9441,6 +9815,7 @@ mod tests { Some([0x3f80_0000; 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!( advance_loaded_mission_control_budget(&mut mine_engine, mine_control_handle, f32::NAN), Err(ControlResourceAccessError::NonFiniteValue { @@ -9448,14 +9823,38 @@ mod tests { 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!( loaded_mission_control_device_tables(&mine_engine), 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::>(); + 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]; assert_eq!(draft.unit_components.len(), 6); 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 55acd6f..d92c46a 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -53,6 +53,10 @@ internal static class BehaviorDistributorTickOracle { private const int ControlResourceCommonSetterRva = 0x21d50; private const int ControlResourceTag9ConstructorRva = 0x25460; 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 ControlResourceTag9RateRva = 0x25700; private const int ControlResourceTag9UpdateRva = 0x257b0; @@ -68,6 +72,7 @@ internal static class BehaviorDistributorTickOracle { private const int ControlResourceTag15GateRva = 0x21820; private const int ControlResourceTag9VtableRva = 0x3c938; private const int ControlResourceTag15VtableRva = 0x3c648; + private const int ControlResourceTag26VtableRva = 0x3ce38; private const int ControlResourceTag25VtableRva = 0x3c448; private const int ControlResourceTag9StringPairRva = 0x25600; 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 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 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 float NativeControlResourceAvailability(IntPtr self, byte flags); [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); RunNativeControlAggregateResourceCases(controlModule); RunNativeControlResourceBudgetTag15Case(controlModule); + RunNativeControlResourceBudgetTag26Case(controlModule); RunNativeControlResourceBudgetClass9Case(controlModule); RunNativeControlGenericDefaultBudgetCase(controlModule); RunNativeControlActivationCase(controlModule); @@ -2362,6 +2370,168 @@ internal static class BehaviorDistributorTickOracle { 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) { NativeControlOwnerConstructor construct = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer( IntPtr.Add(module, 0x31490), typeof(NativeControlOwnerConstructor)); @@ -3692,6 +3862,17 @@ internal static class BehaviorDistributorTickOracle { Marshal.Copy(code, 0, thunk, code.Length); 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) { byte[] code = { 0xd9, 0x3d, 0, 0, 0, 0, 0xc3 }; // fnstcw [absolute address]; ret WriteImm32(code, 2, x87ControlWordCell.ToInt32()); diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 626469f..2f93502 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -361,7 +361,7 @@ API `distribute_loaded_mission_control_provider_amount` переносит Три API работают только с CPU-проекцией и не подключены к GPU/window path или игровому Control clock. Отдельный `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`. Ставки сохраняются в `+0x5dc/+0x5e0`. Шаг выполняется транзакционно, а неподдержанный специализированный callback, provider или диапазон числа возвращает ошибку без частичного @@ -417,20 +417,30 @@ visibility transition `0x11920` с исходными AniMesh callbacks. Отд в этом векторе подставлен mock-ом. Отдельный вектор с исходным генератором tag `19` (`0x22980`, `0x229a0`, `0x22a60`) проверяет полную и ограниченную 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` у потребителя tag 15, пустой результат `-0.0` и невырожденную пару исходных устройств tag 19/tag 15 при `dt = 0.375`; Rust-тест повторяет поля этой пары на синтетической таблице. Rust переносит class-9 rate/update и шестизначную -adjustment `0x25a90`, общую default callback-пару `0x21860/0x21890`, а также -category map только для current descriptor tags `0..=34`. Специализированные -factory callbacks вне этого набора остаются явными ошибками. +adjustment `0x25a90`, tag-26 rate/update и setter selector `0x500`, общую +default callback-пару `0x21860/0x21890`, а также category map только для current +descriptor tags `0..=34`. Остальные ещё не описанные специализированные +callbacks остаются явными ошибками. Лицензированный `min_l_n1.dat` содержит исходные factory/current tags `[19, 19, 9, 15, 12, 12, 13, 13, 26, 25, 21]`. Публичный API оставляет всю таблицу без изменений при не конечном интервале. Для конечного интервала -предварительная проверка распознаёт категории, но находит неподдержанный -factory tag `26` и отклоняет вызов до вычисления ставок и обновлений; полный -бюджетный проход для этой реальной таблицы поэтому не проверен. Успешные бюджетные native-векторы используют синтетические +ignored Rust-тест проходит budget по всем 11 строкам без фильтра. При `dt = 0.5` +обе output rates равны `0x3c23d70a`, первые два множителя tag `19` равны +`0x3f7fd70a`, остальные множители остаются `1.0`, а добавка tag `26` остаётся +нулевой. Эта проверка подтверждает только CPU-проекцию реальной таблицы; она +не запускает native полный mission tick и не подключает Control clock. +Успешные бюджетные 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 5fb0cd0..84bb4b9 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -792,23 +792,19 @@ provider getter и изменение количества не подменен Rust предоставляет явный CPU-вызов `advance_loaded_mission_control_budget(engine, handle, dt)`. Он переносит -исходные callbacks factory tags `9`, `15`, `19` и общий default callback, а -категории выбирает по текущему descriptor tag. Native map подтверждён только -для префикса тегов `0..=34`; неизвестная категория, специализированный -callback, provider или численный диапазон возвращает ошибку без публикации -частичных изменений. Ставки `+0x5dc/+0x5e0` сохраняются отдельно, а сам вызов -ещё не включён в Control clock. +исходные callbacks factory tags `9`, `15`, `19`, `26` и общий 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 -потребителем и подтверждает откат всей таблицы. +Rust также отклоняет нулевой знаменатель обновления tag 19; тест вызывает эту +ошибку после изменения provider потребителем и подтверждает откат всей таблицы. #### Генератор tag 19 в том же бюджетном проходе @@ -911,10 +907,51 @@ Descriptor category lookup `0x2c3a0` возвращает следующие з Именно текущий descriptor выбирает категорию; callback остаётся у исходного factory-класса. В Rust неизвестный текущий тег за концом префикса и -специализированный factory callback не подменяются общей формулой. На -лицензированном Unit DAT полный проход доходит до factory `26` и возвращает -ошибку без частичного изменения таблицы. Ни этот CPU-вызов, ни локальные -векторы не означают включения бюджетного шага в Control clock. +неподдержанный специализированный factory callback не подменяются общей +формулой. Tag `26` поддержан отдельной парой callbacks, описанной ниже. Ни этот +CPU-вызов, ни локальные векторы не означают включения бюджетного шага в Control +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-проекции реальной таблицы, а не сравнение целой миссии с +оригиналом. #### Активация и начальные часы diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 4e71ed9..d3af8f7 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -1082,12 +1082,31 @@ Rust переносит этот порядок, сохраняет native на классы, которые исходный диспетчер направляет в фабрику по умолчанию. Текущая категория читается из descriptor: её native map подтверждён только для тегов `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` умножает интервал на контактный коэффициент и передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,