aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime/tests
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-23 11:02:27 +0300
committerValentin Popov <valentin@popov.link>2026-10-23 11:02:27 +0300
commit7c909ea67ba7f27e11d5b33b8df9f6569da7eb7b (patch)
tree344ca595b3fcd34eb20671b65ad86146813d7873 /crates/fparkan-runtime/tests
parent83db371742ad3865bf4dc534ae38429c07a936d3 (diff)
downloadfparkan-7c909ea67ba7f27e11d5b33b8df9f6569da7eb7b.tar.xz
fparkan-7c909ea67ba7f27e11d5b33b8df9f6569da7eb7b.zip
feat(runtime): advance loaded mission resources
Diffstat (limited to 'crates/fparkan-runtime/tests')
-rw-r--r--crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs165
1 files changed, 163 insertions, 2 deletions
diff --git a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs
index 610e8db..4bf0e2b 100644
--- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs
+++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs
@@ -18,6 +18,7 @@ internal static class BehaviorDistributorTickOracle {
private const string ExpectedControlSha256 = "bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e";
private const string ExpectedArealMapSha256 = "a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea";
private const int TickRva = 0x19e80;
+ private const int DistributorScheduleRva = 0x1adb0;
private const int AddObjectRva = 0x1aac0;
private const int NormalizeRva = 0x1ac90;
private const int IndexedPropertySetRva = 0x0b470;
@@ -27,6 +28,7 @@ internal static class BehaviorDistributorTickOracle {
private const int BehaviorVarBlockConstructorRva = 0x22d90;
private const int BehaviorVarFieldRegisterRva = 0x22e20;
private const int GetSystemArealMapIatRva = 0x59008;
+ private const int GetQueueIatRva = 0x59110;
private const int ControlResourceDeviceGetterRva = 0x21d00;
private const int ControlResourceAggregateQueryRva = 0x2b410;
private const int ControlResourceAggregateSetRva = 0x2ba30;
@@ -66,6 +68,9 @@ internal static class BehaviorDistributorTickOracle {
[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.StdCall)] private delegate void NativeBehaviorSchedule(IntPtr self);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr QueueGetter();
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint QueueClockGetter(IntPtr self);
[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);
@@ -100,6 +105,8 @@ internal static class BehaviorDistributorTickOracle {
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlNodeUpdateCallback(IntPtr self, uint nodeIndex, uint selector, uint valueBits);
private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap;
+ private static readonly QueueGetter QueueGetterCallback = GetQueue;
+ private static readonly QueueClockGetter QueueClockGetterCallback = GetQueueTime;
private static readonly VectorInsert VectorInsertCallback = InsertVector;
private static readonly MapLookup MapLookupCallback = Lookup;
private static readonly ObjectState ObjectStateCallback = GetObjectState;
@@ -127,6 +134,7 @@ internal static class BehaviorDistributorTickOracle {
private static X87ControlWordWriter x87ControlWordWriter;
private static readonly NativeBehaviorProfileLoadCallback BehaviorProfileLoadCallback = CaptureBehaviorProfileLoad;
private static readonly List<BehaviorProfileSelection> behaviorProfileSelections = new List<BehaviorProfileSelection>();
+ private static Fixture activeQueueFixture;
private sealed class BehaviorProfileSelection {
public IntPtr Owner;
@@ -168,6 +176,9 @@ internal static class BehaviorDistributorTickOracle {
public IntPtr VectorVtable;
public IntPtr Manager;
public IntPtr ManagerVtable;
+ public IntPtr Queue;
+ public IntPtr QueueVtable;
+ public uint QueueTime;
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>();
@@ -221,6 +232,8 @@ internal static class BehaviorDistributorTickOracle {
Marshal.WriteInt32(Distributor, 0x20, keys.Length);
Marshal.WriteInt32(Distributor, 0x24, capacity);
Marshal.WriteIntPtr(Distributor, 0x28, Entries);
+ Marshal.WriteInt16(Distributor, 0x18, 3);
+ Marshal.WriteInt16(Distributor, 0x1a, 1);
for (int i = 0; i < keys.Length; i++) {
IntPtr entry = IntPtr.Add(Entries, i * 0x1c);
Marshal.WriteInt32(entry, 0x00, unchecked((int)keys[i]));
@@ -233,8 +246,16 @@ internal static class BehaviorDistributorTickOracle {
Allocations.Add(value);
return value;
}
+ public void MakeQueue(uint time) {
+ QueueTime = time;
+ QueueVtable = Allocate(0x40);
+ Queue = Allocate(0x20);
+ Marshal.WriteIntPtr(Queue, 0, QueueVtable);
+ Marshal.WriteIntPtr(QueueVtable, 0x38, Marshal.GetFunctionPointerForDelegate(QueueClockGetterCallback));
+ }
public void Dispose() {
active = null;
+ if (activeQueueFixture == this) activeQueueFixture = null;
for (int i = Allocations.Count - 1; i >= 0; i--) Marshal.FreeHGlobal(Allocations[i]);
Allocations.Clear();
}
@@ -287,6 +308,16 @@ internal static class BehaviorDistributorTickOracle {
}
private static IntPtr GetAreaMap() { return active.Manager; }
+ private static IntPtr GetQueue() {
+ if (activeQueueFixture == null || activeQueueFixture.Queue == IntPtr.Zero)
+ throw new InvalidOperationException("native scheduler requested a queue without a fixture");
+ return activeQueueFixture.Queue;
+ }
+ private static uint GetQueueTime(IntPtr self) {
+ if (activeQueueFixture == null || self != activeQueueFixture.Queue)
+ throw new InvalidOperationException("native scheduler queried an unexpected queue");
+ return activeQueueFixture.QueueTime;
+ }
private static int OnArealMapEvent(IntPtr owner, uint message, uint subcode, IntPtr descriptor) {
ArealMapFixture fixture = activeArealMap;
if (fixture == null) return 0;
@@ -493,7 +524,9 @@ internal static class BehaviorDistributorTickOracle {
FreeLibrary(module); FreeLibrary(controlModule); return 12;
}
IntPtr import = IntPtr.Add(module, GetSystemArealMapIatRva);
+ IntPtr queueImport = IntPtr.Add(module, GetQueueIatRva);
IntPtr originalImport = Marshal.ReadIntPtr(import);
+ IntPtr originalQueueImport = Marshal.ReadIntPtr(queueImport);
uint oldProtection = 0;
bool protectionChanged = false;
bool thunksAllocated = false;
@@ -513,8 +546,9 @@ internal static class BehaviorDistributorTickOracle {
throw new InvalidOperationException("VirtualProtect(GetSystemArealMap IAT) failed: " + Marshal.GetLastWin32Error());
protectionChanged = true;
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
+ Marshal.WriteIntPtr(queueImport, Marshal.GetFunctionPointerForDelegate(QueueGetterCallback));
RunSmokeCases(module, controlModule, arealMapModule);
- Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890; only synthetic host callbacks, GetSystemArealMap IAT, and bounded vector insertion are substituted; 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; 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).");
return 0;
}
catch (Exception error) {
@@ -524,10 +558,12 @@ internal static class BehaviorDistributorTickOracle {
finally {
if (protectionChanged) {
Marshal.WriteIntPtr(import, originalImport);
+ Marshal.WriteIntPtr(queueImport, originalQueueImport);
uint ignored;
VirtualProtect(import, new UIntPtr((uint)IntPtr.Size), oldProtection, out ignored);
}
- GC.KeepAlive(AreaMapGetterCallback); GC.KeepAlive(VectorInsertCallback); GC.KeepAlive(x87ControlWordReader); GC.KeepAlive(x87ControlWordWriter); GC.KeepAlive(MapLookupCallback);
+ GC.KeepAlive(AreaMapGetterCallback); GC.KeepAlive(QueueGetterCallback); GC.KeepAlive(QueueClockGetterCallback);
+ GC.KeepAlive(VectorInsertCallback); GC.KeepAlive(x87ControlWordReader); GC.KeepAlive(x87ControlWordWriter); GC.KeepAlive(MapLookupCallback);
GC.KeepAlive(ObjectStateCallback); GC.KeepAlive(ObjectClassCallback);
GC.KeepAlive(ObjectQueryCallback); GC.KeepAlive(ObjectRateCallback);
GC.KeepAlive(ObjectGetPropertyCallback); GC.KeepAlive(ObjectSetPropertyCallback);
@@ -565,6 +601,19 @@ internal static class BehaviorDistributorTickOracle {
}
RunNativeIndexedPropertyCases(module);
RunNativeOreInitializationCases(module);
+ ushort tickControlWord = CurrentX87ControlWord();
+ try {
+ SetX87ControlWord(0x007f);
+ Expect(CurrentX87ControlWord() == 0x007f, "Behavior resource-distributor tick uses the measured game x87 control word");
+ Console.WriteLine("native-behavior-tick-x87-control-word=0x" + CurrentX87ControlWord().ToString("x4"));
+ RunNativeBehaviorTickCases(module);
+ RunNativeBehaviorSchedulerCases(module);
+ } finally {
+ SetX87ControlWord(tickControlWord);
+ }
+ }
+
+ private static void RunNativeBehaviorTickCases(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);
@@ -579,6 +628,10 @@ internal static class BehaviorDistributorTickOracle {
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);
+ RunOreAllocationBranch(tick, "ore-allocation-middle", 0.25f, 2.0f, 0.25f, 0.625f, 1.75f, 0.5f);
+ RunOreAllocationBranch(tick, "ore-allocation-surplus", 2.0f, 3.0f, 0.25f, 0.625f, 2.625f, 0.625f);
+ RunOreAllocationBranch(tick, "ore-allocation-equality", 0.375f, 2.0f, 0.25f, 0.625f, 1.625f, 0.625f);
+ RunNativeOreClampCases(tick);
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);
@@ -587,6 +640,62 @@ internal static class BehaviorDistributorTickOracle {
RunSkippedCase(tick);
}
+ private static void RunNativeBehaviorSchedulerCases(IntPtr module) {
+ NativeBehaviorSchedule schedule = (NativeBehaviorSchedule)Marshal.GetDelegateForFunctionPointer(
+ IntPtr.Add(module, DistributorScheduleRva), typeof(NativeBehaviorSchedule));
+ uint[] elapsedVectors = { 1, 1001, 16777217, 16777219, 0xffffffff };
+ uint[] expectedBits = { 0x3a83126f, 0x3f8020c5, 0x46831270, 0x46831271, 0x4a83126f };
+ for (int i = 0; i < elapsedVectors.Length; i++) {
+ uint elapsed = elapsedVectors[i];
+ uint now = unchecked(1u + elapsed);
+ RunNativeSchedulerSample(schedule, now, 1, 0, expectedBits[i], "elapsed-" + elapsed.ToString());
+ }
+
+ using (Fixture fixture = NewFixture()) {
+ NativeObject producer = fixture.AddObject(0x75, 0x80000002, 1.0f, 0.0f, 0.0f, 0);
+ fixture.MakeDistributor(producer.Key);
+ fixture.MakeQueue(10);
+ active = fixture;
+ activeQueueFixture = fixture;
+ Marshal.WriteInt32(fixture.Distributor, 0x10, 1);
+ Marshal.WriteInt32(fixture.Distributor, 0x14, 10);
+ Marshal.WriteInt32(fixture.Distributor, 0x30, unchecked((int)0x41200000));
+ schedule(fixture.Distributor);
+ Expect(fixture.LookupCount == 0 && unchecked((uint)Marshal.ReadInt32(fixture.Distributor, 0x30)) == 0x41200000,
+ "scheduler equality with the deadline performs no resource tick or totals write");
+
+ fixture.QueueTime = 11;
+ schedule(fixture.Distributor);
+ uint deadlineTotal = unchecked((uint)Marshal.ReadInt32(fixture.Distributor, 0x30));
+ Expect(fixture.LookupCount == 2 && deadlineTotal == 0x3c23d70b,
+ "scheduler ticks only after the deadline is strictly passed (lookups=" + fixture.LookupCount +
+ ", total=" + Hex(deadlineTotal) + ")");
+ Console.WriteLine("scheduler-deadline: equality=skip; now=11 delta=0.01; lookups=" + fixture.LookupCount);
+ }
+
+ RunNativeSchedulerSample(schedule, 0, 0xffffffff, 0, 0x3a83126f, "wrapped-elapsed-1");
+ GC.KeepAlive(schedule);
+ }
+
+ private static void RunNativeSchedulerSample(
+ NativeBehaviorSchedule schedule, uint now, uint firstTime, uint deadline, uint expectedTotalBits, string label) {
+ using (Fixture fixture = NewFixture()) {
+ NativeObject producer = fixture.AddObject(0x74, 0x80000002, 1.0f, 0.0f, 0.0f, 0);
+ fixture.MakeDistributor(producer.Key);
+ fixture.MakeQueue(now);
+ active = fixture;
+ activeQueueFixture = fixture;
+ Marshal.WriteInt32(fixture.Distributor, 0x10, unchecked((int)firstTime));
+ Marshal.WriteInt32(fixture.Distributor, 0x14, unchecked((int)deadline));
+ schedule(fixture.Distributor);
+ uint actual = unchecked((uint)Marshal.ReadInt32(fixture.Distributor, 0x30));
+ Expect(actual == expectedTotalBits,
+ label + " native qword-to-f32 scheduler delta expected " + Hex(expectedTotalBits) + " got " + Hex(actual));
+ Console.WriteLine("native-scheduler-" + label + ": now=" + now.ToString("x8") + " first=" + firstTime.ToString("x8") +
+ " total=" + Hex(actual) + " lookups=" + fixture.LookupCount);
+ }
+ }
+
private static void RunNativeControlResourceDeviceCase(IntPtr module) {
Expect(CurrentX87ControlWord() == 0x007f, "Control selector 0x300 uses x87 control word 0x007f");
const uint key = 2;
@@ -1641,6 +1750,58 @@ internal static class BehaviorDistributorTickOracle {
}
}
+ private static void RunOreAllocationBranch(
+ TickDelegate tick, string label, float rate, float stock, float recipientCurrent, float recipientMaximum,
+ float expectedProducerRemaining, float expectedRecipientResult) {
+ using (Fixture fixture = NewFixture()) {
+ NativeObject producer = fixture.AddObject(0x46, 0x80000004, 0.0f, rate, stock, 0);
+ producer.PropertyMetadata = FloatBits(10.0f);
+ NativeObject recipient = fixture.AddObject(0x47, 0x80000001, 0.0f, 0.0f, recipientCurrent, 0);
+ recipient.PropertyMetadata = FloatBits(recipientMaximum);
+ recipient.InterfaceSelector1 = 0.5f;
+ recipient.InterfaceSelector2 = 0.0f;
+ 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 + ": 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)) +
+ "]; stored=[" + Float(producer.Property) + "," + Float(recipient.Property) + "] reads=" + producer.OreRateReads);
+ Expect(ReadFloat(fixture.Distributor, 0x34) == Math.Min(rate, stock) &&
+ ReadFloat(fixture.Distributor, 0x40) == recipientMaximum - recipientCurrent &&
+ producer.Property == expectedProducerRemaining && recipient.Property == expectedRecipientResult &&
+ producer.OreRateReads == 2 && producer.Writes == 1 && recipient.Writes == 1 && fixture.LookupCount == 4,
+ label + " native allocation branch totals and two-pass reread");
+ }
+ }
+
+ private static void RunNativeOreClampCases(TickDelegate tick) {
+ float[][] states = {
+ new float[] { 0.995f, 1.0f, 1.0f },
+ new float[] { 1.25f, 1.0f, 1.0f },
+ new float[] { 0.0f, 0.01f, 0.0f }
+ };
+ foreach (float[] state in states) {
+ float current = state[0];
+ float maximum = state[1];
+ float expected = state[2];
+ using (Fixture fixture = NewFixture()) {
+ NativeObject item = fixture.AddObject(0x48, 0x80000001, 0.0f, 0.0f, current, 0);
+ item.PropertyMetadata = FloatBits(maximum);
+ item.InterfaceSelector2 = 0.0f;
+ fixture.MakeDistributor(item.Key);
+ active = fixture;
+ tick(fixture.Distributor, 1.0f);
+ Expect(item.Property == expected && item.Writes > 0 && fixture.LookupCount == 2,
+ "native first-pass ore clamp for CurrentOre=" + Float(current));
+ Console.WriteLine("native-ore-clamp: current=" + Float(current) + " maximum=" + Float(maximum) + " result=" +
+ Float(item.Property) + " writes=" + item.Writes);
+ }
+ }
+ }
+
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);