fix(runtime): model distributor share normalization
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
This commit is contained in:
5 files changed
+516
-42
No files matched your search
@@ -116,7 +116,7 @@ pub struct MissionObjectDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A mission object registered with one clan's native Behavior distributor.
|
/// A mission object registered with one clan's native Behavior distributor.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct BehaviorRegisteredObject {
|
pub struct BehaviorRegisteredObject {
|
||||||
/// Stable identity from the source TMA object order.
|
/// Stable identity from the source TMA object order.
|
||||||
pub original_id: OriginalObjectId,
|
pub original_id: OriginalObjectId,
|
||||||
@@ -126,10 +126,16 @@ pub struct BehaviorRegisteredObject {
|
|||||||
pub selector: u32,
|
pub selector: u32,
|
||||||
/// Class word recovered from the root Unit DAT header.
|
/// Class word recovered from the root Unit DAT header.
|
||||||
pub class_flags: u32,
|
pub class_flags: u32,
|
||||||
|
/// Native share field at record offset `+0x04`.
|
||||||
|
pub share: f32,
|
||||||
|
/// Raw DWORDs at offsets `+0x08..+0x18`; the first controls whether
|
||||||
|
/// normalization treats this record as fixed, and the remaining words are
|
||||||
|
/// not yet assigned semantics. A normal native Add initializes all five to 0.
|
||||||
|
pub native_tail: [u32; 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resource distributor state associated with one TMA clan's constructed AI.
|
/// Resource distributor state associated with one TMA clan's constructed AI.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct BehaviorDistributorState {
|
pub struct BehaviorDistributorState {
|
||||||
/// TMA clan index; it is not an index into the filtered script bundle list.
|
/// TMA clan index; it is not an index into the filtered script bundle list.
|
||||||
pub clan_index: usize,
|
pub clan_index: usize,
|
||||||
@@ -1022,14 +1028,17 @@ fn load_mission_with_options(
|
|||||||
let Some(original_id) = draft.original_id else {
|
let Some(original_id) = draft.original_id else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
distributors[clan_index]
|
add_behavior_registered_object(
|
||||||
.registered_objects
|
&mut distributors[clan_index],
|
||||||
.push(BehaviorRegisteredObject {
|
BehaviorRegisteredObject {
|
||||||
original_id,
|
original_id,
|
||||||
handle: *handle,
|
handle: *handle,
|
||||||
selector: draft.raw_after_resource,
|
selector: draft.raw_after_resource,
|
||||||
class_flags,
|
class_flags,
|
||||||
});
|
share: 0.0,
|
||||||
|
native_tail: [0; 5],
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
distributors
|
distributors
|
||||||
} else {
|
} else {
|
||||||
@@ -1409,6 +1418,69 @@ fn has_behavior_registration_key(class_flags: u32) -> bool {
|
|||||||
class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0
|
class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_behavior_registered_object(
|
||||||
|
distributor: &mut BehaviorDistributorState,
|
||||||
|
mut object: BehaviorRegisteredObject,
|
||||||
|
) {
|
||||||
|
let existing_index = distributor
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.handle == object.handle);
|
||||||
|
let count = distributor.registered_objects.len() + usize::from(existing_index.is_none());
|
||||||
|
|
||||||
|
// Native Add/normalize ran with x87 control word 0x007f. Keep the list-order
|
||||||
|
// arithmetic in f32 so each operation has the observed 24-bit precision.
|
||||||
|
let share = 1.0_f32 / count as f32;
|
||||||
|
let handle = object.handle;
|
||||||
|
if let Some(index) = existing_index {
|
||||||
|
distributor.registered_objects[index].share = share;
|
||||||
|
} else {
|
||||||
|
object.share = share;
|
||||||
|
object.native_tail = [0; 5];
|
||||||
|
distributor.registered_objects.push(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_behavior_distributor(distributor, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_behavior_distributor(
|
||||||
|
distributor: &mut BehaviorDistributorState,
|
||||||
|
fixed_handle: ObjectHandle,
|
||||||
|
) {
|
||||||
|
let mut fixed_sum = 0.0_f32;
|
||||||
|
let mut free_sum = 0.0_f32;
|
||||||
|
let mut adjustable_count = 0_usize;
|
||||||
|
|
||||||
|
for object in &distributor.registered_objects {
|
||||||
|
if object.handle == fixed_handle || object.native_tail[0] != 0 {
|
||||||
|
fixed_sum = fixed_sum + object.share;
|
||||||
|
} else {
|
||||||
|
free_sum = free_sum + object.share;
|
||||||
|
adjustable_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if adjustable_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if free_sum <= 0.0 {
|
||||||
|
let share = (1.0_f32 - fixed_sum) / adjustable_count as f32;
|
||||||
|
for object in &mut distributor.registered_objects {
|
||||||
|
if object.handle != fixed_handle && object.native_tail[0] == 0 {
|
||||||
|
object.share = share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let factor = (1.0_f32 - fixed_sum) / free_sum;
|
||||||
|
for object in &mut distributor.registered_objects {
|
||||||
|
if object.handle != fixed_handle && object.native_tail[0] == 0 {
|
||||||
|
object.share = object.share * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
||||||
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
||||||
EngineError::Path {
|
EngineError::Path {
|
||||||
@@ -2421,6 +2493,8 @@ mod tests {
|
|||||||
draft.resource_name_raw.as_slice(),
|
draft.resource_name_raw.as_slice(),
|
||||||
object.selector,
|
object.selector,
|
||||||
object.class_flags,
|
object.class_flags,
|
||||||
|
object.share.to_bits(),
|
||||||
|
object.native_tail,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
@@ -2431,6 +2505,8 @@ mod tests {
|
|||||||
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
||||||
0,
|
0,
|
||||||
0x8000_0010,
|
0x8000_0010,
|
||||||
|
0x3f00_0000,
|
||||||
|
[0; 5],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
0,
|
0,
|
||||||
@@ -2438,6 +2514,8 @@ mod tests {
|
|||||||
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
||||||
0,
|
0,
|
||||||
0x8000_0002,
|
0x8000_0002,
|
||||||
|
0x3f00_0000,
|
||||||
|
[0; 5],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
1,
|
1,
|
||||||
@@ -2445,6 +2523,8 @@ mod tests {
|
|||||||
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
||||||
1,
|
1,
|
||||||
0x8000_0010,
|
0x8000_0010,
|
||||||
|
0x3f00_0000,
|
||||||
|
[0; 5],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
1,
|
1,
|
||||||
@@ -2452,6 +2532,8 @@ mod tests {
|
|||||||
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
||||||
1,
|
1,
|
||||||
0x8000_0002,
|
0x8000_0002,
|
||||||
|
0x3f00_0000,
|
||||||
|
[0; 5],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -2519,6 +2601,152 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn behavior_test_object(
|
||||||
|
slot: u32,
|
||||||
|
share: f32,
|
||||||
|
native_tail: [u32; 5],
|
||||||
|
) -> BehaviorRegisteredObject {
|
||||||
|
BehaviorRegisteredObject {
|
||||||
|
original_id: OriginalObjectId(slot),
|
||||||
|
handle: ObjectHandle {
|
||||||
|
generation: 0,
|
||||||
|
slot,
|
||||||
|
},
|
||||||
|
selector: 0,
|
||||||
|
class_flags: 0,
|
||||||
|
share,
|
||||||
|
native_tail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_distributor_add_uses_handle_and_preserves_duplicate_tail() {
|
||||||
|
let mut distributor = BehaviorDistributorState {
|
||||||
|
clan_index: 0,
|
||||||
|
super_ai_visible: true,
|
||||||
|
registered_objects: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
add_behavior_registered_object(&mut distributor, behavior_test_object(4, 0.0, [0; 5]));
|
||||||
|
add_behavior_registered_object(&mut distributor, behavior_test_object(6, 0.0, [0; 5]));
|
||||||
|
assert_eq!(
|
||||||
|
distributor
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.share.to_bits())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![0x3f00_0000, 0x3f00_0000]
|
||||||
|
);
|
||||||
|
|
||||||
|
distributor.registered_objects[0].share = 0.2;
|
||||||
|
distributor.registered_objects[0].native_tail = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5];
|
||||||
|
add_behavior_registered_object(&mut distributor, behavior_test_object(4, 0.0, [0; 5]));
|
||||||
|
assert_eq!(
|
||||||
|
distributor.registered_objects[0].share.to_bits(),
|
||||||
|
0x3f00_0000
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distributor.registered_objects[0].native_tail,
|
||||||
|
[0xa1, 0xa2, 0xa3, 0xa4, 0xa5]
|
||||||
|
);
|
||||||
|
|
||||||
|
add_behavior_registered_object(&mut distributor, behavior_test_object(8, 0.0, [0; 5]));
|
||||||
|
assert_eq!(
|
||||||
|
distributor
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.share.to_bits())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![0x3f00_0000, 0x3e2a_aaa8, 0x3eaa_aaab]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
distributor
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.native_tail)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![[0xa1, 0xa2, 0xa3, 0xa4, 0xa5], [0; 5], [0; 5]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_distributor_normalize_matches_native_24_bit_vectors() {
|
||||||
|
let mut pinned = BehaviorDistributorState {
|
||||||
|
clan_index: 0,
|
||||||
|
super_ai_visible: true,
|
||||||
|
registered_objects: vec![
|
||||||
|
behavior_test_object(0x11, 0.2, [0; 5]),
|
||||||
|
behavior_test_object(0x22, 0.3, [1, 0x11, 0x22, 0x33, 0x44]),
|
||||||
|
behavior_test_object(0x33, 0.3, [0; 5]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
normalize_behavior_distributor(
|
||||||
|
&mut pinned,
|
||||||
|
ObjectHandle {
|
||||||
|
generation: 0,
|
||||||
|
slot: 0x11,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pinned
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.share.to_bits())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![0x3e4c_cccd, 0x3e99_999a, 0x3f00_0000]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut zero_sum = BehaviorDistributorState {
|
||||||
|
clan_index: 0,
|
||||||
|
super_ai_visible: true,
|
||||||
|
registered_objects: vec![
|
||||||
|
behavior_test_object(0x41, 0.25, [0; 5]),
|
||||||
|
behavior_test_object(0x42, 0.0, [0; 5]),
|
||||||
|
behavior_test_object(0x43, 0.0, [0; 5]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
normalize_behavior_distributor(
|
||||||
|
&mut zero_sum,
|
||||||
|
ObjectHandle {
|
||||||
|
generation: 0,
|
||||||
|
slot: 0x41,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
zero_sum
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.share.to_bits())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![0x3e80_0000, 0x3ec0_0000, 0x3ec0_0000]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut near_rounding = BehaviorDistributorState {
|
||||||
|
clan_index: 0,
|
||||||
|
super_ai_visible: true,
|
||||||
|
registered_objects: vec![
|
||||||
|
behavior_test_object(0x51, 0.5, [0; 5]),
|
||||||
|
behavior_test_object(0x52, 1.0, [0; 5]),
|
||||||
|
behavior_test_object(0x53, f32::from_bits(0x3380_0000), [0; 5]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
normalize_behavior_distributor(
|
||||||
|
&mut near_rounding,
|
||||||
|
ObjectHandle {
|
||||||
|
generation: 0,
|
||||||
|
slot: 0x51,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
near_rounding
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(|object| object.share.to_bits())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![0x3f00_0000, 0x3f00_0000, 0x3300_0000]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn licensed_campaign_preview_keeps_unsupported_init_without_partial_writes() {
|
fn licensed_campaign_preview_keeps_unsupported_init_without_partial_writes() {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
// Calls the original x86 Behavior.dll distributor tick against synthetic vtables.
|
// Calls the original x86 Behavior.dll distributor Add, normalize, and tick paths
|
||||||
// Only the imported GetSystemArealMap IAT slot is replaced; the tick instructions
|
// against synthetic storage. Only GetSystemArealMap and the bounded vector backing
|
||||||
// and its internal helpers remain the installed DLL's original code.
|
// are substituted; Behavior's corresponding instructions remain the installed DLL.
|
||||||
internal static class BehaviorDistributorTickOracle {
|
internal static class BehaviorDistributorTickOracle {
|
||||||
private const uint LoadWithAlteredSearchPath = 0x00000008;
|
private const uint LoadWithAlteredSearchPath = 0x00000008;
|
||||||
private const uint MemCommitReserve = 0x00003000;
|
private const uint MemCommitReserve = 0x00003000;
|
||||||
@@ -16,6 +16,8 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private const uint PageReadWrite = 0x04;
|
private const uint PageReadWrite = 0x04;
|
||||||
private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99";
|
private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99";
|
||||||
private const int TickRva = 0x19e80;
|
private const int TickRva = 0x19e80;
|
||||||
|
private const int AddObjectRva = 0x1aac0;
|
||||||
|
private const int NormalizeRva = 0x1ac90;
|
||||||
private const int GetSystemArealMapIatRva = 0x59008;
|
private const int GetSystemArealMapIatRva = 0x59008;
|
||||||
private const uint ResourceProperty = 0x02000100;
|
private const uint ResourceProperty = 0x02000100;
|
||||||
private static Fixture active;
|
private static Fixture active;
|
||||||
@@ -28,7 +30,12 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint protect, out uint oldProtect);
|
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint protect, out uint oldProtect);
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr AreaMapGetter();
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr AreaMapGetter();
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void X87ControlWordReader();
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void X87ControlWordWriter();
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void TickDelegate(IntPtr self, float deltaSeconds);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void TickDelegate(IntPtr self, float deltaSeconds);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeAddObject(IntPtr self, uint key);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeNormalize(IntPtr self, uint key);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void VectorInsert(IntPtr self, IntPtr source, uint count, uint position);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr MapLookup(IntPtr self, uint key);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr MapLookup(IntPtr self, uint key);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int ObjectState(IntPtr self);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int ObjectState(IntPtr self);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint ObjectClass(IntPtr self);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint ObjectClass(IntPtr self);
|
||||||
@@ -40,6 +47,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceSetProperty(IntPtr self, uint selector, IntPtr value);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceSetProperty(IntPtr self, uint selector, IntPtr value);
|
||||||
|
|
||||||
private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap;
|
private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap;
|
||||||
|
private static readonly VectorInsert VectorInsertCallback = InsertVector;
|
||||||
private static readonly MapLookup MapLookupCallback = Lookup;
|
private static readonly MapLookup MapLookupCallback = Lookup;
|
||||||
private static readonly ObjectState ObjectStateCallback = GetObjectState;
|
private static readonly ObjectState ObjectStateCallback = GetObjectState;
|
||||||
private static readonly ObjectClass ObjectClassCallback = GetObjectClass;
|
private static readonly ObjectClass ObjectClassCallback = GetObjectClass;
|
||||||
@@ -53,6 +61,10 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private static IntPtr queryObjectThunk;
|
private static IntPtr queryObjectThunk;
|
||||||
private static IntPtr getInterfacePropertyThunk;
|
private static IntPtr getInterfacePropertyThunk;
|
||||||
private static IntPtr setInterfacePropertyThunk;
|
private static IntPtr setInterfacePropertyThunk;
|
||||||
|
private static IntPtr vectorInsertThunk;
|
||||||
|
private static IntPtr x87ControlWordCell;
|
||||||
|
private static X87ControlWordReader x87ControlWordReader;
|
||||||
|
private static X87ControlWordWriter x87ControlWordWriter;
|
||||||
|
|
||||||
private sealed class NativeObject {
|
private sealed class NativeObject {
|
||||||
public IntPtr Self;
|
public IntPtr Self;
|
||||||
@@ -84,6 +96,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private sealed class Fixture : IDisposable {
|
private sealed class Fixture : IDisposable {
|
||||||
public IntPtr Distributor;
|
public IntPtr Distributor;
|
||||||
public IntPtr Entries;
|
public IntPtr Entries;
|
||||||
|
public IntPtr VectorVtable;
|
||||||
public IntPtr Manager;
|
public IntPtr Manager;
|
||||||
public IntPtr ManagerVtable;
|
public IntPtr ManagerVtable;
|
||||||
public readonly List<IntPtr> Allocations = new List<IntPtr>();
|
public readonly List<IntPtr> Allocations = new List<IntPtr>();
|
||||||
@@ -91,6 +104,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
public readonly List<string> Calls = new List<string>();
|
public readonly List<string> Calls = new List<string>();
|
||||||
public readonly List<string> Trace = new List<string>();
|
public readonly List<string> Trace = new List<string>();
|
||||||
public int LookupCount;
|
public int LookupCount;
|
||||||
|
public int VectorInsertCalls;
|
||||||
public NativeObject AddObject(uint key, uint classCode, float powerRate, float oreRate, float property, int state) {
|
public NativeObject AddObject(uint key, uint classCode, float powerRate, float oreRate, float property, int state) {
|
||||||
NativeObject item = new NativeObject();
|
NativeObject item = new NativeObject();
|
||||||
item.Key = key;
|
item.Key = key;
|
||||||
@@ -129,8 +143,14 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
}
|
}
|
||||||
public void MakeDistributor(params uint[] keys) {
|
public void MakeDistributor(params uint[] keys) {
|
||||||
Distributor = Allocate(0x44);
|
Distributor = Allocate(0x44);
|
||||||
Entries = Allocate(Math.Max(1, keys.Length) * 0x1c);
|
const int capacity = 32;
|
||||||
|
if (keys.Length > capacity) throw new InvalidOperationException("synthetic distributor capacity exceeded");
|
||||||
|
Entries = Allocate(capacity * 0x1c);
|
||||||
|
VectorVtable = Allocate(0x20);
|
||||||
|
Marshal.WriteIntPtr(VectorVtable, 0x10, vectorInsertThunk);
|
||||||
|
Marshal.WriteIntPtr(Distributor, 0x1c, VectorVtable);
|
||||||
Marshal.WriteInt32(Distributor, 0x20, keys.Length);
|
Marshal.WriteInt32(Distributor, 0x20, keys.Length);
|
||||||
|
Marshal.WriteInt32(Distributor, 0x24, capacity);
|
||||||
Marshal.WriteIntPtr(Distributor, 0x28, Entries);
|
Marshal.WriteIntPtr(Distributor, 0x28, Entries);
|
||||||
for (int i = 0; i < keys.Length; i++) {
|
for (int i = 0; i < keys.Length; i++) {
|
||||||
IntPtr entry = IntPtr.Add(Entries, i * 0x1c);
|
IntPtr entry = IntPtr.Add(Entries, i * 0x1c);
|
||||||
@@ -152,6 +172,21 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static IntPtr GetAreaMap() { return active.Manager; }
|
private static IntPtr GetAreaMap() { return active.Manager; }
|
||||||
|
private static void InsertVector(IntPtr self, IntPtr source, uint count, uint position) {
|
||||||
|
int length = Marshal.ReadInt32(self, 4);
|
||||||
|
int capacity = Marshal.ReadInt32(self, 8);
|
||||||
|
IntPtr destination = Marshal.ReadIntPtr(self, 12);
|
||||||
|
Expect(count > 0 && count <= (uint)(capacity - length), "bounded vector insertion capacity");
|
||||||
|
Expect(position == 0xffffffff || position == (uint)length, "native append position");
|
||||||
|
byte[] entry = new byte[0x1c];
|
||||||
|
for (uint index = 0; index < count; index++) {
|
||||||
|
Marshal.Copy(IntPtr.Add(source, unchecked((int)(index * 0x1c))), entry, 0, entry.Length);
|
||||||
|
Marshal.Copy(entry, 0, IntPtr.Add(destination, (length + (int)index) * 0x1c), entry.Length);
|
||||||
|
}
|
||||||
|
Marshal.WriteInt32(self, 4, length + (int)count);
|
||||||
|
active.VectorInsertCalls++;
|
||||||
|
active.Trace.Add("insert:" + count.ToString() + ":" + position.ToString("x8"));
|
||||||
|
}
|
||||||
private static IntPtr Lookup(IntPtr self, uint key) {
|
private static IntPtr Lookup(IntPtr self, uint key) {
|
||||||
active.Calls.Add("lookup:" + key.ToString("x8"));
|
active.Calls.Add("lookup:" + key.ToString("x8"));
|
||||||
active.LookupCount++;
|
active.LookupCount++;
|
||||||
@@ -283,12 +318,17 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
queryObjectThunk = WriteFastcallToStdcallThunk(0x00, Marshal.GetFunctionPointerForDelegate(ObjectQueryCallback));
|
queryObjectThunk = WriteFastcallToStdcallThunk(0x00, Marshal.GetFunctionPointerForDelegate(ObjectQueryCallback));
|
||||||
getInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x40, Marshal.GetFunctionPointerForDelegate(InterfaceGetCallback));
|
getInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x40, Marshal.GetFunctionPointerForDelegate(InterfaceGetCallback));
|
||||||
setInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x80, Marshal.GetFunctionPointerForDelegate(InterfaceSetCallback));
|
setInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x80, Marshal.GetFunctionPointerForDelegate(InterfaceSetCallback));
|
||||||
|
vectorInsertThunk = WriteThisCallThreeArgsToStdcallThunk(0xc0, Marshal.GetFunctionPointerForDelegate(VectorInsertCallback));
|
||||||
|
x87ControlWordCell = Marshal.AllocHGlobal(2);
|
||||||
|
Marshal.WriteInt16(x87ControlWordCell, 0);
|
||||||
|
x87ControlWordReader = (X87ControlWordReader)Marshal.GetDelegateForFunctionPointer(WriteX87ControlWordReader(0x100), typeof(X87ControlWordReader));
|
||||||
|
x87ControlWordWriter = (X87ControlWordWriter)Marshal.GetDelegateForFunctionPointer(WriteX87ControlWordWriter(0x120), typeof(X87ControlWordWriter));
|
||||||
if (!VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), PageReadWrite, out oldProtection))
|
if (!VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), PageReadWrite, out oldProtection))
|
||||||
throw new InvalidOperationException("VirtualProtect(GetSystemArealMap IAT) failed: " + Marshal.GetLastWin32Error());
|
throw new InvalidOperationException("VirtualProtect(GetSystemArealMap IAT) failed: " + Marshal.GetLastWin32Error());
|
||||||
protectionChanged = true;
|
protectionChanged = true;
|
||||||
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
||||||
RunSmokeCases(module);
|
RunSmokeCases(module);
|
||||||
Console.WriteLine("original Behavior.dll distributor tick oracle passed (RVA 0x19e80; only GetSystemArealMap IAT substituted; this does not compare Rust parity).");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception error) {
|
catch (Exception error) {
|
||||||
@@ -301,18 +341,30 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
uint ignored;
|
uint ignored;
|
||||||
VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), oldProtection, out ignored);
|
VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), oldProtection, out ignored);
|
||||||
}
|
}
|
||||||
GC.KeepAlive(AreaMapGetterCallback); GC.KeepAlive(MapLookupCallback);
|
GC.KeepAlive(AreaMapGetterCallback); GC.KeepAlive(VectorInsertCallback); GC.KeepAlive(x87ControlWordReader); GC.KeepAlive(x87ControlWordWriter); GC.KeepAlive(MapLookupCallback);
|
||||||
GC.KeepAlive(ObjectStateCallback); GC.KeepAlive(ObjectClassCallback);
|
GC.KeepAlive(ObjectStateCallback); GC.KeepAlive(ObjectClassCallback);
|
||||||
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
|
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
|
||||||
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
|
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
|
||||||
GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback);
|
GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback);
|
||||||
if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease))
|
if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease))
|
||||||
throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
|
throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
|
||||||
|
if (x87ControlWordCell != IntPtr.Zero) { Marshal.FreeHGlobal(x87ControlWordCell); x87ControlWordCell = IntPtr.Zero; }
|
||||||
FreeLibrary(module);
|
FreeLibrary(module);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunSmokeCases(IntPtr module) {
|
private static void RunSmokeCases(IntPtr module) {
|
||||||
|
ushort savedControlWord = CurrentX87ControlWord();
|
||||||
|
Console.WriteLine("x87-control-word-before=0x" + savedControlWord.ToString("x4") + " (x86 oracle process)");
|
||||||
|
try {
|
||||||
|
SetX87ControlWord(0x007f);
|
||||||
|
Console.WriteLine("native-add-normalize-x87-control-word=0x" + CurrentX87ControlWord().ToString("x4"));
|
||||||
|
RunNativeAddCases(module);
|
||||||
|
RunNativeNormalizeCases(module);
|
||||||
|
} finally {
|
||||||
|
SetX87ControlWord(savedControlWord);
|
||||||
|
Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4"));
|
||||||
|
}
|
||||||
TickDelegate tick = (TickDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, TickRva), typeof(TickDelegate));
|
TickDelegate tick = (TickDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, TickRva), typeof(TickDelegate));
|
||||||
using (Fixture empty = NewFixture()) {
|
using (Fixture empty = NewFixture()) {
|
||||||
empty.MakeDistributor(); active = empty; tick(empty.Distributor, 0.0f);
|
empty.MakeDistributor(); active = empty; tick(empty.Distributor, 0.0f);
|
||||||
@@ -335,6 +387,137 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
RunSkippedCase(tick);
|
RunSkippedCase(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ushort CurrentX87ControlWord() {
|
||||||
|
if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero)
|
||||||
|
throw new InvalidOperationException("x87 control-word reader is not initialized");
|
||||||
|
x87ControlWordReader();
|
||||||
|
return unchecked((ushort)Marshal.ReadInt16(x87ControlWordCell));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetX87ControlWord(ushort value) {
|
||||||
|
if (x87ControlWordWriter == null || x87ControlWordCell == IntPtr.Zero)
|
||||||
|
throw new InvalidOperationException("x87 control-word writer is not initialized");
|
||||||
|
Marshal.WriteInt16(x87ControlWordCell, unchecked((short)value));
|
||||||
|
x87ControlWordWriter();
|
||||||
|
Expect(CurrentX87ControlWord() == value, "x87 control word 0x" + value.ToString("x4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddWithGameControl(NativeAddObject add, IntPtr distributor, uint key) {
|
||||||
|
Expect(CurrentX87ControlWord() == 0x007f, "game x87 control word before native Add");
|
||||||
|
add(distributor, key);
|
||||||
|
Expect(CurrentX87ControlWord() == 0x007f, "game x87 control word after native Add");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NormalizeWithGameControl(NativeNormalize normalize, IntPtr distributor, uint key) {
|
||||||
|
Expect(CurrentX87ControlWord() == 0x007f, "game x87 control word before native normalize");
|
||||||
|
normalize(distributor, key);
|
||||||
|
Expect(CurrentX87ControlWord() == 0x007f, "game x87 control word after native normalize");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeAddCases(IntPtr module) {
|
||||||
|
NativeAddObject add = (NativeAddObject)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, AddObjectRva), typeof(NativeAddObject));
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
fixture.MakeDistributor();
|
||||||
|
active = fixture;
|
||||||
|
AddWithGameControl(add, fixture.Distributor, 0x80000004);
|
||||||
|
Expect(Marshal.ReadInt32(fixture.Distributor, 0x20) == 1, "Add inserts a new first key");
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x80000004, 0x3f800000, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
AddWithGameControl(add, fixture.Distributor, 0x80000006);
|
||||||
|
Expect(Marshal.ReadInt32(fixture.Distributor, 0x20) == 2, "Add appends a second key");
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x80000004, 0x3f000000, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x80000006, 0x3f000000, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
WriteEntry(fixture.Entries, 0, 0x80000004, 0.2f, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5);
|
||||||
|
AddWithGameControl(add, fixture.Distributor, 0x80000004);
|
||||||
|
Expect(Marshal.ReadInt32(fixture.Distributor, 0x20) == 2, "Add on an existing key does not append");
|
||||||
|
Expect(fixture.VectorInsertCalls == 2, "Add inserts exactly the two new keys");
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x80000004, 0x3f000000, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5);
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x80000006, 0x3f000000, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
AddWithGameControl(add, fixture.Distributor, 0x80000008);
|
||||||
|
Expect(Marshal.ReadInt32(fixture.Distributor, 0x20) == 3, "Add appends a third key");
|
||||||
|
Expect(fixture.VectorInsertCalls == 3, "Add inserts each new key exactly once");
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x80000004, 0x3f000000, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5);
|
||||||
|
uint[] third = ReadEntry(IntPtr.Add(fixture.Entries, 0x38));
|
||||||
|
Console.WriteLine("native-add-third-share: " + Float(third[1]) + " bits=" + Hex(third[1]));
|
||||||
|
Expect(third[0] == 0x80000008 && third[2] == 0 && third[3] == 0 && third[4] == 0 && third[5] == 0 && third[6] == 0,
|
||||||
|
"Add initializes the third record apart from its observed share");
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x80000006, 0x3e2aaaa8, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 2, 0x80000008, 0x3eaaaaab, 0, 0, 0, 0, 0);
|
||||||
|
Console.WriteLine("native-add: count=3 inserts=" + fixture.VectorInsertCalls + " trace=" + String.Join(",", fixture.Trace.ToArray()) +
|
||||||
|
" entries=[" + FormatEntry(fixture.Entries) + ";" + FormatEntry(IntPtr.Add(fixture.Entries, 0x1c)) + ";" +
|
||||||
|
FormatEntry(IntPtr.Add(fixture.Entries, 0x38)) + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeNormalizeCases(IntPtr module) {
|
||||||
|
NativeNormalize normalize = (NativeNormalize)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, NormalizeRva), typeof(NativeNormalize));
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
fixture.MakeDistributor(0x11, 0x22, 0x33);
|
||||||
|
active = fixture;
|
||||||
|
WriteEntry(fixture.Entries, 0, 0x11, 0.2f, 0, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd);
|
||||||
|
WriteEntry(fixture.Entries, 1, 0x22, 0.3f, 1, 0x11111111, 0x22222222, 0x33333333, 0x44444444);
|
||||||
|
WriteEntry(fixture.Entries, 2, 0x33, 0.3f, 0, 0x55555555, 0x66666666, 0x77777777, 0x88888888);
|
||||||
|
NormalizeWithGameControl(normalize, fixture.Distributor, 0x11);
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x11, FloatBitsUnsigned(0.2f), 0, 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd);
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x22, FloatBitsUnsigned(0.3f), 1, 0x11111111, 0x22222222, 0x33333333, 0x44444444);
|
||||||
|
ExpectEntry(fixture.Entries, 2, 0x33, 0x3f000000, 0, 0x55555555, 0x66666666, 0x77777777, 0x88888888);
|
||||||
|
Console.WriteLine("native-normalize-pinned: shares=[" + Float(ReadEntry(fixture.Entries)[1]) + "," +
|
||||||
|
Float(ReadEntry(IntPtr.Add(fixture.Entries, 0x1c))[1]) + "," +
|
||||||
|
Float(ReadEntry(IntPtr.Add(fixture.Entries, 0x38))[1]) + "] flags=[0,1,0]");
|
||||||
|
}
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
fixture.MakeDistributor(0x41, 0x42, 0x43);
|
||||||
|
active = fixture;
|
||||||
|
WriteEntry(fixture.Entries, 0, 0x41, 0.25f, 0, 0, 0, 0, 0);
|
||||||
|
WriteEntry(fixture.Entries, 1, 0x42, 0.0f, 0, 0, 0, 0, 0);
|
||||||
|
WriteEntry(fixture.Entries, 2, 0x43, 0.0f, 0, 0, 0, 0, 0);
|
||||||
|
NormalizeWithGameControl(normalize, fixture.Distributor, 0x41);
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x41, 0x3e800000, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x42, 0x3ec00000, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 2, 0x43, 0x3ec00000, 0, 0, 0, 0, 0);
|
||||||
|
Console.WriteLine("native-normalize-zero-free-sum: shares=[0.25,0.375,0.375]");
|
||||||
|
}
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
fixture.MakeDistributor(0x51, 0x52, 0x53);
|
||||||
|
active = fixture;
|
||||||
|
WriteEntry(fixture.Entries, 0, 0x51, 0.5f, 0, 0, 0, 0, 0);
|
||||||
|
WriteEntry(fixture.Entries, 1, 0x52, 1.0f, 0, 0, 0, 0, 0);
|
||||||
|
WriteEntry(fixture.Entries, 2, 0x53, 5.9604644775390625e-8f, 0, 0, 0, 0, 0);
|
||||||
|
NormalizeWithGameControl(normalize, fixture.Distributor, 0x51);
|
||||||
|
uint[] nearFirst = ReadEntry(fixture.Entries);
|
||||||
|
uint[] nearSecond = ReadEntry(IntPtr.Add(fixture.Entries, 0x1c));
|
||||||
|
uint[] nearThird = ReadEntry(IntPtr.Add(fixture.Entries, 0x38));
|
||||||
|
Console.WriteLine("native-normalize-near-rounding: shares=[" + Float(nearFirst[1]) + "," + Float(nearSecond[1]) + "," + Float(nearThird[1]) + "] bits=[" + Hex(nearFirst[1]) + "," + Hex(nearSecond[1]) + "," + Hex(nearThird[1]) + "]");
|
||||||
|
ExpectEntry(fixture.Entries, 0, 0x51, 0x3f000000, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 1, 0x52, 0x3f000000, 0, 0, 0, 0, 0);
|
||||||
|
ExpectEntry(fixture.Entries, 2, 0x53, 0x33000000, 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteEntry(IntPtr entries, int index, uint key, float weight, uint pinned,
|
||||||
|
uint word3, uint word4, uint word5, uint word6) {
|
||||||
|
IntPtr entry = IntPtr.Add(entries, index * 0x1c);
|
||||||
|
Marshal.WriteInt32(entry, 0x00, unchecked((int)key));
|
||||||
|
Marshal.WriteInt32(entry, 0x04, FloatBits(weight));
|
||||||
|
Marshal.WriteInt32(entry, 0x08, unchecked((int)pinned));
|
||||||
|
Marshal.WriteInt32(entry, 0x0c, unchecked((int)word3));
|
||||||
|
Marshal.WriteInt32(entry, 0x10, unchecked((int)word4));
|
||||||
|
Marshal.WriteInt32(entry, 0x14, unchecked((int)word5));
|
||||||
|
Marshal.WriteInt32(entry, 0x18, unchecked((int)word6));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExpectEntry(IntPtr entries, int index, uint key, uint weight, uint pinned,
|
||||||
|
uint word3, uint word4, uint word5, uint word6) {
|
||||||
|
uint[] words = ReadEntry(IntPtr.Add(entries, index * 0x1c));
|
||||||
|
Expect(words[0] == key && words[1] == weight && words[2] == pinned && words[3] == word3 &&
|
||||||
|
words[4] == word4 && words[5] == word5 && words[6] == word6,
|
||||||
|
"native entry " + index + " fields [" + String.Join(",", Array.ConvertAll(words, Hex)) + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint FloatBitsUnsigned(float value) { return unchecked((uint)FloatBits(value)); }
|
||||||
|
|
||||||
private static Fixture NewFixture() {
|
private static Fixture NewFixture() {
|
||||||
Fixture fixture = new Fixture();
|
Fixture fixture = new Fixture();
|
||||||
fixture.ManagerVtable = Marshal.AllocHGlobal(0x60); fixture.Allocations.Add(fixture.ManagerVtable); Zero(fixture.ManagerVtable, 0x60);
|
fixture.ManagerVtable = Marshal.AllocHGlobal(0x60); fixture.Allocations.Add(fixture.ManagerVtable); Zero(fixture.ManagerVtable, 0x60);
|
||||||
@@ -570,6 +753,29 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Marshal.Copy(code, 0, thunk, code.Length);
|
Marshal.Copy(code, 0, thunk, code.Length);
|
||||||
return thunk;
|
return thunk;
|
||||||
}
|
}
|
||||||
|
private static IntPtr WriteThisCallThreeArgsToStdcallThunk(int offset, IntPtr stdcallTarget) {
|
||||||
|
// Adapt ECX=self plus three stack arguments to stdcall(self, arg1, arg2, arg3).
|
||||||
|
byte[] code = { 0xff, 0x74, 0x24, 0x0c, 0xff, 0x74, 0x24, 0x0c, 0xff, 0x74, 0x24, 0x0c,
|
||||||
|
0x51, 0xb8, 0, 0, 0, 0, 0xff, 0xd0, 0xc2, 0x0c, 0x00 };
|
||||||
|
WriteImm32(code, 14, stdcallTarget.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());
|
||||||
|
IntPtr thunk = IntPtr.Add(fastcallThunks, offset);
|
||||||
|
Marshal.Copy(code, 0, thunk, code.Length);
|
||||||
|
return thunk;
|
||||||
|
}
|
||||||
|
private static IntPtr WriteX87ControlWordWriter(int offset) {
|
||||||
|
byte[] code = { 0xd9, 0x2d, 0, 0, 0, 0, 0xc3 }; // fldcw [absolute address]; ret
|
||||||
|
WriteImm32(code, 2, x87ControlWordCell.ToInt32());
|
||||||
|
IntPtr thunk = IntPtr.Add(fastcallThunks, offset);
|
||||||
|
Marshal.Copy(code, 0, thunk, code.Length);
|
||||||
|
return thunk;
|
||||||
|
}
|
||||||
private static void WriteImm32(byte[] code, int offset, int value) {
|
private static void WriteImm32(byte[] code, int offset, int value) {
|
||||||
byte[] bytes = BitConverter.GetBytes(value);
|
byte[] bytes = BitConverter.GetBytes(value);
|
||||||
Buffer.BlockCopy(bytes, 0, code, offset, 4);
|
Buffer.BlockCopy(bytes, 0, code, offset, 4);
|
||||||
@@ -585,7 +791,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
foreach (byte value in digest) hex.Append(value.ToString("x2")); return hex.ToString();
|
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 tick oracle failed: " + message); }
|
private static void Expect(bool value, string message) { if (!value) throw new InvalidOperationException("original Behavior oracle failed: " + message); }
|
||||||
private static void ExpectLookupOrder(Fixture fixture, params string[] keys) {
|
private static void ExpectLookupOrder(Fixture fixture, params string[] keys) {
|
||||||
List<string> actual = new List<string>();
|
List<string> actual = new List<string>();
|
||||||
foreach (string item in fixture.Trace) if (item.StartsWith("lookup:", StringComparison.Ordinal)) actual.Add(item.Substring(7));
|
foreach (string item in fixture.Trace) if (item.StartsWith("lookup:", StringComparison.Ordinal)) actual.Add(item.Substring(7));
|
||||||
|
|||||||
@@ -157,13 +157,17 @@ Behavior (`Behavior.dll`, RVA `0x3600`) вызывает `World3D!stdGetGameMode
|
|||||||
подтверждает только состав списков, не точную эквивалентность всей загрузки или
|
подтверждает только состав списков, не точную эквивалентность всей загрузки или
|
||||||
регулярного шага Behavior.
|
регулярного шага Behavior.
|
||||||
|
|
||||||
Нативный `Behavior.dll!Add` (RVA `0x1aac0`) создаёт для каждого участника запись
|
Нативный `Behavior.dll!Add` (RVA `0x1aac0`) и `normalize` (RVA `0x1ac90`)
|
||||||
размером `0x1c` с начальной долей `1 / count`, а `normalize` (RVA `0x1ac90`)
|
проверены прямыми x86-вызовами на синтетических записях при x87 control word
|
||||||
перенормирует прежние доли после добавления. Rust пока хранит только членство:
|
`0x007f`, измеренном также у игры на входах обоих методов. Rust повторяет
|
||||||
поля записи, доли и их изменение ещё не исполняются. Не восстановлено и
|
начальные доли и нормализацию отдельными операциями `f32`, а повторную запись
|
||||||
соединение с реальными интерфейсами ArealMap, необходимыми для регулярного
|
сопоставляет по `ObjectHandle`, меняя только долю существующего участника.
|
||||||
вызова `FUN_10019e80`; остальной Behavior tick flow и общий dispatcher событий
|
Пять DWORD после доли хранятся сырыми; ненулевое первое слово закрепляет вес,
|
||||||
также остаются открытыми.
|
смысл четырёх следующих не установлен. Лицензированный AutoDemo-тест проверяет
|
||||||
|
список TMA-объектов и получившиеся начальные доли по кланам, но это не сравнение
|
||||||
|
живых весов миссии с полями исходных runtime-записей. Не восстановлено соединение
|
||||||
|
с реальными интерфейсами ArealMap, нужными `FUN_10019e80`; полный Behavior tick
|
||||||
|
flow и общий dispatcher событий также остаются открытыми.
|
||||||
|
|
||||||
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
||||||
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
||||||
|
|||||||
@@ -441,15 +441,36 @@ selector `1` на интерфейсе получателя становится
|
|||||||
`0x3f2aaaab`). Эти векторы проверяют арифметику исходной функции, но не
|
`0x3f2aaaab`). Эти векторы проверяют арифметику исходной функции, но не
|
||||||
распределение реальной карты.
|
распределение реальной карты.
|
||||||
|
|
||||||
Текущие типы Rust не дают честно наполнить этот список: мир хранит
|
Полная загрузка Rust теперь собирает этот список из runtime `ObjectHandle` и
|
||||||
идентификаторы и преобразования объектов, но не нужные tick ресурсные поля;
|
сохраняет веса вместе с пятью исходными DWORD хвоста каждой записи. Членство
|
||||||
`MissionObjectDraft.properties` сохраняет сырые слова без связи с QI
|
строится только для полного режима; preview остаётся статическим. Это пока не
|
||||||
`0x16`/`0x204` или свойством `0x02000100`. Методы добавления и перенастройки
|
даёт ресурсных полей и интерфейсов ArealMap для `FUN_10019e80`, а
|
||||||
строк `FUN_1001a9a0` и `FUN_1001aac0` доступны через entries vtable
|
`step_headless` по-прежнему шагает только `World`.
|
||||||
`0x10059794` по смещениям `+0x0c` и `+0x10`; кто именно вызывает эти методы и
|
|
||||||
какие значения передаёт, пока не установлено. TMA-anchors и сырые preview
|
### Добавление и нормализация долей
|
||||||
drafts не используются вместо неизвестной регистрации. `step_headless` по-прежнему
|
|
||||||
шагает только `World`.
|
В этой же GOG `Behavior.dll` (SHA-256
|
||||||
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`)
|
||||||
|
`FUN_1001aac0` (RVA `0x1aac0`) добавляет запись размером `0x1c`. Её слово по
|
||||||
|
`+0x04` — доля `f32`, а слова `+0x08..+0x18` копируются как сырой хвост.
|
||||||
|
Новая запись начинается с доли `1 / count` и нулевого хвоста. При повторном
|
||||||
|
ключе запись не вставляется снова: меняется только её доля, остальные пять
|
||||||
|
DWORD сохраняются. После каждого вызова выполняется `FUN_1001ac90` (RVA
|
||||||
|
`0x1ac90`). Она считает долю целевого ключа и записей с ненулевым первым словом
|
||||||
|
хвоста фиксированной. Если сумма остальных долей положительна, остаток от
|
||||||
|
единицы распределяется пропорционально; при нулевой или отрицательной сумме он
|
||||||
|
делится поровну между изменяемыми записями. Проверенный ветвящийся путь также
|
||||||
|
направляет unordered-сумму в пропорциональную ветвь, но стандартный набор этих
|
||||||
|
тестов содержит конечные значения.
|
||||||
|
|
||||||
|
Инструкции Add и normalize выполнялись при x87 control word `0x007f`, то есть
|
||||||
|
с 24-битной точностью мантиссы; такой же CW измерен у оригинальной игры на
|
||||||
|
входах обоих методов. Поэтому Rust повторяет их в порядке списка отдельными
|
||||||
|
операциями `f32`, без раннего перехода в `f64`. Native x86 oracle проверяет
|
||||||
|
три последовательных Add, повторный ключ с sentinel-хвостом, фиксированную
|
||||||
|
запись, нулевую сумму и пограничный вектор округления. В последнем доли имеют
|
||||||
|
биты `[0x3f000000, 0x3f000000, 0x33000000]`. Эти сценарии сверяют локальную
|
||||||
|
арифметику на стандартных конечных данных, а не весь игровый запуск.
|
||||||
|
|
||||||
Для проверки использованы три x86 helper-а на синтетической памяти. Caller
|
Для проверки использованы три x86 helper-а на синтетической памяти. Caller
|
||||||
oracle вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование
|
oracle вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование
|
||||||
@@ -485,7 +506,8 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
```
|
```
|
||||||
|
|
||||||
Отдельный oracle distributor tick компилируется и запускается так:
|
Native oracle Add, normalize и distributor tick компилируется и запускается
|
||||||
|
так:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:FPARKAN_ORIGINAL_BEHAVIOR_DLL = 'C:\GOG Games\Parkan - Iron Strategy\Behavior.dll'
|
$env:FPARKAN_ORIGINAL_BEHAVIOR_DLL = 'C:\GOG Games\Parkan - Iron Strategy\Behavior.dll'
|
||||||
@@ -498,8 +520,12 @@ exit $LASTEXITCODE
|
|||||||
|
|
||||||
Oracle проверяет SHA-256 оригинального `Behavior.dll`:
|
Oracle проверяет SHA-256 оригинального `Behavior.dll`:
|
||||||
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`. Harness
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`. Harness
|
||||||
вызывает функцию по RVA `0x19e80` при единственной подмене импортного
|
явно устанавливает x87 CW `0x007f` для вызовов Add (`0x1aac0`) и normalize
|
||||||
слота `GetSystemArealMap`; машинный код tick не патчится. Проверяются нулевой
|
(`0x1ac90`), проверяет CW до и после каждого вызова и восстанавливает прежнее
|
||||||
|
значение перед tick suite. Для этих двух методов подменяется только bounded
|
||||||
|
вставка синтетического vector; их машинный код не патчится. Для tick helper
|
||||||
|
подменяет только IAT-слот `GetSystemArealMap` и вызывает RVA `0x19e80`.
|
||||||
|
Проверяются нулевой
|
||||||
шаг, обе ветви селекторов скорости, пропуск состояния `0xfffe`, missing lookup,
|
шаг, обе ветви селекторов скорости, пропуск состояния `0xfffe`, missing lookup,
|
||||||
динамическое повторное чтение `0x1002`, ограниченный запас руды, разные веса,
|
динамическое повторное чтение `0x1002`, ограниченный запас руды, разные веса,
|
||||||
достаточное и недостаточное значение мощности. Это синтетические ArealMap
|
достаточное и недостаточное значение мощности. Это синтетические ArealMap
|
||||||
|
|||||||
+20
-10
@@ -207,7 +207,9 @@ SuperAI для записей кланов, затем обновляет Behavi
|
|||||||
исполнен на исходной DLL с искусственными объектами. Rust пока не вызывает
|
исполнен на исходной DLL с искусственными объектами. Rust пока не вызывает
|
||||||
регулярный шаг: игровой мир ещё не предоставляет нужные ресурсные интерфейсы.
|
регулярный шаг: игровой мир ещё не предоставляет нужные ресурсные интерфейсы.
|
||||||
При полной загрузке Rust уже восстанавливает членство объектов в клановых
|
При полной загрузке Rust уже восстанавливает членство объектов в клановых
|
||||||
списках Behavior, но сами записи распределителя и их доли пока не моделирует.
|
списках Behavior и задаёт их начальные доли. Остальные слова записи хранятся
|
||||||
|
сырыми: семантика только первого из пяти слов хвоста установлена для
|
||||||
|
нормализации.
|
||||||
Runtime также не подменяет очередь оригинала: вызывающая сторона сама передаёт
|
Runtime также не подменяет очередь оригинала: вызывающая сторона сама передаёт
|
||||||
время, а локальный генератор Rust пока учитывает только случайные числа
|
время, а локальный генератор Rust пока учитывает только случайные числа
|
||||||
восстановленного стартового шага.
|
восстановленного стартового шага.
|
||||||
@@ -241,10 +243,17 @@ Rust-тест повторно читает лицензированную ми
|
|||||||
TMA-записями и проверяет реальные `ObjectHandle`, но помечен `#[ignore]`, чтобы
|
TMA-записями и проверяет реальные `ObjectHandle`, но помечен `#[ignore]`, чтобы
|
||||||
не требовать оригинальных ресурсов от остальных разработчиков.
|
не требовать оригинальных ресурсов от остальных разработчиков.
|
||||||
|
|
||||||
Это пока только перечень участников. Оригинальный `Behavior.dll!Add` создаёт для
|
Список распределителя хранит не только участников, но и долю каждого. При
|
||||||
каждого участника запись размером `0x1c` с долей `1 / count`, после чего
|
добавлении двух объектов оба получают по половине общего веса. Если добавить
|
||||||
нормирует прежние доли. Rust ещё не хранит эти поля и не исполняет их регулярный
|
третий, движок оставляет долю новой записи около одной трети, а оставшийся вес
|
||||||
обмен ресурсами. Поля не выводятся из TMA-соседнего слова или `OwnerId`.
|
делит между прежними изменяемыми записями. Помеченная запись сохраняет свой вес,
|
||||||
|
как закреплённая часть общего набора. Повторное добавление того же объекта не
|
||||||
|
создаёт вторую строку и оставляет остальные слова записи нетронутыми.
|
||||||
|
|
||||||
|
Rust хранит этот небольшой начальный список с долями. Он не смешивает selector
|
||||||
|
из TMA, владельца мирового объекта и внутренний ключ Behavior: связь в Rust
|
||||||
|
опирается на уже созданный `ObjectHandle`. Регулярный обмен ресурсами пока
|
||||||
|
остаётся отдельным этапом, потому что для него нужны интерфейсы игровых объектов.
|
||||||
|
|
||||||
### Регулярная передача ресурсов
|
### Регулярная передача ресурсов
|
||||||
|
|
||||||
@@ -263,11 +272,12 @@ Resource Distributor работает со списком зарегистрир
|
|||||||
исходной `Behavior.dll`, но объекты и ответы интерфейсов заданы тестом; это ещё
|
исходной `Behavior.dll`, но объекты и ответы интерфейсов заданы тестом; это ещё
|
||||||
не воспроизведение реальной миссии.
|
не воспроизведение реальной миссии.
|
||||||
|
|
||||||
Членство списка связано с реальным путём регистрации, но выполнение этой
|
Для правила долей есть отдельная проверка исходной `Behavior.dll` на синтетических
|
||||||
миссии не сравнивалось с тем, как исходная DLL заполняет поля записи `0x1c` и
|
записях при игровом x87 control word `0x007f`: проверены добавление, повторный
|
||||||
нормирует доли. Rust пока не хранит эти поля, а игровому миру ещё не хватает
|
ключ, закреплённая запись, нулевая сумма и округление. Это позволяет Rust
|
||||||
ресурсных интерфейсов ArealMap для регулярного шага. Открытые вопросы и границы
|
сохранять начальный список, но пока не подтверждает веса реальных объектов в
|
||||||
проверки приведены в [границах знания](../appendices/knowledge-boundaries.md).
|
живой миссии. Для регулярного шага по-прежнему не хватает интерфейсов ArealMap.
|
||||||
|
Остальные ограничения собраны в [границах знания](../appendices/knowledge-boundaries.md).
|
||||||
|
|
||||||
Первый часто встречающийся table entry с side effect — `Handler(2)` (176
|
Первый часто встречающийся table entry с side effect — `Handler(2)` (176
|
||||||
records в corpus). Он берёт active instruction, разрешает семь slots через
|
records в corpus). Он берёт active instruction, разрешает семь slots через
|
||||||
|
|||||||
Reference in new issue
Block a user