test(runtime): verify native resource distributor tick
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
This commit is contained in:
4 files changed
+709
-6
No files matched your search
@@ -0,0 +1,603 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
// Calls the original x86 Behavior.dll distributor tick against synthetic vtables.
|
||||||
|
// Only the imported GetSystemArealMap IAT slot is replaced; the tick instructions
|
||||||
|
// and its internal helpers remain the installed DLL's original code.
|
||||||
|
internal static class BehaviorDistributorTickOracle {
|
||||||
|
private const uint LoadWithAlteredSearchPath = 0x00000008;
|
||||||
|
private const uint MemCommitReserve = 0x00003000;
|
||||||
|
private const uint PageExecuteReadWrite = 0x40;
|
||||||
|
private const uint MemRelease = 0x00008000;
|
||||||
|
private const uint PageReadWrite = 0x04;
|
||||||
|
private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99";
|
||||||
|
private const int TickRva = 0x19e80;
|
||||||
|
private const int GetSystemArealMapIatRva = 0x59008;
|
||||||
|
private const uint ResourceProperty = 0x02000100;
|
||||||
|
private static Fixture active;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern IntPtr LoadLibraryEx(string fileName, IntPtr file, uint flags);
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool FreeLibrary(IntPtr module);
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAlloc(IntPtr address, UIntPtr size, uint allocationType, uint protect);
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualFree(IntPtr address, UIntPtr size, uint freeType);
|
||||||
|
[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.ThisCall)] private delegate void TickDelegate(IntPtr self, float deltaSeconds);
|
||||||
|
[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 uint ObjectClass(IntPtr self);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int ObjectQueryInterface(IntPtr self, uint iid, IntPtr result);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr ObjectRate(IntPtr self, uint selector);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate float ObjectGetProperty(IntPtr self, uint property, IntPtr result);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void ObjectSetProperty(IntPtr self, uint property, float value, IntPtr result);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceGetProperty(IntPtr self, uint selector, IntPtr result);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceSetProperty(IntPtr self, uint selector, IntPtr value);
|
||||||
|
|
||||||
|
private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap;
|
||||||
|
private static readonly MapLookup MapLookupCallback = Lookup;
|
||||||
|
private static readonly ObjectState ObjectStateCallback = GetObjectState;
|
||||||
|
private static readonly ObjectClass ObjectClassCallback = GetObjectClass;
|
||||||
|
private static readonly ObjectQueryInterface ObjectQueryCallback = QueryObject;
|
||||||
|
private static readonly ObjectRate ObjectRateCallback = GetRate;
|
||||||
|
private static readonly ObjectGetProperty ObjectGetPropertyCallback = GetProperty;
|
||||||
|
private static readonly ObjectSetProperty ObjectSetPropertyCallback = SetProperty;
|
||||||
|
private static readonly InterfaceGetProperty InterfaceGetCallback = GetInterfaceProperty;
|
||||||
|
private static readonly InterfaceSetProperty InterfaceSetCallback = SetInterfaceProperty;
|
||||||
|
private static IntPtr fastcallThunks;
|
||||||
|
private static IntPtr queryObjectThunk;
|
||||||
|
private static IntPtr getInterfacePropertyThunk;
|
||||||
|
private static IntPtr setInterfacePropertyThunk;
|
||||||
|
|
||||||
|
private sealed class NativeObject {
|
||||||
|
public IntPtr Self;
|
||||||
|
public IntPtr Vtable;
|
||||||
|
public IntPtr Interface16;
|
||||||
|
public IntPtr Interface204;
|
||||||
|
public IntPtr Interface16Vtable;
|
||||||
|
public IntPtr Interface204Vtable;
|
||||||
|
public IntPtr Rate1005;
|
||||||
|
public IntPtr Rate1002;
|
||||||
|
public uint Key;
|
||||||
|
public uint Class;
|
||||||
|
public int State;
|
||||||
|
public float Property;
|
||||||
|
public int PropertyMetadata;
|
||||||
|
public float PowerRate;
|
||||||
|
public float OreRate;
|
||||||
|
public float[] OreRateSequence;
|
||||||
|
public int PowerRateReads;
|
||||||
|
public int OreRateReads;
|
||||||
|
public float InterfaceSelector1;
|
||||||
|
public float InterfaceSelector2;
|
||||||
|
public int Selector1Reads;
|
||||||
|
public int Selector2Reads;
|
||||||
|
public int Writes;
|
||||||
|
public readonly List<string> Calls = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Fixture : IDisposable {
|
||||||
|
public IntPtr Distributor;
|
||||||
|
public IntPtr Entries;
|
||||||
|
public IntPtr Manager;
|
||||||
|
public IntPtr ManagerVtable;
|
||||||
|
public readonly List<IntPtr> Allocations = new List<IntPtr>();
|
||||||
|
public readonly Dictionary<uint, NativeObject> ByKey = new Dictionary<uint, NativeObject>();
|
||||||
|
public readonly List<string> Calls = new List<string>();
|
||||||
|
public readonly List<string> Trace = new List<string>();
|
||||||
|
public int LookupCount;
|
||||||
|
public NativeObject AddObject(uint key, uint classCode, float powerRate, float oreRate, float property, int state) {
|
||||||
|
NativeObject item = new NativeObject();
|
||||||
|
item.Key = key;
|
||||||
|
item.Class = classCode;
|
||||||
|
item.PowerRate = powerRate;
|
||||||
|
item.OreRate = oreRate;
|
||||||
|
item.Property = property;
|
||||||
|
item.State = state;
|
||||||
|
item.InterfaceSelector1 = 1.0f;
|
||||||
|
item.InterfaceSelector2 = 1.0f;
|
||||||
|
item.Vtable = Allocate(0x70);
|
||||||
|
item.Self = Allocate(0x80);
|
||||||
|
item.Interface16Vtable = Allocate(0x20);
|
||||||
|
item.Interface204Vtable = Allocate(0x20);
|
||||||
|
item.Interface16 = Allocate(IntPtr.Size);
|
||||||
|
item.Interface204 = Allocate(IntPtr.Size);
|
||||||
|
item.Rate1005 = Allocate(sizeof(float));
|
||||||
|
item.Rate1002 = Allocate(sizeof(float));
|
||||||
|
Marshal.WriteIntPtr(item.Self, 0, item.Vtable);
|
||||||
|
Marshal.WriteIntPtr(item.Interface16, 0, item.Interface16Vtable);
|
||||||
|
Marshal.WriteIntPtr(item.Interface204, 0, item.Interface204Vtable);
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x00, queryObjectThunk);
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x38, Marshal.GetFunctionPointerForDelegate(ObjectClassCallback));
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x44, Marshal.GetFunctionPointerForDelegate(ObjectStateCallback));
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x48, Marshal.GetFunctionPointerForDelegate(ObjectSetPropertyCallback));
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x4c, Marshal.GetFunctionPointerForDelegate(ObjectGetPropertyCallback));
|
||||||
|
Marshal.WriteIntPtr(item.Vtable, 0x68, Marshal.GetFunctionPointerForDelegate(ObjectRateCallback));
|
||||||
|
Marshal.WriteIntPtr(item.Interface16Vtable, 0x10, getInterfacePropertyThunk);
|
||||||
|
Marshal.WriteIntPtr(item.Interface16Vtable, 0x14, setInterfacePropertyThunk);
|
||||||
|
Marshal.WriteIntPtr(item.Interface204Vtable, 0x10, getInterfacePropertyThunk);
|
||||||
|
Marshal.WriteIntPtr(item.Interface204Vtable, 0x14, setInterfacePropertyThunk);
|
||||||
|
Marshal.WriteInt32(item.Rate1005, 0, BitConverter.ToInt32(BitConverter.GetBytes(powerRate), 0));
|
||||||
|
Marshal.WriteInt32(item.Rate1002, 0, BitConverter.ToInt32(BitConverter.GetBytes(oreRate), 0));
|
||||||
|
ByKey.Add(key, item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
public void MakeDistributor(params uint[] keys) {
|
||||||
|
Distributor = Allocate(0x44);
|
||||||
|
Entries = Allocate(Math.Max(1, keys.Length) * 0x1c);
|
||||||
|
Marshal.WriteInt32(Distributor, 0x20, keys.Length);
|
||||||
|
Marshal.WriteIntPtr(Distributor, 0x28, Entries);
|
||||||
|
for (int i = 0; i < keys.Length; i++) {
|
||||||
|
IntPtr entry = IntPtr.Add(Entries, i * 0x1c);
|
||||||
|
Marshal.WriteInt32(entry, 0x00, unchecked((int)keys[i]));
|
||||||
|
Marshal.WriteInt32(entry, 0x04, BitConverter.ToInt32(BitConverter.GetBytes(1.0f), 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private IntPtr Allocate(int size) {
|
||||||
|
IntPtr value = Marshal.AllocHGlobal(size);
|
||||||
|
Zero(value, size);
|
||||||
|
Allocations.Add(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
public void Dispose() {
|
||||||
|
active = null;
|
||||||
|
for (int i = Allocations.Count - 1; i >= 0; i--) Marshal.FreeHGlobal(Allocations[i]);
|
||||||
|
Allocations.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntPtr GetAreaMap() { return active.Manager; }
|
||||||
|
private static IntPtr Lookup(IntPtr self, uint key) {
|
||||||
|
active.Calls.Add("lookup:" + key.ToString("x8"));
|
||||||
|
active.LookupCount++;
|
||||||
|
active.Trace.Add("lookup:" + key.ToString("x8"));
|
||||||
|
NativeObject item;
|
||||||
|
return active.ByKey.TryGetValue(key, out item) ? item.Self : IntPtr.Zero;
|
||||||
|
}
|
||||||
|
private static NativeObject Resolve(IntPtr self) {
|
||||||
|
foreach (NativeObject item in active.ByKey.Values) if (item.Self == self) return item;
|
||||||
|
throw new InvalidOperationException("unexpected object pointer 0x" + self.ToString("x"));
|
||||||
|
}
|
||||||
|
private static NativeObject ResolveInterface(IntPtr self, out uint iid) {
|
||||||
|
foreach (NativeObject item in active.ByKey.Values) {
|
||||||
|
if (item.Interface16 == self) { iid = 0x16; return item; }
|
||||||
|
if (item.Interface204 == self) { iid = 0x204; return item; }
|
||||||
|
}
|
||||||
|
iid = 0;
|
||||||
|
throw new InvalidOperationException("unexpected interface pointer 0x" + self.ToString("x"));
|
||||||
|
}
|
||||||
|
private static int GetObjectState(IntPtr self) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
active.Calls.Add("state:" + item.Key.ToString("x8"));
|
||||||
|
active.Trace.Add("state:" + item.Key.ToString("x8"));
|
||||||
|
return item.State;
|
||||||
|
}
|
||||||
|
private static uint GetObjectClass(IntPtr self) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
active.Calls.Add("class:" + item.Key.ToString("x8") + ":" + item.Class.ToString("x8"));
|
||||||
|
active.Trace.Add("class:" + item.Key.ToString("x8") + ":" + item.Class.ToString("x8"));
|
||||||
|
return item.Class;
|
||||||
|
}
|
||||||
|
private static int QueryObject(IntPtr self, uint iid, IntPtr result) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
item.Calls.Add("query:" + iid.ToString("x"));
|
||||||
|
active.Trace.Add("query:" + item.Key.ToString("x8") + ":" + iid.ToString("x"));
|
||||||
|
if (iid == 0x16) Marshal.WriteIntPtr(result, item.Interface16);
|
||||||
|
else if (iid == 0x204) Marshal.WriteIntPtr(result, item.Interface204);
|
||||||
|
else return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
private static IntPtr GetRate(IntPtr self, uint selector) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
if (selector == 0x1005) {
|
||||||
|
float value = item.PowerRate;
|
||||||
|
item.PowerRateReads++;
|
||||||
|
Marshal.WriteInt32(item.Rate1005, BitConverter.ToInt32(BitConverter.GetBytes(value), 0));
|
||||||
|
item.Calls.Add("rate:1005:" + Float(value)); active.Trace.Add("rate:" + item.Key.ToString("x8") + ":1005:" + Float(value));
|
||||||
|
return item.Rate1005;
|
||||||
|
}
|
||||||
|
if (selector == 0x1002) {
|
||||||
|
float value = SequenceValue(item.OreRateSequence, item.OreRateReads++, item.OreRate);
|
||||||
|
Marshal.WriteInt32(item.Rate1002, BitConverter.ToInt32(BitConverter.GetBytes(value), 0));
|
||||||
|
item.Calls.Add("rate:1002:" + Float(value)); active.Trace.Add("rate:" + item.Key.ToString("x8") + ":1002:" + Float(value));
|
||||||
|
return item.Rate1002;
|
||||||
|
}
|
||||||
|
item.Calls.Add("rate:" + selector.ToString("x"));
|
||||||
|
active.Trace.Add("rate:" + item.Key.ToString("x8") + ":" + selector.ToString("x"));
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
private static float GetProperty(IntPtr self, uint property, IntPtr result) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
item.Calls.Add("get:" + property.ToString("x8"));
|
||||||
|
active.Trace.Add("get:" + item.Key.ToString("x8") + ":" + property.ToString("x8"));
|
||||||
|
if (result != IntPtr.Zero) Marshal.WriteInt32(result, item.PropertyMetadata);
|
||||||
|
return item.Property;
|
||||||
|
}
|
||||||
|
private static void SetProperty(IntPtr self, uint property, float value, IntPtr outPointer) {
|
||||||
|
NativeObject item = Resolve(self);
|
||||||
|
item.Calls.Add("set:" + property.ToString("x8") + ":" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture) + ":out=" + outPointer.ToString("x"));
|
||||||
|
active.Trace.Add("set:" + item.Key.ToString("x8") + ":" + property.ToString("x8") + ":" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
if (property == ResourceProperty) { item.Property = value; item.Writes++; }
|
||||||
|
if (property == ResourceProperty && outPointer != IntPtr.Zero) item.PropertyMetadata = Marshal.ReadInt32(outPointer);
|
||||||
|
}
|
||||||
|
private static int GetInterfaceProperty(IntPtr self, uint selector, IntPtr result) {
|
||||||
|
uint iid;
|
||||||
|
NativeObject item = ResolveInterface(self, out iid);
|
||||||
|
float value;
|
||||||
|
if (selector == 1) {
|
||||||
|
item.Selector1Reads++;
|
||||||
|
value = item.InterfaceSelector1;
|
||||||
|
item.Calls.Add("iface-get:" + iid.ToString("x") + ":1:" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
active.Trace.Add("iface-get:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":1:" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
} else if (selector == 2) {
|
||||||
|
item.Selector2Reads++;
|
||||||
|
value = item.InterfaceSelector2;
|
||||||
|
item.Calls.Add("iface-get:" + iid.ToString("x") + ":2:" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
active.Trace.Add("iface-get:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":2:" + value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
} else {
|
||||||
|
item.Calls.Add("iface-get:" + iid.ToString("x") + ":" + selector.ToString("x"));
|
||||||
|
active.Trace.Add("iface-get:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":" + selector.ToString("x"));
|
||||||
|
value = 0.0f;
|
||||||
|
}
|
||||||
|
if (result != IntPtr.Zero) Marshal.WriteInt32(result, 0, BitConverter.ToInt32(BitConverter.GetBytes(value), 0));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
private static int SetInterfaceProperty(IntPtr self, uint selector, IntPtr value) {
|
||||||
|
uint iid;
|
||||||
|
NativeObject item = ResolveInterface(self, out iid);
|
||||||
|
float number = value == IntPtr.Zero ? 0.0f : BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(value)), 0);
|
||||||
|
if (selector == 1) item.InterfaceSelector1 = number;
|
||||||
|
else if (selector == 2) item.InterfaceSelector2 = number;
|
||||||
|
item.Calls.Add("iface-set:" + iid.ToString("x") + ":" + selector.ToString("x") + ":" + number.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
active.Trace.Add("iface-set:" + item.Key.ToString("x8") + ":" + iid.ToString("x") + ":" + selector.ToString("x") + ":" + number.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
private static float SequenceValue(float[] sequence, int index, float fallback) {
|
||||||
|
if (sequence == null || sequence.Length == 0) return fallback;
|
||||||
|
return sequence[Math.Min(index, sequence.Length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Main() {
|
||||||
|
if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
|
||||||
|
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; }
|
||||||
|
string hash = Hash(path);
|
||||||
|
if (hash != ExpectedBehaviorSha256) { Console.Error.WriteLine("Unexpected Behavior.dll SHA-256: " + hash); return 4; }
|
||||||
|
IntPtr module = LoadLibraryEx(path, IntPtr.Zero, LoadWithAlteredSearchPath);
|
||||||
|
if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Behavior.dll) failed: " + Marshal.GetLastWin32Error()); return 5; }
|
||||||
|
IntPtr import = IntPtr.Add(module, GetSystemArealMapIatRva);
|
||||||
|
IntPtr originalImport = Marshal.ReadIntPtr(import);
|
||||||
|
uint oldProtection = 0;
|
||||||
|
bool protectionChanged = false;
|
||||||
|
bool thunksAllocated = false;
|
||||||
|
try {
|
||||||
|
fastcallThunks = VirtualAlloc(IntPtr.Zero, new UIntPtr(0x1000), MemCommitReserve, PageExecuteReadWrite);
|
||||||
|
if (fastcallThunks == IntPtr.Zero) throw new InvalidOperationException("VirtualAlloc(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
|
||||||
|
thunksAllocated = true;
|
||||||
|
queryObjectThunk = WriteFastcallToStdcallThunk(0x00, Marshal.GetFunctionPointerForDelegate(ObjectQueryCallback));
|
||||||
|
getInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x40, Marshal.GetFunctionPointerForDelegate(InterfaceGetCallback));
|
||||||
|
setInterfacePropertyThunk = WriteFastcallToStdcallThunk(0x80, Marshal.GetFunctionPointerForDelegate(InterfaceSetCallback));
|
||||||
|
if (!VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), PageReadWrite, out oldProtection))
|
||||||
|
throw new InvalidOperationException("VirtualProtect(GetSystemArealMap IAT) failed: " + Marshal.GetLastWin32Error());
|
||||||
|
protectionChanged = true;
|
||||||
|
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
||||||
|
RunSmokeCases(module);
|
||||||
|
Console.WriteLine("original Behavior.dll distributor tick oracle passed (RVA 0x19e80; only GetSystemArealMap IAT substituted; this does not compare Rust parity).");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception error) {
|
||||||
|
Console.Error.WriteLine(error.ToString());
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (protectionChanged) {
|
||||||
|
Marshal.WriteIntPtr(import, originalImport);
|
||||||
|
uint ignored;
|
||||||
|
VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), oldProtection, out ignored);
|
||||||
|
}
|
||||||
|
GC.KeepAlive(AreaMapGetterCallback); GC.KeepAlive(MapLookupCallback);
|
||||||
|
GC.KeepAlive(ObjectStateCallback); GC.KeepAlive(ObjectClassCallback);
|
||||||
|
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
|
||||||
|
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
|
||||||
|
GC.KeepAlive(InterfaceGetCallback); GC.KeepAlive(InterfaceSetCallback);
|
||||||
|
if (thunksAllocated && !VirtualFree(fastcallThunks, UIntPtr.Zero, MemRelease))
|
||||||
|
throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error());
|
||||||
|
FreeLibrary(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunSmokeCases(IntPtr module) {
|
||||||
|
TickDelegate tick = (TickDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, TickRva), typeof(TickDelegate));
|
||||||
|
using (Fixture empty = NewFixture()) {
|
||||||
|
empty.MakeDistributor(); active = empty; tick(empty.Distributor, 0.0f);
|
||||||
|
Expect(empty.Calls.Count == 0, "zero delta performs no manager lookups");
|
||||||
|
Console.WriteLine("zero-delta: lookups=0");
|
||||||
|
}
|
||||||
|
RunObjectCase(tick, 0x80000002, 0.5f, 2.0f, 0.0f, 0, 0.0f, "power-80000002");
|
||||||
|
RunObjectCase(tick, 0x80010000, 0.5f, 2.0f, 0.0f, 0, 0.0f, "power-80010000");
|
||||||
|
RunObjectCase(tick, 0x80020000, 0.5f, 2.0f, 0.0f, 0, 0.0f, "power-80020000");
|
||||||
|
RunObjectCase(tick, 0x80040000, 0.5f, 2.0f, 0.0f, 0, 0.0f, "power-80040000");
|
||||||
|
RunObjectCase(tick, 0x80000004, 0.5f, 0.0f, 3.0f, 0, 2.0f, "ore-80000004");
|
||||||
|
RunObjectCase(tick, 0x80000008, 0.5f, 0.0f, 3.0f, 0, 2.0f, "ore-80000008");
|
||||||
|
RunMixedOreCase(tick, "mixed-ore-producer-recipient", 1.0f, 0.5f);
|
||||||
|
RunMixedOreCase(tick, "bounded-ore-store", 0.25f, 0.5f);
|
||||||
|
RunWeightedOreCase(tick);
|
||||||
|
RunMixedPowerCase(tick, "mixed-power-sufficient", 4.0f, 1.0f, 0.5f, 0.75f);
|
||||||
|
RunMixedPowerCase(tick, "mixed-power-shortage", 0.125f, 1.0f, 0.5f, 0.75f);
|
||||||
|
RunMixedPowerCase(tick, "mixed-power-recipient-reserve", 4.0f, 8.0f, 0.5f, 0.75f);
|
||||||
|
RunMissingCase(tick);
|
||||||
|
RunSkippedCase(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture NewFixture() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
fixture.ManagerVtable = Marshal.AllocHGlobal(0x60); fixture.Allocations.Add(fixture.ManagerVtable); Zero(fixture.ManagerVtable, 0x60);
|
||||||
|
fixture.Manager = Marshal.AllocHGlobal(0x80); fixture.Allocations.Add(fixture.Manager); Zero(fixture.Manager, 0x80);
|
||||||
|
Marshal.WriteIntPtr(fixture.Manager, 0, fixture.ManagerVtable);
|
||||||
|
Marshal.WriteIntPtr(fixture.ManagerVtable, 0x54, Marshal.GetFunctionPointerForDelegate(MapLookupCallback));
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunObjectCase(TickDelegate tick, uint classCode, float delta, float powerRate, float oreRate, int state, float property, string name) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeObject item = fixture.AddObject(0x100 + classCode, classCode, powerRate, oreRate, property, state);
|
||||||
|
fixture.MakeDistributor(item.Key); active = fixture;
|
||||||
|
tick(fixture.Distributor, delta);
|
||||||
|
uint[] fields = ReadEntry(fixture.Entries);
|
||||||
|
Console.WriteLine(name + ": trace=" + String.Join(",", fixture.Trace.ToArray()) +
|
||||||
|
"; entry=[" + Hex(fields[0]) + "," + Float(fields[1]) + "," + Hex(fields[2]) + "," + Float(fields[3]) + "," + Hex(fields[4]) + "," + Float(fields[5]) + "," + Hex(fields[6]) +
|
||||||
|
"]; totals=[" + Float(ReadFloat(fixture.Distributor,0x30)) + "," + Float(ReadFloat(fixture.Distributor,0x34)) + "," + Float(ReadFloat(fixture.Distributor,0x38)) + "," + Float(ReadFloat(fixture.Distributor,0x3c)) + "," + Float(ReadFloat(fixture.Distributor,0x40)) +
|
||||||
|
"]; property=" + Float(item.Property) + " writes=" + item.Writes + " managerLookups=" + fixture.LookupCount);
|
||||||
|
Expect(fixture.LookupCount == 2, name + " is looked up in both native passes");
|
||||||
|
if (classCode == 0x80000002 || classCode == 0x80010000 || classCode == 0x80020000 || classCode == 0x80040000) {
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == 1.0f && ReadFloat(fixture.Distributor, 0x34) == 0.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x38) == 0.0f && ReadFloat(fixture.Distributor, 0x3c) == 0.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == 0.0f && item.Property == property && item.Writes == 2 &&
|
||||||
|
item.PowerRateReads == 1, name + " native power-rate branch totals and writes");
|
||||||
|
Expect(fields[1] == FloatBits(1.0f) && fields[3] == FloatBits(0.0f) && fields[4] == 0 &&
|
||||||
|
fields[5] == FloatBits(0.0f) && fields[6] == 0, name + " native power row fields");
|
||||||
|
ExpectOrderedCalls(fixture.Trace, "rate:" + item.Key.ToString("x8") + ":1005:2",
|
||||||
|
"lookup:" + item.Key.ToString("x8"), "iface-set:" + item.Key.ToString("x8") + ":204:1:1");
|
||||||
|
} else if (classCode == 0x80000004 || classCode == 0x80000008) {
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == 0.0f && ReadFloat(fixture.Distributor, 0x34) == 1.5f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x38) == 2.0f && ReadFloat(fixture.Distributor, 0x3c) == 0.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == 0.0f && item.Property == property && item.Writes == 1 &&
|
||||||
|
item.OreRateReads == 2, name + " native ore-rate branch totals and writes");
|
||||||
|
Expect(fields[1] == FloatBits(1.0f) && fields[3] == 0 && fields[4] == 0 &&
|
||||||
|
fields[5] == 0 && fields[6] == 0, name + " native ore row fields");
|
||||||
|
ExpectOrderedCalls(fixture.Trace, "rate:" + item.Key.ToString("x8") + ":1002:3",
|
||||||
|
"lookup:" + item.Key.ToString("x8"), "rate:" + item.Key.ToString("x8") + ":1002:3",
|
||||||
|
"set:" + item.Key.ToString("x8") + ":02000100:2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunMissingCase(TickDelegate tick) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
fixture.MakeDistributor(0xdeadbeef); active = fixture; tick(fixture.Distributor, 1.0f);
|
||||||
|
Expect(fixture.LookupCount == 2, "one missing list item is looked up once in each native pass");
|
||||||
|
Console.WriteLine("missing-map-entry: managerLookups=" + fixture.LookupCount + " trace=" + String.Join(",", fixture.Trace.ToArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunMixedOreCase(TickDelegate tick, string label, float producerProperty, float recipientWeight) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeObject producer = fixture.AddObject(0x41, 0x80000004, 0.0f, 4.0f, producerProperty, 0);
|
||||||
|
producer.OreRateSequence = new float[] { 4.0f, 1.0f };
|
||||||
|
producer.PropertyMetadata = FloatBits(10.0f);
|
||||||
|
NativeObject recipient = fixture.AddObject(0x42, 0x80000001, 0.0f, 0.0f, 6.0f, 0);
|
||||||
|
recipient.PropertyMetadata = FloatBits(12.0f);
|
||||||
|
recipient.InterfaceSelector1 = 0.5f;
|
||||||
|
recipient.InterfaceSelector2 = 0.75f;
|
||||||
|
fixture.MakeDistributor(producer.Key, recipient.Key);
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x04, BitConverter.ToInt32(BitConverter.GetBytes(1.0f), 0));
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x1c + 0x04, BitConverter.ToInt32(BitConverter.GetBytes(recipientWeight), 0));
|
||||||
|
active = fixture;
|
||||||
|
tick(fixture.Distributor, 1.0f);
|
||||||
|
Console.WriteLine(label + ": trace=" + String.Join(",", fixture.Trace.ToArray()) +
|
||||||
|
"; totals=[" + Float(ReadFloat(fixture.Distributor, 0x30)) + "," + Float(ReadFloat(fixture.Distributor, 0x34)) + "," + Float(ReadFloat(fixture.Distributor, 0x38)) + "," + Float(ReadFloat(fixture.Distributor, 0x3c)) + "," + Float(ReadFloat(fixture.Distributor, 0x40)) +
|
||||||
|
"]; entries=[" + FormatEntry(fixture.Entries) + ";" + FormatEntry(IntPtr.Add(fixture.Entries, 0x1c)) +
|
||||||
|
"]; producer(property=" + Float(producer.Property) + ",metadata=" + producer.PropertyMetadata + ",writes=" + producer.Writes + ",1002reads=" + producer.OreRateReads + "); recipient(property=" + Float(recipient.Property) + ",metadata=" + recipient.PropertyMetadata + ",writes=" + recipient.Writes + "); managerLookups=" + fixture.LookupCount);
|
||||||
|
if (producerProperty == 1.0f && recipientWeight == 0.5f) {
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == 0.0f && ReadFloat(fixture.Distributor, 0x34) == 1.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x38) == 1.0f && ReadFloat(fixture.Distributor, 0x3c) == 0.375f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == 6.0f, "mixed vector native totals");
|
||||||
|
Expect(producer.Property == 0.0f && recipient.Property == 7.0f && producer.OreRateReads == 2 &&
|
||||||
|
producer.PropertyMetadata == FloatBits(10.0f) && recipient.PropertyMetadata == FloatBits(12.0f) &&
|
||||||
|
producer.Writes == 1 && recipient.Writes == 1 && fixture.LookupCount == 4, "mixed vector native stores and two-pass selector reads");
|
||||||
|
uint[] recipientEntry = ReadEntry(IntPtr.Add(fixture.Entries, 0x1c));
|
||||||
|
Expect(recipientEntry[3] == FloatBits(0.375f) && recipientEntry[5] == FloatBits(6.0f) &&
|
||||||
|
recipientEntry[6] == 0x3f800000, "mixed vector per-item native fields");
|
||||||
|
ExpectLookupOrder(fixture, "00000041", "00000042", "00000041", "00000042");
|
||||||
|
ExpectOrderedCalls(fixture.Trace,
|
||||||
|
"rate:00000041:1002:4",
|
||||||
|
"lookup:00000042",
|
||||||
|
"rate:00000041:1002:1",
|
||||||
|
"set:00000041:02000100:0",
|
||||||
|
"lookup:00000042",
|
||||||
|
"set:00000042:02000100:7");
|
||||||
|
} else if (producerProperty == 0.25f && recipientWeight == 0.5f) {
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == 0.0f && ReadFloat(fixture.Distributor, 0x34) == 0.25f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x38) == 0.25f && ReadFloat(fixture.Distributor, 0x3c) == 0.375f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == 6.0f, "bounded ore native totals");
|
||||||
|
Expect(producer.Property == 0.0f && recipient.Property == 6.25f && producer.OreRateReads == 2 &&
|
||||||
|
producer.PropertyMetadata == FloatBits(10.0f) && recipient.PropertyMetadata == FloatBits(12.0f) &&
|
||||||
|
producer.Writes == 1 && recipient.Writes == 1 && fixture.LookupCount == 4,
|
||||||
|
"bounded ore stores are limited by producer stock");
|
||||||
|
uint[] recipientEntry = ReadEntry(IntPtr.Add(fixture.Entries, 0x1c));
|
||||||
|
Expect(recipientEntry[3] == FloatBits(0.375f) && recipientEntry[5] == FloatBits(6.0f) &&
|
||||||
|
recipientEntry[6] == FloatBits(0.25f), "bounded ore per-item native fields");
|
||||||
|
ExpectLookupOrder(fixture, "00000041", "00000042", "00000041", "00000042");
|
||||||
|
ExpectOrderedCalls(fixture.Trace,
|
||||||
|
"rate:00000041:1002:4",
|
||||||
|
"lookup:00000042",
|
||||||
|
"rate:00000041:1002:1",
|
||||||
|
"set:00000041:02000100:0",
|
||||||
|
"lookup:00000042",
|
||||||
|
"set:00000042:02000100:6.25");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunWeightedOreCase(TickDelegate tick) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeObject producer = fixture.AddObject(0x43, 0x80000004, 0.0f, 4.0f, 1.0f, 0);
|
||||||
|
producer.OreRateSequence = new float[] { 4.0f, 1.0f };
|
||||||
|
producer.PropertyMetadata = FloatBits(10.0f);
|
||||||
|
NativeObject zeroWeight = fixture.AddObject(0x44, 0x80000001, 0.0f, 0.0f, 6.0f, 0);
|
||||||
|
zeroWeight.PropertyMetadata = FloatBits(12.0f);
|
||||||
|
NativeObject partialWeight = fixture.AddObject(0x45, 0x80000001, 0.0f, 0.0f, 8.0f, 0);
|
||||||
|
partialWeight.PropertyMetadata = FloatBits(16.0f);
|
||||||
|
partialWeight.InterfaceSelector1 = 0.25f;
|
||||||
|
partialWeight.InterfaceSelector2 = 0.5f;
|
||||||
|
fixture.MakeDistributor(producer.Key, zeroWeight.Key, partialWeight.Key);
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x04, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x1c + 0x04, FloatBits(0.0f));
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x38 + 0x04, FloatBits(0.25f));
|
||||||
|
active = fixture;
|
||||||
|
tick(fixture.Distributor, 1.0f);
|
||||||
|
Console.WriteLine("weighted-ore-recipient: trace=" + String.Join(",", fixture.Trace.ToArray()) +
|
||||||
|
"; totals=[" + Float(ReadFloat(fixture.Distributor, 0x30)) + "," + Float(ReadFloat(fixture.Distributor, 0x34)) + "," + Float(ReadFloat(fixture.Distributor, 0x38)) + "," + Float(ReadFloat(fixture.Distributor, 0x3c)) + "," + Float(ReadFloat(fixture.Distributor, 0x40)) +
|
||||||
|
"]; entries=[" + FormatEntry(fixture.Entries) + ";" + FormatEntry(IntPtr.Add(fixture.Entries,0x1c)) + ";" + FormatEntry(IntPtr.Add(fixture.Entries,0x38)) +
|
||||||
|
"]; properties=[" + Float(producer.Property) + "," + Float(zeroWeight.Property) + "," + Float(partialWeight.Property) + "]; reads1002=" + producer.OreRateReads + " lookups=" + fixture.LookupCount);
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == 0.0f && ReadFloat(fixture.Distributor, 0x34) == 1.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x38) == 1.0f && ReadFloat(fixture.Distributor, 0x3c) == 0.375f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == 14.0f, "weighted ore native totals");
|
||||||
|
Expect(producer.Property == 0.0f && zeroWeight.Property == 6.0f && partialWeight.Property == 9.0f &&
|
||||||
|
producer.OreRateReads == 2 && producer.Writes == 1 && zeroWeight.Writes == 1 && partialWeight.Writes == 1 &&
|
||||||
|
fixture.LookupCount == 6, "weighted ore changes only the positive-weight recipient and preserves total stock");
|
||||||
|
uint[] zeroEntry = ReadEntry(IntPtr.Add(fixture.Entries, 0x1c));
|
||||||
|
uint[] partialEntry = ReadEntry(IntPtr.Add(fixture.Entries, 0x38));
|
||||||
|
Expect(zeroEntry[1] == FloatBits(0.0f) && zeroEntry[3] == FloatBits(0.0f) &&
|
||||||
|
zeroEntry[5] == FloatBits(6.0f) && partialEntry[1] == FloatBits(0.25f) &&
|
||||||
|
partialEntry[3] == FloatBits(0.375f) && partialEntry[5] == FloatBits(8.0f) &&
|
||||||
|
partialEntry[6] == FloatBits(1.0f), "weighted ore per-item native fields");
|
||||||
|
ExpectLookupOrder(fixture, "00000043", "00000044", "00000045", "00000043", "00000044", "00000045");
|
||||||
|
ExpectOrderedCalls(fixture.Trace,
|
||||||
|
"rate:00000043:1002:4",
|
||||||
|
"lookup:00000044",
|
||||||
|
"lookup:00000045",
|
||||||
|
"rate:00000043:1002:1",
|
||||||
|
"set:00000043:02000100:0",
|
||||||
|
"set:00000044:02000100:6",
|
||||||
|
"set:00000045:02000100:9");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunMixedPowerCase(TickDelegate tick, string label, float powerRate, float recipientProperty, float selector1, float selector2) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeObject producer = fixture.AddObject(0x61, 0x80000002, powerRate, 0.0f, 1.0f, 0);
|
||||||
|
producer.PropertyMetadata = FloatBits(10.0f);
|
||||||
|
NativeObject recipient = fixture.AddObject(0x62, 0x80000001, 0.0f, 0.0f, recipientProperty, 0);
|
||||||
|
recipient.PropertyMetadata = FloatBits(10.0f);
|
||||||
|
recipient.InterfaceSelector1 = selector1;
|
||||||
|
recipient.InterfaceSelector2 = selector2;
|
||||||
|
fixture.MakeDistributor(producer.Key, recipient.Key);
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x04, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(fixture.Entries, 0x1c + 0x04, FloatBits(0.5f));
|
||||||
|
active = fixture;
|
||||||
|
tick(fixture.Distributor, 1.0f);
|
||||||
|
Console.WriteLine(label + ": trace=" + String.Join(",", fixture.Trace.ToArray()) +
|
||||||
|
"; totals=[" + Float(ReadFloat(fixture.Distributor, 0x30)) + "," + Float(ReadFloat(fixture.Distributor, 0x34)) + "," + Float(ReadFloat(fixture.Distributor, 0x38)) + "," + Float(ReadFloat(fixture.Distributor, 0x3c)) + "," + Float(ReadFloat(fixture.Distributor, 0x40)) +
|
||||||
|
"]; entries=[" + FormatEntry(fixture.Entries) + ";" + FormatEntry(IntPtr.Add(fixture.Entries, 0x1c)) +
|
||||||
|
"]; producer(property=" + Float(producer.Property) + ",metadata=" + producer.PropertyMetadata + ",writes=" + producer.Writes + "); recipient(property=" + Float(recipient.Property) + ",metadata=" + recipient.PropertyMetadata + ",writes=" + recipient.Writes + "); managerLookups=" + fixture.LookupCount);
|
||||||
|
bool shortage = label == "mixed-power-shortage";
|
||||||
|
bool reserve = label == "mixed-power-recipient-reserve";
|
||||||
|
float expectedAvailable = powerRate;
|
||||||
|
float expectedRecipient = recipientProperty;
|
||||||
|
float expectedSelector = shortage ? 2.0f / 3.0f : 1.0f;
|
||||||
|
Expect(ReadFloat(fixture.Distributor, 0x30) == expectedAvailable &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x34) == 0.0f && ReadFloat(fixture.Distributor, 0x38) == 0.0f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x3c) == 0.375f &&
|
||||||
|
ReadFloat(fixture.Distributor, 0x40) == (reserve ? 11.0f : 18.0f), label + " native power totals");
|
||||||
|
Expect(producer.Property == 1.0f && recipient.Property == expectedRecipient &&
|
||||||
|
producer.Writes == 1 && recipient.Writes == 1 && producer.PowerRateReads == 1 &&
|
||||||
|
fixture.LookupCount == 4 && recipient.InterfaceSelector1 == expectedSelector,
|
||||||
|
label + " native resource and interface fields");
|
||||||
|
uint[] sourceEntry = ReadEntry(fixture.Entries);
|
||||||
|
uint[] recipientEntry = ReadEntry(IntPtr.Add(fixture.Entries, 0x1c));
|
||||||
|
Expect(sourceEntry[1] == FloatBits(1.0f) && sourceEntry[5] == FloatBits(9.0f) &&
|
||||||
|
recipientEntry[1] == FloatBits(0.5f) && recipientEntry[3] == FloatBits(0.375f) &&
|
||||||
|
recipientEntry[4] == FloatBits(shortage ? 0.125f : 0.375f) &&
|
||||||
|
recipientEntry[5] == FloatBits(reserve ? 2.0f : 9.0f), label + " native per-entry values");
|
||||||
|
Expect(FloatBits(recipient.InterfaceSelector1) == (shortage ? unchecked((int)0x3f2aaaab) : FloatBits(1.0f)),
|
||||||
|
label + " native interface selector bits");
|
||||||
|
ExpectLookupOrder(fixture, "00000061", "00000062", "00000061", "00000062");
|
||||||
|
ExpectOrderedCalls(fixture.Trace,
|
||||||
|
"rate:00000061:1005:" + Float(powerRate),
|
||||||
|
"lookup:00000062",
|
||||||
|
"lookup:00000061",
|
||||||
|
"iface-set:00000061:204:1:1",
|
||||||
|
"set:00000061:02000100:1",
|
||||||
|
"lookup:00000062",
|
||||||
|
"iface-set:00000062:204:1:" + Float(expectedSelector),
|
||||||
|
"set:00000062:02000100:" + Float(expectedRecipient));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunSkippedCase(TickDelegate tick) {
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeObject item = fixture.AddObject(0x55, 0x80000002, 1.0f, 0.0f, 0.0f, 0xfffe);
|
||||||
|
fixture.MakeDistributor(item.Key); active = fixture; tick(fixture.Distributor, 1.0f);
|
||||||
|
string expectedTrace = "lookup:00000055,state:00000055,lookup:00000055,state:00000055";
|
||||||
|
Expect(String.Join(",", fixture.Trace.ToArray()) == expectedTrace,
|
||||||
|
"0xfffe state is looked up in both passes and skips class and interface queries");
|
||||||
|
Expect(item.Calls.Count == 0, "0xfffe state skips all object vtable slots beyond state");
|
||||||
|
Console.WriteLine("state-fffe: trace=" + String.Join(",", fixture.Trace.ToArray()) + "; managerLookups=" + fixture.LookupCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint[] ReadEntry(IntPtr entry) {
|
||||||
|
uint[] words = new uint[7];
|
||||||
|
for (int i = 0; i < words.Length; i++) words[i] = unchecked((uint)Marshal.ReadInt32(entry, i * 4));
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
private static string FormatEntry(IntPtr entry) {
|
||||||
|
uint[] words = ReadEntry(entry);
|
||||||
|
return Hex(words[0]) + ",w1=" + Float(words[1]) + ",w2=" + Hex(words[2]) + ",w3=" + Float(words[3]) + ",w4=" + Hex(words[4]) + ",w5=" + Float(words[5]) + ",w6=" + Hex(words[6]);
|
||||||
|
}
|
||||||
|
private static IntPtr WriteFastcallToStdcallThunk(int offset, IntPtr stdcallTarget) {
|
||||||
|
// Adapt ECX=self, EDX=selector/IID, stack arg3 to a managed stdcall (self, selector, arg3).
|
||||||
|
byte[] code = { 0xff, 0x74, 0x24, 0x04, 0x52, 0x51, 0xb8, 0, 0, 0, 0, 0xff, 0xd0, 0xc2, 0x04, 0x00 };
|
||||||
|
WriteImm32(code, 7, stdcallTarget.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) {
|
||||||
|
byte[] bytes = BitConverter.GetBytes(value);
|
||||||
|
Buffer.BlockCopy(bytes, 0, code, offset, 4);
|
||||||
|
}
|
||||||
|
private static float ReadFloat(IntPtr address, int offset) { return BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(address, offset)), 0); }
|
||||||
|
private static string Float(uint bits) { return Float(BitConverter.ToSingle(BitConverter.GetBytes(unchecked((int)bits)), 0)); }
|
||||||
|
private static int FloatBits(float value) { return BitConverter.ToInt32(BitConverter.GetBytes(value), 0); }
|
||||||
|
private static string Float(float value) { return value.ToString("R", System.Globalization.CultureInfo.InvariantCulture); }
|
||||||
|
private static string Hex(uint value) { return "0x" + value.ToString("x8"); }
|
||||||
|
private static string Hash(string path) {
|
||||||
|
using (SHA256 sha = SHA256.Create()) using (FileStream input = File.OpenRead(path)) {
|
||||||
|
byte[] digest = sha.ComputeHash(input); StringBuilder hex = new StringBuilder();
|
||||||
|
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 ExpectLookupOrder(Fixture fixture, params string[] keys) {
|
||||||
|
List<string> actual = new List<string>();
|
||||||
|
foreach (string item in fixture.Trace) if (item.StartsWith("lookup:", StringComparison.Ordinal)) actual.Add(item.Substring(7));
|
||||||
|
Expect(String.Join(",", actual.ToArray()) == String.Join(",", keys), "native lookup order [" + String.Join(",", actual.ToArray()) + "]");
|
||||||
|
}
|
||||||
|
private static void ExpectOrderedCalls(List<string> trace, params string[] expected) {
|
||||||
|
int previous = -1;
|
||||||
|
foreach (string call in expected) {
|
||||||
|
int index = trace.IndexOf(call, previous + 1);
|
||||||
|
Expect(index >= 0, "native call order missing " + call);
|
||||||
|
previous = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void Zero(IntPtr buffer, int length) { for (int offset = 0; offset < length; offset++) Marshal.WriteByte(buffer, offset, 0); }
|
||||||
|
}
|
||||||
@@ -127,8 +127,12 @@ Rust `load_mission` принимает явный `queue_time_ms`, а не чи
|
|||||||
объектов в создаваемом мире Rust;
|
объектов в создаваемом мире Rust;
|
||||||
это интеграционный выбор runtime, а не подтверждённая эквивалентность всех
|
это интеграционный выбор runtime, а не подтверждённая эквивалентность всех
|
||||||
host construction phases. Не проверена другая лицензированная миссия с
|
host construction phases. Не проверена другая лицензированная миссия с
|
||||||
непустым `Init`, не восстановлены игровой tick Behavior и общий dispatcher
|
непустым `Init`. Вызов `Behavior.dll!FUN_10019e80` (RVA `0x19e80`, SHA-256
|
||||||
остальных событий.
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) исполнялся
|
||||||
|
на синтетических интерфейсах, но runtime пока не вызывает его в игровом цикле:
|
||||||
|
не доказаны создание реальных записей distributor и интерфейсов ArealMap,
|
||||||
|
которые ему нужны. Остальной Behavior tick flow и общий dispatcher событий
|
||||||
|
также не восстановлены.
|
||||||
|
|
||||||
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
||||||
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
||||||
|
|||||||
@@ -400,10 +400,57 @@ callback-точку входа и порядок внутри неё, но не
|
|||||||
bundle, но остальные потребители исходного глобального RNG не моделируются.
|
bundle, но остальные потребители исходного глобального RNG не моделируются.
|
||||||
В Rust также нет значения аргумента в `AI+0x3fc`, recursion guard `AI+0x78`
|
В Rust также нет значения аргумента в `AI+0x3fc`, recursion guard `AI+0x78`
|
||||||
или одноразового latch `AI+0x8a8`. Поэтому это связанный начальный поднабор, а
|
или одноразового latch `AI+0x8a8`. Поэтому это связанный начальный поднабор, а
|
||||||
не полная активация SuperAI и не обновление Behavior на игровых тиках. При
|
не полная активация SuperAI. Восстановленный ниже distributor tick проверен на
|
||||||
|
исходной DLL, но пока не подключён к Rust игровому циклу: у runtime нет
|
||||||
|
ArealMap-объектов и интерфейсов, которые нужны этому вызову. При
|
||||||
ошибке ограниченного `Init` Rust отклоняет полную загрузку и сохраняет прежний
|
ошибке ограниченного `Init` Rust отклоняет полную загрузку и сохраняет прежний
|
||||||
мир и состояние генератора; такая политика отката принадлежит Rust loader-у.
|
мир и состояние генератора; такая политика отката принадлежит Rust loader-у.
|
||||||
|
|
||||||
|
В проверенной GOG-сборке `Behavior.dll` (SHA-256
|
||||||
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`)
|
||||||
|
`FUN_10019e80` (RVA `0x19e80`) — отдельный шаг Resource Distributor. При
|
||||||
|
нулевом `dt` он сразу возвращается. Иначе он проходит список
|
||||||
|
строк по семь DWORD в порядке регистрации дважды: первый проход собирает
|
||||||
|
значения, второй заново ищет объекты и применяет результаты. Поэтому найденный
|
||||||
|
при втором проходе объект или getter может вернуть уже другое значение. Для каждой
|
||||||
|
строки вызывается ArealMap lookup; отсутствующий объект пропускается. Если
|
||||||
|
младшие 16 бит результата vslot `+0x44` равны `0xfffe`, строка пропускается до
|
||||||
|
запроса интерфейсов. Иначе исходный код запрашивает QI `0x16` и `0x204`, читает
|
||||||
|
класс через vslot `+0x38`, а для классов мощности вызывает vslot `+0x68` с
|
||||||
|
селектором `0x1005`, для двух классов руды — с `0x1002`. Остальные ветви
|
||||||
|
используют интерфейс `0x204` и свойство `0x02000100` через vslots `+0x4c` и
|
||||||
|
`+0x48`. В реализации этого свойства `FUN_10026990` (RVA `0x26990`) возвращает
|
||||||
|
DWORD по смещению `+0x14` как `f32`; если выходной указатель не null, функция
|
||||||
|
копирует в него DWORD по смещению `+0x08`. Парный setter `FUN_100269E0` (RVA
|
||||||
|
`0x269e0`) записывает переданный `f32` в `+0x14`; только если третий аргумент
|
||||||
|
не null, setter копирует указанный им DWORD в `+0x08`. При null старое значение
|
||||||
|
`+0x08` сохраняется. Это нулевые индексы 5 и 2 вектора DWORD; значение `+0x08`
|
||||||
|
пока не получило доменного имени. Идентификаторы и границы вызовов не
|
||||||
|
задают полную семантику свойств.
|
||||||
|
|
||||||
|
Изолированный x86 oracle вызывает исходный tick с подменой только IAT-слота
|
||||||
|
`GetSystemArealMap`; синтетические vtables записывают последовательность
|
||||||
|
вызовов и ответы. Для смешанного примера два прохода нашли объекты в порядке
|
||||||
|
`41,42,41,42`. Производитель руды с начальным значением `1` и скоростью `4`
|
||||||
|
вернул при следующем чтении скорость `1`; получатель с начальным значением `6`
|
||||||
|
получил `7`, а агрегаты distributor равны `[0,1,1,0.375,6]`. При запасе
|
||||||
|
производителя `0.25` получатель стал `6.25`; в трёхстрочном примере вес
|
||||||
|
получателя `0` не изменил значение `6`, а вес `0.25` дал `8 -> 9`. В примере
|
||||||
|
мощности доступное значение `4` покрывает спрос `0.375`; при значении `0.125`
|
||||||
|
selector `1` на интерфейсе получателя становится `2/3` (битовый образ
|
||||||
|
`0x3f2aaaab`). Эти векторы проверяют арифметику исходной функции, но не
|
||||||
|
распределение реальной карты.
|
||||||
|
|
||||||
|
Текущие типы Rust не дают честно наполнить этот список: мир хранит
|
||||||
|
идентификаторы и преобразования объектов, но не нужные tick ресурсные поля;
|
||||||
|
`MissionObjectDraft.properties` сохраняет сырые слова без связи с QI
|
||||||
|
`0x16`/`0x204` или свойством `0x02000100`. Методы добавления и перенастройки
|
||||||
|
строк `FUN_1001a9a0` и `FUN_1001aac0` доступны через entries vtable
|
||||||
|
`0x10059794` по смещениям `+0x0c` и `+0x10`; кто именно вызывает эти методы и
|
||||||
|
какие значения передаёт, пока не установлено. TMA-anchors и сырые preview
|
||||||
|
drafts не используются вместо неизвестной регистрации. `step_headless` по-прежнему
|
||||||
|
шагает только `World`.
|
||||||
|
|
||||||
Для проверки использованы три x86 helper-а на синтетической памяти. Caller
|
Для проверки использованы три x86 helper-а на синтетической памяти. Caller
|
||||||
oracle вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование
|
oracle вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование
|
||||||
TMA-координат в аргументы `CreateSuperAI`. AI oracle проходит оригинальный
|
TMA-координат в аргументы `CreateSuperAI`. AI oracle проходит оригинальный
|
||||||
@@ -438,6 +485,27 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Отдельный oracle distributor tick компилируется и запускается так:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:FPARKAN_ORIGINAL_BEHAVIOR_DLL = 'C:\GOG Games\Parkan - Iron Strategy\Behavior.dll'
|
||||||
|
New-Item -ItemType Directory -Force -Path target\behavior-distributor-native-oracle | Out-Null
|
||||||
|
& "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" /nologo /platform:x86 /out:target\behavior-distributor-native-oracle\BehaviorDistributorTickOracle.exe crates\fparkan-runtime\tests\behavior_distributor_tick_native_oracle.cs
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
& .\target\behavior-distributor-native-oracle\BehaviorDistributorTickOracle.exe
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
```
|
||||||
|
|
||||||
|
Oracle проверяет SHA-256 оригинального `Behavior.dll`:
|
||||||
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`. Harness
|
||||||
|
вызывает функцию по RVA `0x19e80` при единственной подмене импортного
|
||||||
|
слота `GetSystemArealMap`; машинный код tick не патчится. Проверяются нулевой
|
||||||
|
шаг, обе ветви селекторов скорости, пропуск состояния `0xfffe`, missing lookup,
|
||||||
|
динамическое повторное чтение `0x1002`, ограниченный запас руды, разные веса,
|
||||||
|
достаточное и недостаточное значение мощности. Это синтетические ArealMap
|
||||||
|
объекты; исполнение оригинальной функции проверяет векторы oracle, но не
|
||||||
|
подменяет сравнение Rust и интеграционную проверку миссии в игре.
|
||||||
|
|
||||||
Caller oracle проверяет AutoDemo пару `(500.2857, 751.8217)`, четыре режима
|
Caller oracle проверяет AutoDemo пару `(500.2857, 751.8217)`, четыре режима
|
||||||
x87 на tie-векторах, границу `10000` и младший DWORD. AI oracle отдельно
|
x87 на tie-векторах, границу `10000` и младший DWORD. AI oracle отдельно
|
||||||
проверяет, что `FUN_10005c70(0xffffffff, 10001)` даёт поля `(2^32, 10001.0)`,
|
проверяет, что `FUN_10005c70(0xffffffff, 10001)` даёт поля `(2^32, 10001.0)`,
|
||||||
|
|||||||
@@ -203,9 +203,37 @@ SuperAI для записей кланов, затем обновляет Behavi
|
|||||||
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
|
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
|
||||||
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
||||||
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или обновления Behavior на игровых
|
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или обновления Behavior на игровых
|
||||||
тиках. Он также не подменяет очередь оригинала: вызывающая сторона сама
|
тиках. При этом один конкретный шаг Behavior — распределение ресурсов — уже
|
||||||
передаёт время, а локальный генератор Rust пока учитывает только случайные
|
исполнен на исходной DLL с искусственными объектами; FParkan пока не вызывает
|
||||||
числа этого восстановленного шага.
|
его, потому что игровой мир Rust ещё не предоставляет нужные ресурсные
|
||||||
|
интерфейсы. Runtime также не подменяет очередь оригинала: вызывающая сторона
|
||||||
|
сама передаёт время, а локальный генератор Rust пока учитывает только случайные
|
||||||
|
числа восстановленного стартового шага.
|
||||||
|
|
||||||
|
### Регулярная передача ресурсов
|
||||||
|
|
||||||
|
Resource Distributor работает со списком зарегистрированных объектов. За один
|
||||||
|
шаг он сначала проходит список и собирает нужные значения, а затем проходит его
|
||||||
|
ещё раз и применяет результат. Во втором проходе объект ищется заново, поэтому
|
||||||
|
его состояние или ответ getter-а может измениться между чтением и записью.
|
||||||
|
Нулевой временной шаг ничего не делает; некоторые состояния объекта также
|
||||||
|
пропускают строку ещё до запросов ресурсных интерфейсов.
|
||||||
|
|
||||||
|
В синтетическом примере исходного кода два объекта с ключами `41` и `42`
|
||||||
|
обрабатываются в порядке `41, 42, 41, 42`: производитель сначала сообщает
|
||||||
|
скорость `4`, затем `1`, а получатель со значением `6` заканчивает шаг со
|
||||||
|
значением `7`. Это показывает, почему одного результата чтения недостаточно:
|
||||||
|
алгоритм снова получает величины при применении. Сценарий был запущен прямо из
|
||||||
|
исходной `Behavior.dll`, но объекты и ответы интерфейсов заданы тестом; это ещё
|
||||||
|
не воспроизведение реальной миссии.
|
||||||
|
|
||||||
|
Пока мы знаем точные границы интерфейсных запросов и некоторые ветви классов
|
||||||
|
объектов, но не все доменные значения полей записи и не происхождение реальных
|
||||||
|
строк списка. Загрузчик FParkan не должен создавать такие строки из похожих
|
||||||
|
полей миссии: сначала нужно доказать путь регистрации и связать его с типами
|
||||||
|
ресурсных объектов. Технические идентификаторы и численные векторы приведены
|
||||||
|
в [приложении о сценарной машине](../appendices/script-vm.md), а оставшиеся
|
||||||
|
вопросы перечислены в [границах знания](../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