test(runtime): verify native Control provider updates
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
This commit is contained in:
4 files changed
+312
-26
No files matched your search
@@ -6,8 +6,8 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
// Calls selected original x86 Behavior.dll, ArealMap.dll, and Control.dll paths
|
// Calls selected original x86 Behavior.dll, ArealMap.dll, and Control.dll paths
|
||||||
// against synthetic storage. Only host callbacks, GetSystemArealMap, and bounded
|
// against synthetic storage. Only host callbacks, GetSystemArealMap/GetQueue IAT
|
||||||
// vector backing are substituted; the tested module instructions remain installed code.
|
// slots, and bounded vector backing are substituted; tested module code remains installed.
|
||||||
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;
|
||||||
@@ -36,6 +36,9 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private const int ControlProviderType1GetterRva = 0x0dc40;
|
private const int ControlProviderType1GetterRva = 0x0dc40;
|
||||||
private const int ControlProviderRowConstructorRva = 0x0b240;
|
private const int ControlProviderRowConstructorRva = 0x0b240;
|
||||||
private const int ControlProviderAmountUpdateRva = 0x10f30;
|
private const int ControlProviderAmountUpdateRva = 0x10f30;
|
||||||
|
private const int ControlProviderAmountDistributeRva = 0x10ba0;
|
||||||
|
private const int ControlContactRateUpdateRva = 0x12a40;
|
||||||
|
private const int ControlTimedUpdateRva = 0x0bcf0;
|
||||||
private const int ArealMapUnitDatComponentBuilderRva = 0x14b90;
|
private const int ArealMapUnitDatComponentBuilderRva = 0x14b90;
|
||||||
private const int ControlResourceDeviceConstructorRva = 0x22980;
|
private const int ControlResourceDeviceConstructorRva = 0x22980;
|
||||||
private const int ControlResourceComponentOverrideRva = 0x2d890;
|
private const int ControlResourceComponentOverrideRva = 0x2d890;
|
||||||
@@ -101,6 +104,9 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountDistribute(IntPtr self, float delta, uint mode, uint flags);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlContactRateUpdate(IntPtr self, float deltaSeconds);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlTimedUpdate(IntPtr self, float inputTime);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlNodeUpdateCallback(IntPtr self, uint nodeIndex, uint selector, uint valueBits);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlNodeUpdateCallback(IntPtr self, uint nodeIndex, uint selector, uint valueBits);
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
private static uint controlNodeUpdateIndex;
|
private static uint controlNodeUpdateIndex;
|
||||||
private static uint controlNodeUpdateSelector;
|
private static uint controlNodeUpdateSelector;
|
||||||
private static uint controlNodeUpdateValueBits;
|
private static uint controlNodeUpdateValueBits;
|
||||||
|
private static readonly List<uint> controlNodeUpdateIndices = new List<uint>();
|
||||||
private static IntPtr fastcallThunks;
|
private static IntPtr fastcallThunks;
|
||||||
private static IntPtr queryObjectThunk;
|
private static IntPtr queryObjectThunk;
|
||||||
private static IntPtr getInterfacePropertyThunk;
|
private static IntPtr getInterfacePropertyThunk;
|
||||||
@@ -170,6 +177,11 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
public readonly List<string> Calls = new List<string>();
|
public readonly List<string> Calls = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class NativeProviderTable {
|
||||||
|
public IntPtr Owner;
|
||||||
|
public IntPtr Rows;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class Fixture : IDisposable {
|
private sealed class Fixture : IDisposable {
|
||||||
public IntPtr Distributor;
|
public IntPtr Distributor;
|
||||||
public IntPtr Entries;
|
public IntPtr Entries;
|
||||||
@@ -467,6 +479,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
controlNodeUpdateIndex = nodeIndex;
|
controlNodeUpdateIndex = nodeIndex;
|
||||||
controlNodeUpdateSelector = selector;
|
controlNodeUpdateSelector = selector;
|
||||||
controlNodeUpdateValueBits = valueBits;
|
controlNodeUpdateValueBits = valueBits;
|
||||||
|
controlNodeUpdateIndices.Add(nodeIndex);
|
||||||
}
|
}
|
||||||
private static float SequenceValue(float[] sequence, int index, float fallback) {
|
private static float SequenceValue(float[] sequence, int index, float fallback) {
|
||||||
if (sequence == null || sequence.Length == 0) return fallback;
|
if (sequence == null || sequence.Length == 0) return fallback;
|
||||||
@@ -548,7 +561,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
||||||
Marshal.WriteIntPtr(queueImport, Marshal.GetFunctionPointerForDelegate(QueueGetterCallback));
|
Marshal.WriteIntPtr(queueImport, Marshal.GetFunctionPointerForDelegate(QueueGetterCallback));
|
||||||
RunSmokeCases(module, controlModule, arealMapModule);
|
RunSmokeCases(module, controlModule, arealMapModule);
|
||||||
Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, scheduler 0x1adb0, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890; synthetic host callbacks, GetSystemArealMap/GetQueue IAT slots, and bounded vector insertion are substituted; scheduler and tick machine code are original; this does not compare Rust parity).");
|
Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, scheduler 0x1adb0, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890, provider distribution 0x10ba0, class-4 update 0x12a40 and inactive timing baseline 0xbcf0; synthetic host callbacks, GetSystemArealMap/GetQueue IAT slots, and bounded vector insertion are substituted; Control provider selector vtable is original; no full Control update or Rust parity is claimed).");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception error) {
|
catch (Exception error) {
|
||||||
@@ -591,6 +604,9 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
RunNativeControlIntoOverrideCase(controlModule);
|
RunNativeControlIntoOverrideCase(controlModule);
|
||||||
RunNativeControlResourceDeviceCase(controlModule);
|
RunNativeControlResourceDeviceCase(controlModule);
|
||||||
RunNativeControlProviderLiveStateCase(controlModule);
|
RunNativeControlProviderLiveStateCase(controlModule);
|
||||||
|
RunNativeControlProviderDistributionCases(controlModule);
|
||||||
|
RunNativeControlContactRateUpdateCase(controlModule);
|
||||||
|
RunNativeControlTimedUpdateBaselineCases(controlModule);
|
||||||
RunNativeControlAggregateResourceCases(controlModule);
|
RunNativeControlAggregateResourceCases(controlModule);
|
||||||
RunNativeBehaviorProfileMapCases(module);
|
RunNativeBehaviorProfileMapCases(module);
|
||||||
RunNativeBehaviorVarSyntheticCase(module);
|
RunNativeBehaviorVarSyntheticCase(module);
|
||||||
@@ -841,6 +857,212 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static NativeProviderTable CreateNativeProviderTable(Fixture fixture, IntPtr module,
|
||||||
|
float[] maxima, float[] amounts, byte[] rowFlags) {
|
||||||
|
Expect(maxima.Length == amounts.Length && maxima.Length == rowFlags.Length,
|
||||||
|
"native provider fixture arrays have matching lengths");
|
||||||
|
NativeControlProviderRowConstructor construct = (NativeControlProviderRowConstructor)
|
||||||
|
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva),
|
||||||
|
typeof(NativeControlProviderRowConstructor));
|
||||||
|
NativeProviderTable table = new NativeProviderTable();
|
||||||
|
table.Owner = fixture.Allocate(0x664);
|
||||||
|
table.Rows = fixture.Allocate(maxima.Length * 0x2c);
|
||||||
|
IntPtr providerInterface = IntPtr.Add(table.Owner, 4);
|
||||||
|
IntPtr providerVtable = IntPtr.Add(module, ControlProviderInterfaceVtableRva);
|
||||||
|
Marshal.WriteIntPtr(providerInterface, providerVtable);
|
||||||
|
Expect(Marshal.ReadIntPtr(providerVtable, 0x0c) == IntPtr.Add(module, ControlProviderType1GetterRva),
|
||||||
|
"native provider fixture uses the installed selector interface vtable");
|
||||||
|
Marshal.WriteIntPtr(table.Owner, 0x55c, table.Rows);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x560, maxima.Length);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x548, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x660, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x550, 0xfffe); // Exclude the separate root-node depletion lifecycle.
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x594, 0);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x598, 0);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x618, 0);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x386, 0);
|
||||||
|
|
||||||
|
float totalMaximum = 0.0f;
|
||||||
|
float totalAmount = 0.0f;
|
||||||
|
for (int i = 0; i < maxima.Length; i++) {
|
||||||
|
IntPtr descriptor = fixture.Allocate(0x80);
|
||||||
|
IntPtr row = IntPtr.Add(table.Rows, i * 0x2c);
|
||||||
|
Marshal.WriteInt32(descriptor, 0x04, FloatBits(maxima[i]));
|
||||||
|
construct(row, descriptor);
|
||||||
|
Marshal.WriteInt32(row, 0x04, -1); // No component parent or stage chain.
|
||||||
|
Marshal.WriteInt32(row, 0x08, FloatBits(amounts[i]));
|
||||||
|
Marshal.WriteInt32(row, 0x10, 0);
|
||||||
|
Marshal.WriteInt32(row, 0x14, FloatBits(amounts[i] / maxima[i]));
|
||||||
|
Marshal.WriteInt32(row, 0x18, 0);
|
||||||
|
Marshal.WriteInt32(row, 0x1c, 0);
|
||||||
|
Marshal.WriteInt32(row, 0x24, -1);
|
||||||
|
Marshal.WriteByte(row, 0x28, rowFlags[i]);
|
||||||
|
totalMaximum += maxima[i];
|
||||||
|
totalAmount += amounts[i];
|
||||||
|
}
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x58c, FloatBits(totalMaximum));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x590, FloatBits(totalAmount));
|
||||||
|
|
||||||
|
IntPtr callbackObject = fixture.Allocate(IntPtr.Size);
|
||||||
|
IntPtr callbackVtable = fixture.Allocate(0x38);
|
||||||
|
Marshal.WriteIntPtr(callbackObject, 0, callbackVtable);
|
||||||
|
Marshal.WriteIntPtr(callbackVtable, 0x34, Marshal.GetFunctionPointerForDelegate(ControlNodeUpdateCallback));
|
||||||
|
Marshal.WriteIntPtr(table.Owner, 0x20, callbackObject);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ReadNativeProviderAmount(NativeProviderTable table, int index) {
|
||||||
|
return ReadFloat(table.Rows, index * 0x2c + 0x08);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte ReadNativeProviderFlags(NativeProviderTable table, int index) {
|
||||||
|
return Marshal.ReadByte(table.Rows, index * 0x2c + 0x28);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeControlProviderDistributionCases(IntPtr module) {
|
||||||
|
NativeControlProviderAmountDistribute distribute = (NativeControlProviderAmountDistribute)
|
||||||
|
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderAmountDistributeRva),
|
||||||
|
typeof(NativeControlProviderAmountDistribute));
|
||||||
|
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||||
|
new float[] { 30.0f, 60.0f, 50.0f }, new float[] { 10.0f, 20.0f, 0.0f },
|
||||||
|
new byte[] { 0, 0, 0 });
|
||||||
|
controlNodeUpdateCount = 0;
|
||||||
|
controlNodeUpdateIndices.Clear();
|
||||||
|
distribute(table.Owner, 22.0f, 0, 0);
|
||||||
|
Expect(ReadNativeProviderAmount(table, 0) == 14.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 1) == 28.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 2) == 10.0f,
|
||||||
|
"Control native proportional amount distribution weights each row by its remaining capacity");
|
||||||
|
Expect(controlNodeUpdateCount == 3 && controlNodeUpdateIndex == 2 &&
|
||||||
|
controlNodeUpdateSelector == 0x200,
|
||||||
|
"Control proportional distribution publishes each row update through the existing native node callback");
|
||||||
|
Expect(String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "0,1,2",
|
||||||
|
"Control proportional distribution visits rows in ascending order");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||||
|
new float[] { 30.0f, 60.0f, 50.0f }, new float[] { 10.0f, 20.0f, 30.0f },
|
||||||
|
new byte[] { 0, 0, 0 });
|
||||||
|
controlNodeUpdateIndices.Clear();
|
||||||
|
distribute(table.Owner, -35.0f, 1, 0);
|
||||||
|
Expect(ReadNativeProviderAmount(table, 0) == 10.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 1) == 15.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 2) == 0.0f,
|
||||||
|
"Control native sequential withdrawal consumes rows in reverse order");
|
||||||
|
Expect(String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "2,1",
|
||||||
|
"Control sequential withdrawal calls the node updater from the last row backwards");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||||
|
new float[] { 10.0f, 10.0f }, new float[] { 9.0f, 0.0f },
|
||||||
|
new byte[] { 0, 0x10 });
|
||||||
|
distribute(table.Owner, 10.0f, 1, 0);
|
||||||
|
Expect(ReadNativeProviderAmount(table, 0) == 10.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 1) == 0.0f &&
|
||||||
|
(ReadNativeProviderFlags(table, 1) & 0x10) != 0,
|
||||||
|
"Control sequential distribution clips at active capacity and skips exhausted rows without revive");
|
||||||
|
distribute(table.Owner, -20.0f, 1, 0);
|
||||||
|
Expect(ReadNativeProviderAmount(table, 0) == 0.0f &&
|
||||||
|
(ReadNativeProviderFlags(table, 0) & 0x10) != 0,
|
||||||
|
"Control native depletion marks the row exhausted");
|
||||||
|
distribute(table.Owner, 15.0f, 1, 1);
|
||||||
|
Expect(ReadNativeProviderAmount(table, 0) == 10.0f &&
|
||||||
|
ReadNativeProviderAmount(table, 1) == 5.0f &&
|
||||||
|
(ReadNativeProviderFlags(table, 0) & 0x10) == 0 &&
|
||||||
|
(ReadNativeProviderFlags(table, 1) & 0x10) == 0,
|
||||||
|
"Control native revive flag admits exhausted rows and clears their exhausted flags when refilled");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||||
|
new float[] { 33554432.0f, 10.0f }, new float[] { 16777216.0f, 2.0f },
|
||||||
|
new byte[] { 0, 0 });
|
||||||
|
controlNodeUpdateIndices.Clear();
|
||||||
|
distribute(table.Owner, -1.0f, 0, 0);
|
||||||
|
uint firstBits = unchecked((uint)FloatBits(ReadNativeProviderAmount(table, 0)));
|
||||||
|
uint secondBits = unchecked((uint)FloatBits(ReadNativeProviderAmount(table, 1)));
|
||||||
|
uint totalBits = unchecked((uint)FloatBits(ReadFloat(table.Owner, 0x590)));
|
||||||
|
Expect(firstBits == unchecked((uint)FloatBits(16777214.0f)) &&
|
||||||
|
secondBits == 0x3ffffffe && totalBits == unchecked((uint)FloatBits(16777216.0f)),
|
||||||
|
"Control proportional distribution preserves the native x87 add-then-subtract rounding at a 2^24 cancellation boundary; actual=[" +
|
||||||
|
Hex(firstBits) + "," + Hex(secondBits) + "] total=" + Hex(totalBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("native-control-provider-distribution: controlHash=" + ExpectedControlSha256 +
|
||||||
|
" rva=0x10ba0 providerVtable=0x3d254 selector3=0xdc40 proportional=[14,28,10]" +
|
||||||
|
" sequentialReverse=[10,15,0] cap/exhausted/revive=verified cancellation=2^24");
|
||||||
|
GC.KeepAlive(distribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeControlContactRateUpdateCase(IntPtr module) {
|
||||||
|
NativeControlContactRateUpdate update = (NativeControlContactRateUpdate)
|
||||||
|
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlContactRateUpdateRva),
|
||||||
|
typeof(NativeControlContactRateUpdate));
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||||
|
new float[] { 100.0f }, new float[] { 10.000001f }, new byte[] { 0 });
|
||||||
|
IntPtr profile = fixture.Allocate(0x6c);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x50, 4);
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x1a4, FloatBits(1.0f));
|
||||||
|
Marshal.WriteIntPtr(table.Owner, 0x46c, profile);
|
||||||
|
Marshal.WriteInt32(profile, 0x68, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x4d8, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x354, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x358, FloatBits(1.0f));
|
||||||
|
Marshal.WriteInt32(table.Owner, 0x1a8, FloatBits(1.0f));
|
||||||
|
controlNodeUpdateCount = 0;
|
||||||
|
|
||||||
|
update(table.Owner, 2.0f);
|
||||||
|
Expect(FloatBits(ReadNativeProviderAmount(table, 0)) == FloatBits(8.000001f) &&
|
||||||
|
FloatBits(ReadFloat(table.Rows, 0x10)) == FloatBits(-2.0f) &&
|
||||||
|
ReadFloat(table.Owner, 0x590) == 8.000001f,
|
||||||
|
"Control class-4 contact-rate update applies negative rate times elapsed seconds and publishes animation amount");
|
||||||
|
const int expectedNormalizedBits = unchecked((int)0x3da3d70c);
|
||||||
|
Expect(Marshal.ReadByte(table.Owner, 0x598) == 0 &&
|
||||||
|
FloatBits(ReadFloat(table.Owner, 0x588)) == expectedNormalizedBits &&
|
||||||
|
controlNodeUpdateCount == 1 && controlNodeUpdateValueBits == unchecked((uint)expectedNormalizedBits),
|
||||||
|
"Control class-4 contact-rate update clears its dirty state after computing the aggregate and provider callback");
|
||||||
|
Console.WriteLine("native-control-contact-rate-update: controlHash=" + ExpectedControlSha256 +
|
||||||
|
" rva=0x12a40 class=4 dt=2 rate=1 current=10.000001->8.000001 animation=-2 normalizedBits=0x" +
|
||||||
|
unchecked((uint)FloatBits(ReadFloat(table.Owner, 0x588))).ToString("x8") + " dirty=cleared");
|
||||||
|
}
|
||||||
|
GC.KeepAlive(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunNativeControlTimedUpdateBaselineCases(IntPtr module) {
|
||||||
|
NativeControlTimedUpdate update = (NativeControlTimedUpdate)
|
||||||
|
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlTimedUpdateRva),
|
||||||
|
typeof(NativeControlTimedUpdate));
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr owner = fixture.Allocate(0x664);
|
||||||
|
controlNodeUpdateCount = 0;
|
||||||
|
update(owner, 0.5f);
|
||||||
|
Expect(controlNodeUpdateCount == 0 && ReadFloat(owner, 0xe8) == 0.5f &&
|
||||||
|
ReadFloat(owner, 0xe4) == 0.0f && ReadFloat(owner, 0xec) == 0.0f &&
|
||||||
|
ReadFloat(owner, 0xd8) == 0.0f && ReadFloat(owner, 0xdc) == 0.0f,
|
||||||
|
"Control timed update records the sub-threshold delta without entering its active callback path");
|
||||||
|
update(owner, 1.0f);
|
||||||
|
Expect(controlNodeUpdateCount == 0 && ReadFloat(owner, 0xe8) == 1.0f &&
|
||||||
|
ReadFloat(owner, 0xe4) == 1.0f && ReadFloat(owner, 0xec) == 1.0f &&
|
||||||
|
ReadFloat(owner, 0xd8) == 1.0f && ReadFloat(owner, 0xdc) == 1.0f &&
|
||||||
|
Marshal.ReadByte(owner, 0xe0) == 0,
|
||||||
|
"Control timed update accepts the one-unit boundary and synchronizes its baseline while e0 remains clear");
|
||||||
|
update(owner, 2.0f);
|
||||||
|
Expect(controlNodeUpdateCount == 0 && ReadFloat(owner, 0xe8) == 1.0f &&
|
||||||
|
ReadFloat(owner, 0xe4) == 2.0f && ReadFloat(owner, 0xec) == 2.0f &&
|
||||||
|
ReadFloat(owner, 0xd8) == 2.0f && ReadFloat(owner, 0xdc) == 2.0f &&
|
||||||
|
Marshal.ReadByte(owner, 0xe0) == 0,
|
||||||
|
"Control timed update keeps synchronizing accepted times without starting the active branch while e0 is clear");
|
||||||
|
Console.WriteLine("native-control-timed-update-baseline: controlHash=" + ExpectedControlSha256 +
|
||||||
|
" rva=0xbcf0 sub-threshold=0.5 accepted=[1,2] e4/ec/d8/dc synchronized owner.e0=0 callback=none");
|
||||||
|
}
|
||||||
|
GC.KeepAlive(update);
|
||||||
|
}
|
||||||
|
|
||||||
private static void RunNativeControlAggregateResourceCases(IntPtr module) {
|
private static void RunNativeControlAggregateResourceCases(IntPtr module) {
|
||||||
NativeControlAggregateQuery query = (NativeControlAggregateQuery)Marshal.GetDelegateForFunctionPointer(
|
NativeControlAggregateQuery query = (NativeControlAggregateQuery)Marshal.GetDelegateForFunctionPointer(
|
||||||
WriteStdCallToFastcallThunk(0x1c0, IntPtr.Add(module, ControlResourceAggregateQueryRva)),
|
WriteStdCallToFastcallThunk(0x1c0, IntPtr.Add(module, ControlResourceAggregateQueryRva)),
|
||||||
|
|||||||
@@ -326,20 +326,23 @@ sidecar недоступен, запрос `0xe00` сообщает это от
|
|||||||
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
|
||||||
текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
|
текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
|
||||||
factory paths остаются явными границами. Этот запрос возвращает начальный
|
factory paths остаются явными границами. Этот запрос возвращает начальный
|
||||||
снимок provider amount: Native `FUN_10010f30` (RVA `0x10f30`) изменяет и
|
снимок provider amount; дальнейшее изменение строк в Rust не реализовано.
|
||||||
ограничивает количество, а связанные callbacks переносят его между строками;
|
Native oracle проверил изолированные пути распределения, обновления класса `4`
|
||||||
этот переход в Rust ещё не реализован.
|
и начальную ветвь таймера. Активную clock-ветвь не запускали; внешний источник
|
||||||
|
времени и порядок вызовов остаются незакрытыми. Источник контактного коэффициента
|
||||||
|
прослежен статически, но не исполнен; синтетические provider-строки не имеют
|
||||||
|
родителей, а родительская логика и lifecycle истощения не проверены. Подробные
|
||||||
|
векторы и статические границы приведены в
|
||||||
|
[техническом подразделе о provider-состоянии Control](script-vm.md#изменение-provider-состояния-control).
|
||||||
|
|
||||||
Отдельный API `advance_loaded_mission_resources` принимает явное время очереди
|
Отдельный API `advance_loaded_mission_resources` принимает явное время очереди
|
||||||
и обновляет представленные поля `CurrentOre`, строк распределителя и Control
|
и обновляет представленные поля `CurrentOre`, строк распределителя и Control
|
||||||
selector `1`. Его граница — статические снимки профиля и provider-значений:
|
selector `1`. API не подключён к игровому циклу и не обновляет Control amount,
|
||||||
динамическое распространение Control amount, смена профилей, special
|
профили, special object-state gate или жизненный цикл объектов. Если обязательный
|
||||||
object-state gate и жизненный цикл созданных/удалённых объектов здесь не
|
запрос позднего объекта не поддержан, транзакция возвращает ошибку без применения
|
||||||
моделируются. Если обязательный запрос позднего объекта не поддержан,
|
подготовленных изменений. AutoDemo-тест подтверждает связь настоящих профилей с
|
||||||
транзакция возвращает ошибку и не применяет подготовленные изменения. API не
|
этим шагом, но не полный паритет миссии; отдельный aggregate getter Behavior RVA
|
||||||
подключён к игровому циклу; AutoDemo-тест подтверждает связь настоящих профилей
|
`0xa490` в oracle не исполнен.
|
||||||
с этим шагом, но не полный паритет миссии. Отдельный aggregate getter Behavior
|
|
||||||
RVA `0xa490` в oracle не исполнен.
|
|
||||||
|
|
||||||
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
|
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
|
||||||
профиля из `behpsp.res`, разобранного до создания нового мира. `0x1002` складывает
|
профиля из `behpsp.res`, разобранного до создания нового мира. `0x1002` складывает
|
||||||
|
|||||||
@@ -541,11 +541,14 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
```
|
```
|
||||||
|
|
||||||
Native oracle Add, normalize и distributor tick компилируется и запускается
|
Изолированный x86 oracle оригинальных Behavior, ArealMap и Control
|
||||||
так:
|
компилируется и запускается так:
|
||||||
|
|
||||||
```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'
|
||||||
|
$env:FPARKAN_ORIGINAL_CONTROL_DLL = 'C:\GOG Games\Parkan - Iron Strategy\Control.dll'
|
||||||
|
$env:FPARKAN_ORIGINAL_AREALMAP_DLL = 'C:\GOG Games\Parkan - Iron Strategy\ArealMap.dll'
|
||||||
|
$env:FPARKAN_ORIGINAL_BEHAVIOR_PROFILE_RES = 'C:\GOG Games\Parkan - Iron Strategy\behpsp.res'
|
||||||
New-Item -ItemType Directory -Force -Path target\behavior-distributor-native-oracle | Out-Null
|
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
|
& "$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 }
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
@@ -567,9 +570,15 @@ IAT-слоты `GetQueue` и `GetSystemArealMap`, а synthetic vtables зада
|
|||||||
Tick-векторы проверяют нулевой шаг, обе ветви селекторов скорости, пропуск
|
Tick-векторы проверяют нулевой шаг, обе ветви селекторов скорости, пропуск
|
||||||
состояния `0xfffe`, отсутствующий объект, повторное чтение `0x1002`, ограниченный
|
состояния `0xfffe`, отсутствующий объект, повторное чтение `0x1002`, ограниченный
|
||||||
запас руды, порог `0.01`, ветви дефицита/частичного/избыточного предложения и
|
запас руды, порог `0.01`, ветви дефицита/частичного/избыточного предложения и
|
||||||
равенство спросу, разные веса и достаточную/недостаточную мощность. Это
|
равенство спросу, разные веса и достаточную/недостаточную мощность. Тот же
|
||||||
синтетические ArealMap объекты: oracle исполняет оригинальные scheduler и tick,
|
harness проверяет в оригинальном Control пропорциональное и последовательное
|
||||||
но не сравнивает паритет Rust и не заменяет проверку полной миссии в игре.
|
распределение provider amount, коэффициент класса `4` с интервалом `2`, а также
|
||||||
|
неактивную временную ветвь с входами `0.5`, `1` и `2`. Вызовы Control используют
|
||||||
|
синтетические строки и хостовые callbacks; ArealMap тоже получает синтетические
|
||||||
|
объекты. Harness сверяет SHA-256 всех трёх DLL; ArealMap загружается из
|
||||||
|
изолированной временной рабочей папки, которую процесс затем удаляет. Эти тесты
|
||||||
|
проверяют отдельные native-пути, но не полный Control update и не паритет 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 отдельно
|
||||||
@@ -596,6 +605,45 @@ tie-векторов подтверждает реакцию `FISTP`, но не
|
|||||||
форме инструкции. Всё остальное содержимое `.scr` сохраняется reader-ом, но
|
форме инструкции. Всё остальное содержимое `.scr` сохраняется reader-ом, но
|
||||||
остаётся вне этого исполнения.
|
остаётся вне этого исполнения.
|
||||||
|
|
||||||
|
### Изменение provider-состояния Control
|
||||||
|
|
||||||
|
Для `Control.dll` (SHA-256
|
||||||
|
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) native
|
||||||
|
oracle вызывает оригинальные `FUN_10010ba0` (RVA `0x10ba0`), `FUN_10010f30`
|
||||||
|
(RVA `0x10f30`) и selector-3 provider getter через vtable `0x3d254`, RVA
|
||||||
|
`0xdc40`. Пропорциональный режим проверен на максимумах `[30, 60, 50]`, текущих
|
||||||
|
значениях `[10, 20, 0]` и добавке `22`, результат `[14, 28, 10]`; отдельный
|
||||||
|
вектор подтверждает последовательное заполнение вперёд и вычитание с конца.
|
||||||
|
Проверены ограничение строк, снятие признака истощения при пополнении и порядок
|
||||||
|
обратных вызовов. При x87 control word `0x007f` для `[2^24, 2]` с запросом `-1`
|
||||||
|
получены значения `[16777214, 1.99999976]` и итог `16777216`: округление
|
||||||
|
вычисленного изменения даёт `-2`. Oracle задаёт внешнюю публикацию результата
|
||||||
|
обратным вызовом; provider строки не имеют родительских узлов и не проходят
|
||||||
|
отдельный жизненный цикл истощения корневого объекта.
|
||||||
|
|
||||||
|
Для `FUN_10012a40` (RVA `0x12a40`) нативный вектор задаёт класс `4`, интервал
|
||||||
|
`2`, коэффициент `+0x1a4` равный `1` и текущее значение `10.000001`; результат
|
||||||
|
равен `8.000001`, вспомогательное поле — `-2`, нормализованное поле имеет биты
|
||||||
|
`0x3da3d70c`. Происхождение коэффициента подтверждено только статическим
|
||||||
|
трассированием: `FUN_1001a450` (RVA `0x1a450`) запрашивает интерфейс IID `0x0d`,
|
||||||
|
вызывает его slot `+0x24`, читает DWORD по смещению `+0x0c` в полученной записи
|
||||||
|
и сохраняет его в поле объекта `+0x1a4`. Поэтому нативный вектор проверяет
|
||||||
|
арифметику `FUN_10012a40` с заданным коэффициентом, а не исполнение контактного
|
||||||
|
пути или его игровой источник.
|
||||||
|
|
||||||
|
`FUN_1000bcf0` (RVA `0xbcf0`) получает абсолютное значение времени. Oracle
|
||||||
|
исполняет только безопасную начальную ветвь при `+0xe0 = 0`: вход `0.5`
|
||||||
|
обновляет `+0xe8`, а входы `1` и `2` синхронизируют `+0xe4`, `+0xec`, `+0xd8` и
|
||||||
|
`+0xdc` без callbacks. Активную ветвь этот тест не запускал. Статический код
|
||||||
|
показывает gate по `+0xe0`; при условии `randomized_deadline < +0xe4` или
|
||||||
|
`+0xd4 != 0` вычисляет `dt = (+0xe4 - +0xec) * 0.001`. x87 выполняет `FSUB`,
|
||||||
|
затем `FMUL` с binary32-константой `0.001` (`0x3a83126f`), после чего результат
|
||||||
|
сохраняется в `f32`. Код вызывает `FUN_10012a40`, затем `FUN_1002d340` и
|
||||||
|
присваивает `+0xec = +0xe4`. В соседней ветви update packet queue обрабатывается
|
||||||
|
через `FUN_10012ce0`. Источник абсолютного времени извне и полный порядок
|
||||||
|
вызовов в игре не установлены; эти связи остаются статическим выводом. Rust
|
||||||
|
продолжает читать начальные provider snapshots и не моделирует эти изменения.
|
||||||
|
|
||||||
### Аргументы и обратный вызов Handler(30)
|
### Аргументы и обратный вызов Handler(30)
|
||||||
|
|
||||||
`resolve_handler30_with_values` сохраняет восстановленное ABI типов объявлений,
|
`resolve_handler30_with_values` сохраняет восстановленное ABI типов объявлений,
|
||||||
|
|||||||
@@ -859,13 +859,26 @@ tag-26 ряда получается `1.0` provider × `1.0` coefficient × `1.0
|
|||||||
`1.0`. Эти запросы используют начальные provider-значения после конструкторов,
|
`1.0`. Эти запросы используют начальные provider-значения после конструкторов,
|
||||||
а не результаты живого игрового тика.
|
а не результаты живого игрового тика.
|
||||||
|
|
||||||
Поведение provider-строк меняется и после загрузки: например,
|
После загрузки количество в строках может меняться. Контроллер распределяет
|
||||||
`Control.dll!FUN_10010f30` (RVA `0x10f30`) обновляет и ограничивает текущее
|
добавку либо пропорционально свободному месту, либо по очереди: при максимумах
|
||||||
значение, а соседние процедуры распространяют его между связанными строками.
|
`[30, 60, 50]`, текущих значениях `[10, 20, 0]` и добавке `22` первый способ
|
||||||
Эти изменения пока не отражены в Rust-снимке. Поэтому запрос Control `0xe00` и
|
даёт `[14, 28, 10]`. При вычитании последовательный способ начинает с последней
|
||||||
Behavior selector `0x1002`, который его суммирует, используют исходное состояние
|
строки. Так одна таблица описывает и текущее значение, и то, как оно меняется.
|
||||||
полной загрузки. Selector `0x1005` прямо возвращает загруженную ставку профиля.
|
|
||||||
Ни один из них пока не исполняет Control tick или последующие изменения профиля.
|
Для точного повтора важен порядок округления. При исходных значениях `[2^24, 2]`
|
||||||
|
и запросе `-1` оригинал сначала складывает общий итог с изменением, затем
|
||||||
|
вычитает прежний итог. Из-за округления фактическое изменение получается `-2`.
|
||||||
|
Нативная проверка подтвердила это поведение; Rust пока использует начальный
|
||||||
|
снимок provider-строк и не исполняет их дальнейшее распределение.
|
||||||
|
|
||||||
|
Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и
|
||||||
|
передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,
|
||||||
|
и значение строки уменьшается на `2`; этот коэффициент не является ставкой
|
||||||
|
производства из профиля. Проверка времени охватывает только начальную ветвь:
|
||||||
|
активную ветвь не запускали, а источник времени и полный порядок вызовов в игре
|
||||||
|
пока не установлены. Поэтому запросы Rust `0xe00` и `0x1002` используют
|
||||||
|
начальные снимки provider-строк, а `0x1005` — загруженную ставку профиля.
|
||||||
|
Точные нативные векторы и открытые границы описаны в [приложении](../appendices/script-vm.md#изменение-provider-состояния-control).
|
||||||
|
|
||||||
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
|
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
|
||||||
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
|
но промежуточная степень может выйти за диапазон `f32` и вернуться в него при
|
||||||
|
|||||||
Reference in new issue
Block a user