From ccb5af0e37b94942962004ea2e84eea1d519c4bb Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 22 Oct 2026 12:20:42 +0400 Subject: feat(assets): sample native resource device coefficient --- crates/fparkan-assets/src/lib.rs | 332 +++++++++++++++++++++ .../behavior_distributor_tick_native_oracle.cs | 96 +++++- 2 files changed, 422 insertions(+), 6 deletions(-) (limited to 'crates') diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index d34effb..da3411e 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -1180,6 +1180,19 @@ impl Default for ControlTag21InstanceState { /// Input or metadata prevented a safe native control sample. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ControlSamplingError { + /// This resource-device helper only implements typed-record tag 26. + UnsupportedResourceDeviceTag { actual: u32 }, + /// The record's raw provider key has no resolved NDP table. + MissingResourceDeviceNodeData { key: u32 }, + /// The record's raw provider key is not a row in its NDP table. + ResourceDeviceNodeDataKeyOutOfRange { key: u32, row_count: usize }, + /// The caller did not supply the provider's current type-1 value. + MissingResourceDeviceProviderValue { key: u32 }, + /// A coefficient is outside the helper's finite normal-or-zero contract. + InvalidResourceDeviceOperand { operand: &'static str }, + /// The native x87 intermediate is outside the range represented by normal + /// f32 values, so the Rust result could differ from the original. + UnsupportedResourceDeviceFloatRange { stage: &'static str }, /// A state object was reused with a different model node count. StateNodeCountMismatch { expected: usize, actual: usize }, /// A state object was reused with a different number of tag-21 records. @@ -1216,6 +1229,74 @@ pub enum ControlSamplingError { NonFiniteSample { record_index: usize }, } +/// Samples Control's tag-26 typed-record query `0x300` for one current state. +/// +/// The key in fixed word 1 must select an existing NDP row. The caller supplies +/// that provider's current type-1 value and the object's current multiplier; +/// this function does not invent their runtime state. Fixed word 11 is the +/// coefficient loaded by selector `0x300`. Native code multiplies in the order +/// `provider_now * coefficient * multiplier_now`. +/// +/// The source x87 code returns a value with an extended exponent range even +/// though it uses 24-bit precision. This f32 helper therefore accepts only +/// finite normal-or-zero operands and normal-or-exact-zero intermediate and +/// final products. It rejects subnormal or out-of-f32-range products rather +/// than silently underflowing or overflowing differently from x87. +pub fn sample_control_resource_device_selector_300( + record: &PreparedControlTypedRecord, + node_data: Option<&PreparedControlNodeDataTable>, + provider_now: Option, + multiplier_now: f32, +) -> Result { + if record.tag != 26 { + return Err(ControlSamplingError::UnsupportedResourceDeviceTag { actual: record.tag }); + } + + let key = record.fixed_words[1]; + let Some(node_data) = node_data else { + return Err(ControlSamplingError::MissingResourceDeviceNodeData { key }); + }; + if !usize::try_from(key).is_ok_and(|key| key < node_data.rows.len()) { + return Err(ControlSamplingError::ResourceDeviceNodeDataKeyOutOfRange { + key, + row_count: node_data.rows.len(), + }); + } + let provider_now = + provider_now.ok_or(ControlSamplingError::MissingResourceDeviceProviderValue { key })?; + let base = f32::from_bits(record.fixed_words[11]); + + for (operand, value) in [ + ("provider", provider_now), + ("base coefficient", base), + ("multiplier", multiplier_now), + ] { + if !value.is_finite() || (value != 0.0 && !value.is_normal()) { + return Err(ControlSamplingError::InvalidResourceDeviceOperand { operand }); + } + } + + let provider_times_base = + native_range_compatible_product(provider_now, base, "provider times base")?; + native_range_compatible_product(provider_times_base, multiplier_now, "then multiplier") +} + +fn native_range_compatible_product( + lhs: f32, + rhs: f32, + stage: &'static str, +) -> Result { + let exact = f64::from(lhs) * f64::from(rhs); + let absolute = exact.abs(); + let minimum_normal = f64::from(f32::MIN_POSITIVE); + let maximum = f64::from(f32::MAX); + if exact != 0.0 && (absolute < minimum_normal || absolute > maximum) { + return Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { stage }); + } + + Ok(lhs * rhs) +} + /// Samples per-node animation times using the decoded Control timeline. /// /// `native_clock_ms` is the absolute u32 millisecond clock passed to native @@ -4141,6 +4222,203 @@ mod tests { // Preserve the authored CTLD fixture bits; this is not the `f32` value of tau. const AUTHORED_TAG21_RAW_PARAMETER_3: f32 = f32::from_bits(0x40C8_F5C3); + fn resource_device_record(tag: u32, key: u32, base_bits: u32) -> PreparedControlTypedRecord { + let mut fixed_words = [0; 44]; + fixed_words[1] = key; + fixed_words[11] = base_bits; + PreparedControlTypedRecord { + tag, + fixed_words, + values: Vec::new(), + string_bytes: Vec::new(), + gate_provider: PreparedControlGateProvider::NotApplicable, + } + } + + fn resource_device_node_data_table(row_count: usize) -> PreparedControlNodeDataTable { + PreparedControlNodeDataTable { + source: ResourceKey { + archive: parse_path("parts.rlb").expect("NDP archive"), + name: resource_name(b"resource-device.ndp"), + type_id: None, + }, + rows: vec![ + PreparedControlNodeDataRow { + raw_flags: 0, + max_volume: 0.0, + mobility_per_volume: 0.0, + effect: PreparedControlResourcePair { + archive: [0; 32], + name: [0; 32], + }, + }; + row_count + ], + } + } + + #[test] + fn tag26_selector_300_multiplies_provider_then_fixed_base_then_multiplier() { + let record = resource_device_record(26, 1, 0x3D25_AE4A); + let node_data = resource_device_node_data_table(2); + let provider_now = f32::from_bits(0x3FE6_982F); + let multiplier_now = f32::from_bits(0x4071_5539); + + let sampled = sample_control_resource_device_selector_300( + &record, + Some(&node_data), + Some(provider_now), + multiplier_now, + ) + .expect("valid tag-26 resource-device sample"); + + assert_eq!(sampled.to_bits(), 0x3E8C_B02E); + assert_ne!( + sampled.to_bits(), + ((provider_now * multiplier_now) * f32::from_bits(record.fixed_words[11])).to_bits(), + "the original selector multiplies the provider by the record base first" + ); + } + + #[test] + fn tag26_selector_300_requires_its_ndp_row_and_explicit_provider() { + let record = resource_device_record(26, 1, 2.0_f32.to_bits()); + let one_row = resource_device_node_data_table(1); + assert_eq!( + sample_control_resource_device_selector_300(&record, Some(&one_row), Some(3.0), 4.0,), + Err(ControlSamplingError::ResourceDeviceNodeDataKeyOutOfRange { + key: 1, + row_count: 1, + }) + ); + + let two_rows = resource_device_node_data_table(2); + assert_eq!( + sample_control_resource_device_selector_300(&record, Some(&two_rows), None, 4.0), + Err(ControlSamplingError::MissingResourceDeviceProviderValue { key: 1 }) + ); + assert_eq!( + sample_control_resource_device_selector_300(&record, None, Some(3.0), 4.0), + Err(ControlSamplingError::MissingResourceDeviceNodeData { key: 1 }) + ); + } + + #[test] + fn tag26_selector_300_rejects_other_tags_and_extended_x87_ranges() { + let node_data = resource_device_node_data_table(1); + let tag21 = resource_device_record(21, 0, 1.0_f32.to_bits()); + assert_eq!( + sample_control_resource_device_selector_300(&tag21, Some(&node_data), Some(1.0), 1.0), + Err(ControlSamplingError::UnsupportedResourceDeviceTag { actual: 21 }) + ); + + let tag26 = resource_device_record(26, 0, f32::MIN_POSITIVE.to_bits()); + assert_eq!( + sample_control_resource_device_selector_300( + &tag26, + Some(&node_data), + Some(f32::MIN_POSITIVE), + 1.0, + ), + Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { + stage: "provider times base", + }) + ); + } + + #[test] + fn tag26_selector_300_preserves_negative_and_signed_zero_values() { + let node_data = resource_device_node_data_table(1); + let record = resource_device_record(26, 0, 0.5_f32.to_bits()); + let negative = + sample_control_resource_device_selector_300(&record, Some(&node_data), Some(-2.0), 4.0) + .expect("negative factors are not clamped"); + assert_eq!(negative, -4.0); + + let negative_zero = sample_control_resource_device_selector_300( + &resource_device_record(26, 0, 2.0_f32.to_bits()), + Some(&node_data), + Some(-0.0), + 3.0, + ) + .expect("signed zero is an exact native input"); + assert_eq!(negative_zero.to_bits(), (-0.0_f32).to_bits()); + + let positive_zero = sample_control_resource_device_selector_300( + &resource_device_record(26, 0, 2.0_f32.to_bits()), + Some(&node_data), + Some(0.0), + 3.0, + ) + .expect("positive zero is an exact native input"); + assert_eq!(positive_zero.to_bits(), 0.0_f32.to_bits()); + } + + #[test] + fn tag26_selector_300_rejects_nonfinite_and_subnormal_operands() { + let node_data = resource_device_node_data_table(1); + for (operand, provider_now, multiplier_now, base_bits) in [ + ("provider", f32::NAN, 1.0, 1.0_f32.to_bits()), + ("multiplier", 1.0, f32::INFINITY, 1.0_f32.to_bits()), + ("base coefficient", 1.0, 1.0, 1), + ] { + let record = resource_device_record(26, 0, base_bits); + assert_eq!( + sample_control_resource_device_selector_300( + &record, + Some(&node_data), + Some(provider_now), + multiplier_now, + ), + Err(ControlSamplingError::InvalidResourceDeviceOperand { operand }) + ); + } + } + + #[test] + fn tag26_selector_300_rejects_extended_intermediate_and_final_ranges() { + let node_data = resource_device_node_data_table(1); + let provider = f32::MAX; + let overflowing_intermediate = resource_device_record(26, 0, 2.0_f32.to_bits()); + assert_eq!( + sample_control_resource_device_selector_300( + &overflowing_intermediate, + Some(&node_data), + Some(provider), + 0.25, + ), + Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { + stage: "provider times base", + }) + ); + + let underflowing_intermediate = resource_device_record(26, 0, f32::MIN_POSITIVE.to_bits()); + assert_eq!( + sample_control_resource_device_selector_300( + &underflowing_intermediate, + Some(&node_data), + Some(f32::MIN_POSITIVE), + f32::MAX, + ), + Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { + stage: "provider times base", + }) + ); + + let subnormal_final = resource_device_record(26, 0, 1.0_f32.to_bits()); + assert_eq!( + sample_control_resource_device_selector_300( + &subnormal_final, + Some(&node_data), + Some(f32::MIN_POSITIVE), + 0.5, + ), + Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { + stage: "then multiplier", + }) + ); + } + fn tag21_fullcycle_control() -> PreparedComponentControl { let source = ResourceKey { archive: parse_path("parts.rlb").expect("archive"), @@ -6164,6 +6442,60 @@ mod tests { assert_eq!(plan.lightmap_count, 0); } + #[test] + #[ignore = "requires licensed corpus"] + fn tag26_selector_300_uses_a_real_control_record_and_ndp_key() { + let root = fixture_root("IS"); + let vfs: Arc = Arc::new(DirectoryVfs::new(&root)); + let repository = CachedResourceRepository::new(Arc::clone(&vfs)); + let roots = [resource_name(b"UNITS/BUILDS/MINE/min_l_n1.dat")]; + let (_graph, prototypes) = + build_prototype_graph(&repository, vfs.as_ref(), &roots).expect("prototype graph"); + let assets = prepare_mission_assets_with_repository( + &repository, + &[0..prototypes.len()], + &prototypes, + ) + .expect("prepared licensed unit controls"); + + let mut sampled_records = 0; + for control in assets.object_component_controls.iter().flatten().flatten() { + for record in control + .typed_records + .iter() + .filter(|record| record.tag == 26) + { + let node_data = control + .node_data + .as_ref() + .expect("tag-26 control has its explicit NDP table"); + assert_eq!(control.source.name.0, b"fr_l_mine.ctl"); + assert_eq!(node_data.source.name.0, b"fr_l_mine.ndp"); + assert_eq!(node_data.rows.len(), 22); + assert_eq!(record.fixed_words[1], 2); + assert_eq!(record.fixed_words[11], 0x3F80_0000); + let sample = sample_control_resource_device_selector_300( + record, + Some(node_data), + Some(1.0), + 1.0, + ) + .expect("real tag-26 key selects an NDP row"); + assert_eq!(sample.to_bits(), record.fixed_words[11]); + println!( + "control={}: ndp={}: key={} rows={} coefficient=0x{:08x}", + String::from_utf8_lossy(&control.source.name.0), + String::from_utf8_lossy(&node_data.source.name.0), + record.fixed_words[1], + node_data.rows.len(), + record.fixed_words[11], + ); + sampled_records += 1; + } + } + assert_eq!(sampled_records, 1); + } + #[test] #[ignore = "requires licensed corpus"] fn repository_plan_deduplicates_duplicate_visuals_but_graph_preserves_requests() { 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 8532d1a..d2e5bb9 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -16,6 +16,7 @@ internal static class BehaviorDistributorTickOracle { private const uint MemRelease = 0x00008000; private const uint PageReadWrite = 0x04; private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99"; + private const string ExpectedControlSha256 = "bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e"; private const int TickRva = 0x19e80; private const int AddObjectRva = 0x1aac0; private const int NormalizeRva = 0x1ac90; @@ -23,6 +24,9 @@ internal static class BehaviorDistributorTickOracle { private const int IndexedPropertyGetRva = 0x0b5b0; private const int OreInitializationRva = 0x08790; private const int GetSystemArealMapIatRva = 0x59008; + private const int ControlResourceDeviceGetterRva = 0x21d00; + private const int ControlProviderInterfaceVtableRva = 0x3d254; + private const int ControlProviderType1GetterRva = 0x0dc40; private const uint ResourceProperty = 0x02000100; private static Fixture active; @@ -52,6 +56,7 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void IndexedPropertySet(IntPtr self, uint index, uint valueBits); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int IndexedPropertyGet(IntPtr self, uint index, IntPtr resultBits); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeOreInitialization(IntPtr self); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceGet(IntPtr self, uint selector); private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap; private static readonly VectorInsert VectorInsertCallback = InsertVector; @@ -306,13 +311,20 @@ internal static class BehaviorDistributorTickOracle { private static int Main() { if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; } + string controlPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_CONTROL_DLL"); + if (String.IsNullOrEmpty(controlPath)) controlPath = @"C:\GOG Games\Parkan - Iron Strategy\Control.dll"; + if (!File.Exists(controlPath)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_CONTROL_DLL to the installed Control.dll."); return 7; } + string controlHash = Hash(controlPath); + if (controlHash != ExpectedControlSha256) { Console.Error.WriteLine("Unexpected Control.dll SHA-256: " + controlHash); return 8; } + IntPtr controlModule = LoadLibraryEx(controlPath, IntPtr.Zero, LoadWithAlteredSearchPath); + if (controlModule == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Control.dll) failed: " + Marshal.GetLastWin32Error()); return 9; } string path = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_BEHAVIOR_DLL"); if (String.IsNullOrEmpty(path)) path = @"C:\GOG Games\Parkan - Iron Strategy\Behavior.dll"; - if (!File.Exists(path)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_BEHAVIOR_DLL to the installed Behavior.dll."); return 3; } + if (!File.Exists(path)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_BEHAVIOR_DLL to the installed Behavior.dll."); FreeLibrary(controlModule); return 3; } string hash = Hash(path); - if (hash != ExpectedBehaviorSha256) { Console.Error.WriteLine("Unexpected Behavior.dll SHA-256: " + hash); return 4; } + if (hash != ExpectedBehaviorSha256) { Console.Error.WriteLine("Unexpected Behavior.dll SHA-256: " + hash); FreeLibrary(controlModule); return 4; } IntPtr module = LoadLibraryEx(path, IntPtr.Zero, LoadWithAlteredSearchPath); - if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Behavior.dll) failed: " + Marshal.GetLastWin32Error()); return 5; } + if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Behavior.dll) failed: " + Marshal.GetLastWin32Error()); FreeLibrary(controlModule); return 5; } IntPtr import = IntPtr.Add(module, GetSystemArealMapIatRva); IntPtr originalImport = Marshal.ReadIntPtr(import); uint oldProtection = 0; @@ -334,7 +346,7 @@ internal static class BehaviorDistributorTickOracle { throw new InvalidOperationException("VirtualProtect(GetSystemArealMap IAT) failed: " + Marshal.GetLastWin32Error()); protectionChanged = true; Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback)); - RunSmokeCases(module); + RunSmokeCases(module, controlModule); Console.WriteLine("original Behavior.dll distributor oracle passed (native Add 0x1aac0, normalize 0x1ac90, tick 0x19e80; only GetSystemArealMap IAT and bounded vector insertion are substituted; this does not compare Rust parity)."); return 0; } @@ -357,10 +369,11 @@ internal static class BehaviorDistributorTickOracle { throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error()); if (x87ControlWordCell != IntPtr.Zero) { Marshal.FreeHGlobal(x87ControlWordCell); x87ControlWordCell = IntPtr.Zero; } FreeLibrary(module); + FreeLibrary(controlModule); } } - private static void RunSmokeCases(IntPtr module) { + private static void RunSmokeCases(IntPtr module, IntPtr controlModule) { ushort savedControlWord = CurrentX87ControlWord(); Console.WriteLine("x87-control-word-before=0x" + savedControlWord.ToString("x4") + " (x86 oracle process)"); try { @@ -368,6 +381,7 @@ internal static class BehaviorDistributorTickOracle { Console.WriteLine("native-add-normalize-x87-control-word=0x" + CurrentX87ControlWord().ToString("x4")); RunNativeAddCases(module); RunNativeNormalizeCases(module); + RunNativeControlResourceDeviceCase(controlModule); } finally { SetX87ControlWord(savedControlWord); Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4")); @@ -396,6 +410,76 @@ internal static class BehaviorDistributorTickOracle { RunSkippedCase(tick); } + private static void RunNativeControlResourceDeviceCase(IntPtr module) { + Expect(CurrentX87ControlWord() == 0x007f, "Control selector 0x300 uses x87 control word 0x007f"); + const uint key = 2; + const int rowCount = 22; + const int providerRow = (int)key * 0x2c; + IntPtr providerOwner = Marshal.AllocHGlobal(0x560); + IntPtr providerRows = Marshal.AllocHGlobal(rowCount * 0x2c); + IntPtr device = IntPtr.Zero; + try { + Zero(providerOwner, 0x560); + Zero(providerRows, rowCount * 0x2c); + IntPtr providerInterface = IntPtr.Add(providerOwner, 4); + IntPtr providerVtable = IntPtr.Add(module, ControlProviderInterfaceVtableRva); + Marshal.WriteIntPtr(providerInterface, providerVtable); + Expect( + Marshal.ReadIntPtr(providerVtable, 0x0c) == IntPtr.Add(module, ControlProviderType1GetterRva), + "Control provider interface vtable uses the native type-1 getter"); + Marshal.WriteIntPtr(providerOwner, 0x55c, providerRows); + + device = Marshal.AllocHGlobal(0x94); + Zero(device, 0x94); + Marshal.WriteInt32(device, 0x04, unchecked((int)key)); + Marshal.WriteIntPtr(device, 0x44, providerInterface); + + NativeControlResourceDeviceGet getter = (NativeControlResourceDeviceGet) + Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, ControlResourceDeviceGetterRva), + typeof(NativeControlResourceDeviceGet)); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0x3fe6982f, 0x3d25ae4a, 0x40715539, 0x3e8cb02e, "order-sensitive normal"); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0x7f7fffff, 0x40000000, 0x3e800000, 0x7effffff, "extended intermediate overflow then finite"); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0x00800000, 0x00800000, 0x7f7fffff, 0x017fffff, "extended intermediate underflow then finite"); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0x00800000, 0x3f800000, 0x3f000000, 0x00400000, "subnormal final result"); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0xc0000000, 0x3f000000, 0x40800000, 0xc0800000, "negative factors"); + ExpectControlResourceDeviceResult(getter, device, providerRows, providerRow, + 0x80000000, 0x40000000, 0x40400000, 0x80000000, "negative zero"); + Expect(CurrentX87ControlWord() == 0x007f, "Control selector 0x300 retains x87 control word 0x007f"); + Console.WriteLine("native-control-tag26-selector-300: controlHash=" + ExpectedControlSha256 + + " cw=0x" + CurrentX87ControlWord().ToString("x4") + " key=2 rows=22"); + GC.KeepAlive(getter); + } finally { + if (device != IntPtr.Zero) Marshal.FreeHGlobal(device); + Marshal.FreeHGlobal(providerRows); + Marshal.FreeHGlobal(providerOwner); + } + } + + private static void ExpectControlResourceDeviceResult( + NativeControlResourceDeviceGet getter, + IntPtr device, + IntPtr providerRows, + int providerRow, + uint providerBits, + uint baseBits, + uint multiplierBits, + uint expectedBits, + string label) { + Marshal.WriteInt32(providerRows, providerRow + 0x14, unchecked((int)providerBits)); + Marshal.WriteInt32(device, 0x54, unchecked((int)baseBits)); + Marshal.WriteInt32(device, 0x4c, unchecked((int)multiplierBits)); + uint actualBits = unchecked((uint)FloatBits(getter(device, 0x300))); + Expect(actualBits == expectedBits, + "Control selector 0x300 " + label + " expected " + Hex(expectedBits) + " but got " + Hex(actualBits)); + Console.WriteLine("native-control-tag26-selector-300-" + label + ": result=" + Hex(actualBits)); + } + private static void RunNativeIndexedPropertyCases(IntPtr module) { IndexedPropertySet set = (IndexedPropertySet)Marshal.GetDelegateForFunctionPointer( IntPtr.Add(module, IndexedPropertySetRva), typeof(IndexedPropertySet)); @@ -911,7 +995,7 @@ internal static class BehaviorDistributorTickOracle { foreach (byte value in digest) hex.Append(value.ToString("x2")); return hex.ToString(); } } - private static void Expect(bool value, string message) { if (!value) throw new InvalidOperationException("original Behavior oracle failed: " + message); } + private static void Expect(bool value, string message) { if (!value) throw new InvalidOperationException("original native oracle failed: " + message); } private static void ExpectLookupOrder(Fixture fixture, params string[] keys) { List actual = new List(); foreach (string item in fixture.Trace) if (item.StartsWith("lookup:", StringComparison.Ordinal)) actual.Add(item.Substring(7)); -- cgit v1.2.3