Compare commits
2
Commits
5a819c9c70
...
bcedda6e93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcedda6e93
|
||
|
|
a970974eb0
|
No files matched your search
@@ -856,8 +856,10 @@ pub struct Handler2Record {
|
|||||||
pub key: Handler2RecordKey,
|
pub key: Handler2RecordKey,
|
||||||
/// Resolved slot 1 scalar.
|
/// Resolved slot 1 scalar.
|
||||||
pub scalar_1: f32,
|
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,
|
pub counter: u32,
|
||||||
|
/// Original record offset `+0x28`: slot 2 captured when the record was created.
|
||||||
|
increment: u32,
|
||||||
/// Resolved slot 3 word.
|
/// Resolved slot 3 word.
|
||||||
pub word_3: u32,
|
pub word_3: u32,
|
||||||
/// Resolved slot 6 scalar.
|
/// Resolved slot 6 scalar.
|
||||||
@@ -892,8 +894,8 @@ impl Handler2RecordScheduler {
|
|||||||
///
|
///
|
||||||
/// The original compares the three identity words bit-for-bit. On an
|
/// The original compares the three identity words bit-for-bit. On an
|
||||||
/// existing key, it refreshes only when `scalar_1` compares unequal; that
|
/// existing key, it refreshes only when `scalar_1` compares unequal; that
|
||||||
/// update replaces `scalar_1` and `scalar_6`, then adds the record's own
|
/// update replaces `scalar_1` and `scalar_6`, then adds the slot-2 word
|
||||||
/// slot-2 counter word with x86 wrapping arithmetic.
|
/// captured when that record was created, with x86 wrapping arithmetic.
|
||||||
pub fn submit(&mut self, input: Handler2RecordInput) -> Handler2RecordUpdate {
|
pub fn submit(&mut self, input: Handler2RecordInput) -> Handler2RecordUpdate {
|
||||||
let key = Handler2RecordKey::from(input);
|
let key = Handler2RecordKey::from(input);
|
||||||
if let Some((index, record)) = self
|
if let Some((index, record)) = self
|
||||||
@@ -905,7 +907,7 @@ impl Handler2RecordScheduler {
|
|||||||
if !handler_two_scalar_equal(record.scalar_1, input.scalar_1) {
|
if !handler_two_scalar_equal(record.scalar_1, input.scalar_1) {
|
||||||
record.scalar_1 = input.scalar_1;
|
record.scalar_1 = input.scalar_1;
|
||||||
record.scalar_6 = input.scalar_6;
|
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 {
|
return Handler2RecordUpdate {
|
||||||
index,
|
index,
|
||||||
created: false,
|
created: false,
|
||||||
@@ -923,6 +925,7 @@ impl Handler2RecordScheduler {
|
|||||||
key,
|
key,
|
||||||
scalar_1: input.scalar_1,
|
scalar_1: input.scalar_1,
|
||||||
counter: input.word_2,
|
counter: input.word_2,
|
||||||
|
increment: input.word_2,
|
||||||
word_3: input.word_3,
|
word_3: input.word_3,
|
||||||
scalar_6: input.scalar_6,
|
scalar_6: input.scalar_6,
|
||||||
});
|
});
|
||||||
@@ -1254,12 +1257,20 @@ mod tests {
|
|||||||
refreshed: true,
|
refreshed: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(scheduler.records()[0].counter, 103);
|
assert_eq!(scheduler.records()[0].counter, 8);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scheduler.records()[0].scalar_6.to_bits(),
|
scheduler.records()[0].scalar_6.to_bits(),
|
||||||
12.0_f32.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);
|
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.submit(positive_zero_key).index, 1);
|
||||||
assert_eq!(scheduler.records().len(), 2);
|
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));
|
let update = scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 4.0, 2));
|
||||||
assert_eq!(update.index, 0);
|
assert_eq!(update.index, 0);
|
||||||
assert!(update.refreshed);
|
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());
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,6 +143,43 @@ slot2 одновременно в `+0x24/+0x28`, slot3 в `+0x2c` и slot6 в `+
|
|||||||
изолированную часть как `Handler2RecordScheduler`; он не выполняет bytecode,
|
изолированную часть как `Handler2RecordScheduler`; он не выполняет bytecode,
|
||||||
не назначает игровых имён и не делает event lookup за original VM.
|
не назначает игровых имён и не делает event lookup за original VM.
|
||||||
|
|
||||||
|
Шаг прибавления проверен прямым вызовом оригинального helper-а
|
||||||
|
`FUN_10005070` (RVA `0x5070`) из GOG `ai.dll`, SHA-256
|
||||||
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
|
||||||
|
Исходник x86 test harness-а хранится в
|
||||||
|
[`handler2_native_oracle.cs`](../../crates/fparkan-script/tests/handler2_native_oracle.cs):
|
||||||
|
он сначала сверяет hash DLL, затем вызывает helper с 100-byte record и
|
||||||
|
проверяет оба поля. Пример воспроизведения в Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:FPARKAN_ORIGINAL_AI_DLL = 'C:\GOG Games\Parkan - Iron Strategy\ai.dll'
|
||||||
|
& "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" /nologo /platform:x86 /out:target\Handler2NativeOracle.exe crates\fparkan-script\tests\handler2_native_oracle.cs
|
||||||
|
.\target\Handler2NativeOracle.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Полученные значения:
|
||||||
|
|
||||||
|
| `+0x24` до вызова | сохранённый `+0x28` | `+0x24` после вызова | `+0x28` после вызова |
|
||||||
|
| ---: | ---: | ---: | ---: |
|
||||||
|
| `0x00000004` | `0x00000004` | `0x00000008` | `0x00000004` |
|
||||||
|
| `0x00000008` | `0x00000004` | `0x0000000C` | `0x00000004` |
|
||||||
|
| `0xFFFFFFFF` | `0xFFFFFFFF` | `0xFFFFFFFE` | `0xFFFFFFFF` |
|
||||||
|
| `0xFFFFFFFE` | `0xFFFFFFFF` | `0xFFFFFFFD` | `0xFFFFFFFF` |
|
||||||
|
|
||||||
|
Rust-тест повторяет эти начальные значения. Для первой пары обновлений входы
|
||||||
|
несут slot 2, равный `99` и `7`, однако сохранённый при создании шаг равен `4`;
|
||||||
|
для второй пары оба счётчика и шаг начинаются с `0xFFFFFFFF`, а новые входы
|
||||||
|
несут `2` и `17`. Правило refresh читается прямо из модели:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
record.counter = record.counter.wrapping_add(record.increment);
|
||||||
|
```
|
||||||
|
|
||||||
|
Native helper получает только указатель на record, поэтому напрямую проверяет
|
||||||
|
сложение текущего `+0x24` с сохранённым `+0x28`; свежий slot 2 принадлежит
|
||||||
|
сценарию Rust-теста, а не аргументам helper-а. Проверка не охватывает полный
|
||||||
|
scheduler path внутри запущенной игры.
|
||||||
|
|
||||||
На границе mission runtime выбранный TMA clan `first_resource`
|
На границе mission runtime выбранный TMA clan `first_resource`
|
||||||
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
||||||
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
||||||
|
|||||||
Reference in new issue
Block a user