aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-script
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 00:31:26 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 00:31:26 +0300
commita970974eb086372dc292a69b52b97b343da41e30 (patch)
treef46e41c2fe6edc6f31e62095b687a8fe946ed39c /crates/fparkan-script
parentda0672bdf04b497df064e685bd1e2da9f9c4d2ca (diff)
downloadfparkan-a970974eb086372dc292a69b52b97b343da41e30.tar.xz
fparkan-a970974eb086372dc292a69b52b97b343da41e30.zip
fix(script): preserve scheduler counter increment
Diffstat (limited to 'crates/fparkan-script')
-rw-r--r--crates/fparkan-script/src/lib.rs28
-rw-r--r--crates/fparkan-script/tests/handler2_native_oracle.cs87
2 files changed, 109 insertions, 6 deletions
diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs
index 88c2d0a..010c62a 100644
--- a/crates/fparkan-script/src/lib.rs
+++ b/crates/fparkan-script/src/lib.rs
@@ -856,8 +856,10 @@ pub struct Handler2Record {
pub key: Handler2RecordKey,
/// Resolved slot 1 scalar.
pub scalar_1: f32,
- /// Initial and per-refresh counter word from resolved slot 2.
+ /// Current counter word stored at original record offset `+0x24`.
pub counter: u32,
+ /// Original record offset `+0x28`: slot 2 captured when the record was created.
+ increment: u32,
/// Resolved slot 3 word.
pub word_3: u32,
/// Resolved slot 6 scalar.
@@ -892,8 +894,8 @@ impl Handler2RecordScheduler {
///
/// The original compares the three identity words bit-for-bit. On an
/// existing key, it refreshes only when `scalar_1` compares unequal; that
- /// update replaces `scalar_1` and `scalar_6`, then adds the record's own
- /// slot-2 counter word with x86 wrapping arithmetic.
+ /// update replaces `scalar_1` and `scalar_6`, then adds the slot-2 word
+ /// captured when that record was created, with x86 wrapping arithmetic.
pub fn submit(&mut self, input: Handler2RecordInput) -> Handler2RecordUpdate {
let key = Handler2RecordKey::from(input);
if let Some((index, record)) = self
@@ -905,7 +907,7 @@ impl Handler2RecordScheduler {
if !handler_two_scalar_equal(record.scalar_1, input.scalar_1) {
record.scalar_1 = input.scalar_1;
record.scalar_6 = input.scalar_6;
- record.counter = record.counter.wrapping_add(input.word_2);
+ record.counter = record.counter.wrapping_add(record.increment);
return Handler2RecordUpdate {
index,
created: false,
@@ -923,6 +925,7 @@ impl Handler2RecordScheduler {
key,
scalar_1: input.scalar_1,
counter: input.word_2,
+ increment: input.word_2,
word_3: input.word_3,
scalar_6: input.scalar_6,
});
@@ -1254,12 +1257,20 @@ mod tests {
refreshed: true,
}
);
- assert_eq!(scheduler.records()[0].counter, 103);
+ assert_eq!(scheduler.records()[0].counter, 8);
assert_eq!(
scheduler.records()[0].scalar_6.to_bits(),
12.0_f32.to_bits()
);
+ let refreshed_again = handler_two_input(3.5, -0.0, 3.0, 13.0, 7);
+ assert!(scheduler.submit(refreshed_again).refreshed);
+ assert_eq!(scheduler.records()[0].counter, 12);
+ assert_eq!(
+ scheduler.records()[0].scalar_6.to_bits(),
+ 13.0_f32.to_bits()
+ );
+
let positive_zero_key = handler_two_input(2.5, 0.0, 3.0, 12.0, 1);
assert_eq!(scheduler.submit(positive_zero_key).index, 1);
assert_eq!(scheduler.records().len(), 2);
@@ -1272,8 +1283,13 @@ mod tests {
let update = scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 4.0, 2));
assert_eq!(update.index, 0);
assert!(update.refreshed);
- assert_eq!(scheduler.records()[0].counter, 1);
+ assert_eq!(scheduler.records()[0].counter, u32::MAX - 1);
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 4.0_f32.to_bits());
+
+ let update = scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 5.0, 17));
+ assert!(update.refreshed);
+ assert_eq!(scheduler.records()[0].counter, u32::MAX - 2);
+ assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 5.0_f32.to_bits());
}
#[test]
diff --git a/crates/fparkan-script/tests/handler2_native_oracle.cs b/crates/fparkan-script/tests/handler2_native_oracle.cs
new file mode 100644
index 0000000..f632c68
--- /dev/null
+++ b/crates/fparkan-script/tests/handler2_native_oracle.cs
@@ -0,0 +1,87 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+
+internal static class Handler2NativeOracle {
+ private const uint LoadWithAlteredSearchPath = 0x00000008;
+ private const int RefreshRecordRva = 0x5070;
+ private const string ExpectedSha256 = "c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc";
+
+ [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);
+
+ [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
+ private delegate void RefreshRecord(IntPtr record);
+
+ private static int Main() {
+ if (IntPtr.Size != 4) {
+ Console.Error.WriteLine("Build this oracle with an x86 target; ai.dll is 32-bit.");
+ return 2;
+ }
+
+ string dllPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_AI_DLL");
+ if (String.IsNullOrEmpty(dllPath) || !File.Exists(dllPath)) {
+ Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AI_DLL to the original GOG ai.dll.");
+ return 3;
+ }
+
+ string actualSha256;
+ using (SHA256 sha256 = SHA256.Create())
+ using (FileStream input = File.OpenRead(dllPath)) {
+ byte[] digest = sha256.ComputeHash(input);
+ StringBuilder hex = new StringBuilder(digest.Length * 2);
+ foreach (byte value in digest) hex.Append(value.ToString("x2"));
+ actualSha256 = hex.ToString();
+ }
+ if (actualSha256 != ExpectedSha256) {
+ Console.Error.WriteLine("Unexpected ai.dll SHA-256: " + actualSha256);
+ return 4;
+ }
+
+ IntPtr module = LoadLibraryEx(dllPath, IntPtr.Zero, LoadWithAlteredSearchPath);
+ if (module == IntPtr.Zero) {
+ Console.Error.WriteLine("LoadLibraryEx failed: " + Marshal.GetLastWin32Error());
+ return 5;
+ }
+
+ try {
+ var refresh = (RefreshRecord)Marshal.GetDelegateForFunctionPointer(
+ IntPtr.Add(module, RefreshRecordRva), typeof(RefreshRecord));
+ IntPtr record = Marshal.AllocHGlobal(100);
+ try {
+ for (int offset = 0; offset < 100; offset += 4) Marshal.WriteInt32(record, offset, 0);
+ Marshal.WriteInt32(record, 0x24, 4);
+ Marshal.WriteInt32(record, 0x28, 4);
+ AssertRefresh(refresh, record, 8);
+ AssertRefresh(refresh, record, 12);
+
+ Marshal.WriteInt32(record, 0x24, unchecked((int)UInt32.MaxValue));
+ Marshal.WriteInt32(record, 0x28, unchecked((int)UInt32.MaxValue));
+ AssertRefresh(refresh, record, UInt32.MaxValue - 1);
+ AssertRefresh(refresh, record, UInt32.MaxValue - 2);
+ }
+ finally { Marshal.FreeHGlobal(record); }
+ }
+ finally { FreeLibrary(module); }
+ return 0;
+ }
+
+ private static void AssertRefresh(RefreshRecord refresh, IntPtr record, uint expectedCounter) {
+ uint savedIncrement = unchecked((uint)Marshal.ReadInt32(record, 0x28));
+ refresh(record);
+ uint actualCounter = unchecked((uint)Marshal.ReadInt32(record, 0x24));
+ uint actualIncrement = unchecked((uint)Marshal.ReadInt32(record, 0x28));
+ Console.WriteLine("record[+0x24]=0x{0:X8} record[+0x28]=0x{1:X8}",
+ actualCounter, actualIncrement);
+ if (actualCounter != expectedCounter || actualIncrement != savedIncrement) {
+ throw new InvalidOperationException(String.Format(
+ "Expected +0x24=0x{0:X8}, +0x28 unchanged at 0x{1:X8}.",
+ expectedCounter, savedIncrement));
+ }
+ }
+}