1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
|
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
internal static class Handler19DispatchOracle {
private const uint LoadWithAlteredSearchPath = 0x00000008;
private const string ExpectedAiSha256 = "c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc";
private const string ExpectedIron3dSha256 = "d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa";
private const string ExpectedScriptSha256 = "db1c6fde57aff43181619f19e19e1cacf7301ebb859e8cb883db6b4b7181eced";
private const int EventLookupRva = 0x11f40;
private const int EventDispatchRva = 0x11f10;
private const int Handler19Rva = 0x0aa38;
private const int AiWarmStartRva = 0x01ae0;
private const int Iron3dClanRefreshRva = 0x39440;
private const int EventStride = 0x14;
private const int InstructionStride = 0x20;
private const int VarSetStride = 0x30;
[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 int FindEvent(IntPtr vm, IntPtr name);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate uint DispatchEvent(IntPtr vm, int eventIndex);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate uint ReadVariable(IntPtr record);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void SetDwordPosition(IntPtr record, uint x, uint y);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void RefreshClan(IntPtr record, uint requestedMode);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void HostInterfaceRefresh(IntPtr host);
private sealed class WarmStartObservation {
public int Calls;
public IntPtr Host;
public IntPtr Record;
public IntPtr Ai;
public IntPtr Cells;
public int EntryMode;
public int AiMode;
public byte Latch;
public uint XCellBeforeDispatch;
}
private static WarmStartObservation CurrentWarmStartObservation;
private static HostInterfaceRefresh WarmStartRecorder = RecordWarmStart;
private static void RecordWarmStart(IntPtr host) {
WarmStartObservation observation = CurrentWarmStartObservation;
observation.Calls++;
observation.Host = host;
observation.EntryMode = Marshal.ReadInt32(observation.Record, 0x18);
observation.AiMode = Marshal.ReadInt32(observation.Ai, 0x3fc);
observation.Latch = Marshal.ReadByte(observation.Ai, 0x8a8);
observation.XCellBeforeDispatch = ReadWord(observation.Cells, 0);
}
private sealed class Instruction {
public readonly uint[] Words;
public readonly uint[] References;
public Instruction(uint[] words, uint[] references) { Words = words; References = references; }
}
private sealed class Event {
public readonly byte[] Name;
public readonly uint EventWord;
public readonly Instruction[] Instructions;
public Event(byte[] name, uint eventWord, Instruction[] instructions) { Name = name; EventWord = eventWord; Instructions = instructions; }
}
private sealed class Package {
public readonly uint HandlerCount;
public readonly Event[] Events;
public Package(uint handlerCount, Event[] events) { HandlerCount = handlerCount; Events = events; }
}
private sealed class Instance : IDisposable {
private readonly List<IntPtr> allocations = new List<IntPtr>();
public IntPtr Ai;
public IntPtr EventTable;
public IntPtr HandlerTable;
public IntPtr VarRecords;
public IntPtr Cells;
public void Dispose() { for (int i = allocations.Count - 1; i >= 0; --i) Marshal.FreeHGlobal(allocations[i]); }
public IntPtr Allocate(int length) {
IntPtr p = Marshal.AllocHGlobal(length);
if (p == IntPtr.Zero) throw new OutOfMemoryException();
Zero(p, length); allocations.Add(p); return p;
}
}
private static int Main() {
if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
string aiPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_AI_DLL");
string iron3dPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_IRON3D_DLL");
string scriptPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_DEFAULT_SCR");
if (String.IsNullOrEmpty(aiPath) || !File.Exists(aiPath) || String.IsNullOrEmpty(iron3dPath) || !File.Exists(iron3dPath) || String.IsNullOrEmpty(scriptPath) || !File.Exists(scriptPath)) {
Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AI_DLL, FPARKAN_ORIGINAL_IRON3D_DLL, and FPARKAN_ORIGINAL_DEFAULT_SCR to the installed GOG files."); return 3;
}
if (Hash(aiPath) != ExpectedAiSha256) { Console.Error.WriteLine("Unexpected ai.dll SHA-256: " + Hash(aiPath)); return 4; }
if (Hash(iron3dPath) != ExpectedIron3dSha256) { Console.Error.WriteLine("Unexpected iron3d.dll SHA-256: " + Hash(iron3dPath)); return 4; }
if (Hash(scriptPath) != ExpectedScriptSha256) { Console.Error.WriteLine("Unexpected default.scr SHA-256: " + Hash(scriptPath)); return 5; }
Package package = ReadPackage(scriptPath);
if (package.HandlerCount != 73 || package.Events.Length != 9) throw new InvalidDataException("Unexpected default.scr header.");
if (package.Events[0].Name.Length != 5 || Encoding.ASCII.GetString(package.Events[0].Name) != "Init\0" || package.Events[0].Instructions.Length != 1)
throw new InvalidDataException("Expected a single-instruction Init event at index 0.");
Instruction init = package.Events[0].Instructions[0];
if (init.Words[0] != 19 || init.Words[1] != 0xffffffff || init.Words[2] != 0xffffffff || init.References.Length != 3 || init.References[0] != 224 || init.References[1] != 225 || init.References[2] != 226)
throw new InvalidDataException("Init instruction no longer matches the recovered Handler(19) shape.");
IntPtr module = LoadLibraryEx(aiPath, IntPtr.Zero, LoadWithAlteredSearchPath);
if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(ai.dll) failed: " + Marshal.GetLastWin32Error()); return 6; }
try {
FindEvent find = (FindEvent)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, EventLookupRva), typeof(FindEvent));
DispatchEvent dispatch = (DispatchEvent)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, EventDispatchRva), typeof(DispatchEvent));
ReadVariable readVariable = (ReadVariable)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, 0x13570), typeof(ReadVariable));
SetDwordPosition setDwordPosition = (SetDwordPosition)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, 0x5c70), typeof(SetDwordPosition));
VerifyEventLookupVectors(package, find, dispatch, module);
RunInstance(package, find, dispatch, readVariable, module, 500.2857f, 752.0f, 0, 500, 752);
RunInstance(package, find, dispatch, readVariable, module, 728.0f, 449.0f, 1, 728, 449);
RunDwordToHandlerRoundTrip(package, find, dispatch, readVariable, setDwordPosition, module);
RunOriginalClanRefreshPath(package, module, iron3dPath);
Console.WriteLine("original ai.dll dispatcher passed for default.scr Init instances and DWORD round-trip; original iron3d clan-refresh to AI-vtable Init path also passed; code bytes were not modified");
return 0;
}
finally { FreeLibrary(module); }
}
private static void RunOriginalClanRefreshPath(Package package, IntPtr aiModule, string iron3dPath) {
IntPtr iron3d = LoadLibraryEx(iron3dPath, IntPtr.Zero, LoadWithAlteredSearchPath);
if (iron3d == IntPtr.Zero) throw new InvalidOperationException("LoadLibraryEx(iron3d.dll) failed: " + Marshal.GetLastWin32Error());
try {
RefreshClan refresh = (RefreshClan)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(iron3d, Iron3dClanRefreshRva), typeof(RefreshClan));
IntPtr method = IntPtr.Add(aiModule, AiWarmStartRva);
RunClanRefreshCase(package, aiModule, refresh, method, 1, 2, 2, "forward requested mode");
RunClanRefreshCase(package, aiModule, refresh, method, 0, 2, 1, "zero flag forces mode one");
RunClanRefreshCase(package, aiModule, refresh, method, 1, 1, 1, "forward mode one");
RunClanRefreshMissingCase(package, aiModule, refresh, method);
RunClanRefreshBusyCase(package, aiModule, refresh, method);
RunClanRefreshNullAiCase(refresh);
}
finally { FreeLibrary(iron3d); }
}
private static void RunClanRefreshCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method, int forceFlag, uint requestedMode, uint expectedMode, string label) {
using (Instance instance = BuildInstance(package, 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
IntPtr host = ConfigureClanRefresh(instance, method);
IntPtr record = PrepareClanRecord(instance, forceFlag);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, requestedMode);
WarmStartObservation observed = CurrentWarmStartObservation;
Expect(Marshal.ReadInt32(record, 0x18) == requestedMode, label + ": Iron3D stores the requested per-clan mode");
Expect(Marshal.ReadInt32(instance.Ai, 0x3fc) == expectedMode, label + ": AI callback receives the forced/forwarded mode");
Expect(observed.Calls == 1 && observed.Host == host, label + ": original AI slot called its host interface first");
Expect(observed.EntryMode == requestedMode && observed.AiMode == expectedMode && observed.Latch == 0 && observed.XCellBeforeDispatch == 0,
label + ": record and AI mode are stored before host callback, and Init dispatch follows it");
Expect(Marshal.ReadByte(instance.Ai, 0x78) == 0, label + ": normal Init dispatch clears the recursion guard");
Expect(Marshal.ReadByte(instance.Ai, 0x8a8) == 1, label + ": successful Init lookup latches after dispatch");
Expect(ReadWord(instance.Cells, 0) == 500 && ReadWord(instance.Cells, 1) == 752 && ReadWord(instance.Cells, 2) == 0,
label + ": original Init handler writes X/Y/clan after host callback");
Console.WriteLine("iron3d 39440 -> ai 01ae0 ({0}): entry_mode={1}, ai_mode={2}, latch={3}", label, requestedMode, expectedMode, Marshal.ReadByte(instance.Ai, 0x8a8));
Marshal.WriteInt32(instance.Cells, 0, unchecked((int)0xfeedface));
refresh(record, requestedMode);
Expect(observed.Calls == 2 && Marshal.ReadInt32(instance.Ai, 0x3fc) == expectedMode, label + ": repeat still reaches interface before latch check");
Expect(ReadWord(instance.Cells, 0) == 0xfeedface, label + ": Init latch suppresses a second event dispatch");
}
}
private static void RunClanRefreshMissingCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method) {
Event[] events = (Event[])package.Events.Clone();
bool replaced = false;
for (int i = 0; i < events.Length; i++) {
if (Encoding.ASCII.GetString(events[i].Name) == "Init\0") {
events[i] = new Event(Encoding.ASCII.GetBytes("Missing\0"), events[i].EventWord, events[i].Instructions);
replaced = true; break;
}
}
Expect(replaced, "missing-path package contains Init to remove");
using (Instance instance = BuildInstance(new Package(package.HandlerCount, events), 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
ConfigureClanRefresh(instance, method);
IntPtr record = PrepareClanRecord(instance, 1);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, 2);
Expect(Marshal.ReadByte(instance.Ai, 0x8a8) == 0 && CurrentWarmStartObservation.Calls == 1, "missing Init leaves the native latch clear after the host callback");
Expect(ReadWord(instance.Cells, 0) == 0 && ReadWord(instance.Cells, 1) == 0 && ReadWord(instance.Cells, 2) == 0, "missing Init has no Handler(19) writes");
}
}
private static void RunClanRefreshBusyCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method) {
using (Instance instance = BuildInstance(package, 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
ConfigureClanRefresh(instance, method);
Marshal.WriteByte(instance.Ai, 0x78, 1);
IntPtr record = PrepareClanRecord(instance, 1);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, 2);
Expect(CurrentWarmStartObservation.Calls == 1 && ReadWord(instance.Cells, 0) == 0, "busy recursion guard suppresses Init dispatch");
Expect(Marshal.ReadByte(instance.Ai, 0x78) == 1 && Marshal.ReadByte(instance.Ai, 0x8a8) == 1, "found Init latches even when the preexisting busy guard suppresses dispatch");
Marshal.WriteByte(instance.Ai, 0x78, 0);
Marshal.WriteInt32(instance.Cells, 0, unchecked((int)0xfeedface));
refresh(record, 2);
Expect(CurrentWarmStartObservation.Calls == 2 && Marshal.ReadByte(instance.Ai, 0x78) == 0 && Marshal.ReadByte(instance.Ai, 0x8a8) == 1,
"clearing the busy guard cannot bypass the already-set Init latch");
Expect(ReadWord(instance.Cells, 0) == 0xfeedface, "busy-path latch suppresses a later Init dispatch after the guard clears");
}
}
private static void RunClanRefreshNullAiCase(RefreshClan refresh) {
IntPtr record = Marshal.AllocHGlobal(0x68);
try {
Zero(record, 0x68);
Marshal.WriteInt32(record, 0x0c, 1);
Marshal.WriteInt32(record, 0x18, -1);
refresh(record, 2);
Expect(Marshal.ReadInt32(record, 0x18) == 2, "null AI still stores the requested record mode");
}
finally { Marshal.FreeHGlobal(record); }
}
private static IntPtr ConfigureClanRefresh(Instance instance, IntPtr method) {
IntPtr vtable = instance.Allocate(0x18);
IntPtr host = instance.Allocate(4);
IntPtr hostVtable = instance.Allocate(0x30);
Marshal.WriteInt32(vtable, 0x14, method.ToInt32());
Marshal.WriteInt32(instance.Ai, 0, vtable.ToInt32());
Marshal.WriteInt32(host, 0, hostVtable.ToInt32());
Marshal.WriteInt32(hostVtable, 0x2c, Marshal.GetFunctionPointerForDelegate(WarmStartRecorder).ToInt32());
Marshal.WriteInt32(instance.Ai, 0x438, host.ToInt32());
Marshal.WriteByte(instance.Ai, 0x78, 0);
Marshal.WriteByte(instance.Ai, 0x8a8, 0);
return host;
}
private static IntPtr PrepareClanRecord(Instance instance, int forceFlag) {
IntPtr record = instance.Allocate(0x68);
Marshal.WriteInt32(record, 0x0c, forceFlag);
Marshal.WriteInt32(record, 0x50, instance.Ai.ToInt32());
return record;
}
private static void VerifyEventLookupVectors(Package package, FindEvent find, DispatchEvent dispatch, IntPtr module) {
var duplicateEvents = new Event[package.Events.Length + 1];
Array.Copy(package.Events, duplicateEvents, package.Events.Length);
duplicateEvents[duplicateEvents.Length - 1] = new Event(
Encoding.ASCII.GetBytes("Init\0"), 0xabcdef01,
new Instruction[] { new Instruction(new uint[] { 19, 0xffffffff, 0xffffffff, 0, 0, 3, 0 }, new uint[] { 224, 225, 226 }) });
var duplicatePackage = new Package(package.HandlerCount, duplicateEvents);
using (Instance instance = BuildInstance(duplicatePackage, 500.2857f, 752.0f, 0, IntPtr.Add(module, Handler19Rva))) {
Expect(LookupEvent(find, IntPtr.Add(instance.Ai, 8), "Init") == 0, "native lookup chooses first exact duplicate event");
}
var embeddedNulEvents = (Event[])package.Events.Clone();
for (int i = 0; i < embeddedNulEvents.Length; i++) {
if (Encoding.ASCII.GetString(embeddedNulEvents[i].Name) == "Init\0") {
embeddedNulEvents[i] = new Event(Encoding.ASCII.GetBytes("Init\0suffix\0"), embeddedNulEvents[i].EventWord, embeddedNulEvents[i].Instructions);
break;
}
}
var embeddedNulPackage = new Package(package.HandlerCount, embeddedNulEvents);
using (Instance instance = BuildInstance(embeddedNulPackage, 500.2857f, 752.0f, 0, IntPtr.Add(module, Handler19Rva))) {
Expect(LookupEvent(find, IntPtr.Add(instance.Ai, 8), "Init") == 0, "native lookup stops at an embedded NUL and ignores trailing name bytes");
}
var missingEvents = (Event[])package.Events.Clone();
for (int i = 0; i < missingEvents.Length; i++) {
if (Encoding.ASCII.GetString(missingEvents[i].Name) == "Init\0") {
missingEvents[i] = new Event(Encoding.ASCII.GetBytes("NoInit\0"), missingEvents[i].EventWord, missingEvents[i].Instructions);
}
}
var missingPackage = new Package(package.HandlerCount, missingEvents);
using (Instance instance = BuildInstance(missingPackage, 500.2857f, 752.0f, 0, IntPtr.Add(module, Handler19Rva))) {
IntPtr vm = IntPtr.Add(instance.Ai, 8);
int eventIndex = LookupEvent(find, vm, "Init");
Expect(eventIndex == -1, "native lookup reports missing exact event");
Expect((dispatch(vm, eventIndex) & 0xff) == 0, "dispatching missing event performs no callback");
Expect(Marshal.ReadInt32(instance.Cells, 0) == 0 && Marshal.ReadInt32(instance.Cells, 4) == 0 && Marshal.ReadInt32(instance.Cells, 8) == 0,
"missing event makes no varset writes");
}
Console.WriteLine("original event lookup: first duplicate wins; Init\\0suffix also matches; missing Init returns -1 without writes");
}
private static int LookupEvent(FindEvent find, IntPtr vm, string nameText) {
byte[] nameBytes = Encoding.ASCII.GetBytes(nameText + "\0");
IntPtr name = Marshal.AllocHGlobal(nameBytes.Length);
try {
Marshal.Copy(nameBytes, 0, name, nameBytes.Length);
return find(vm, name);
}
finally { Marshal.FreeHGlobal(name); }
}
private static void RunDwordToHandlerRoundTrip(Package package, FindEvent find, DispatchEvent dispatch, ReadVariable readVariable, SetDwordPosition setDwordPosition, IntPtr module) {
IntPtr position = Marshal.AllocHGlobal(12);
try {
Zero(position, 12);
setDwordPosition(position, UInt32.MaxValue, 10001);
float x = BitConverter.ToSingle(BitConverter.GetBytes(ReadWord(position, 1)), 0);
float y = BitConverter.ToSingle(BitConverter.GetBytes(ReadWord(position, 2)), 0);
Expect(x == 4294967296.0f && y == 10001.0f, "FUN_10005c70 converts DWORD coordinates to the expected single-precision fields");
Console.WriteLine("FUN_10005c70 input=({0:X8}, {1}) fields=({2:R}, {3:R})", UInt32.MaxValue, 10001, x, y);
RunInstance(package, find, dispatch, readVariable, module, x, y, 2, 0, 10001);
}
finally { Marshal.FreeHGlobal(position); }
}
private static void RunInstance(Package package, FindEvent find, DispatchEvent dispatch, ReadVariable readVariable, IntPtr module, float x, float y, uint clanId, uint expectedX, uint expectedY) {
using (Instance instance = BuildInstance(package, x, y, clanId, IntPtr.Add(module, Handler19Rva))) {
IntPtr vm = IntPtr.Add(instance.Ai, 8);
IntPtr name = Marshal.AllocHGlobal(5);
try {
Marshal.Copy(Encoding.ASCII.GetBytes("Init\0"), 0, name, 5);
int eventIndex = find(vm, name);
Expect(eventIndex == 0, "original name lookup returns Init index 0");
uint result = dispatch(vm, eventIndex);
uint first = ReadWord(instance.Cells, 0);
uint second = ReadWord(instance.Cells, 1);
uint third = ReadWord(instance.Cells, 2);
uint getterX = readVariable(IntPtr.Add(instance.VarRecords, 224 * VarSetStride));
uint getterY = readVariable(IntPtr.Add(instance.VarRecords, 225 * VarSetStride));
uint getterClan = readVariable(IntPtr.Add(instance.VarRecords, 226 * VarSetStride));
Console.WriteLine("probe clan={0}: cell_words=({1:X8}, {2:X8}, {3:X8}), native_getter=({4:X8}, {5:X8}, {6:X8})", clanId, first, second, third, getterX, getterY, getterClan);
Expect((result & 0xff) == 1, "dispatcher reports completion");
Expect(Marshal.ReadInt32(vm, 0x40) == 0, "current event field +0x48 is Init index");
Expect(Marshal.ReadInt32(vm, 0x44) == 1, "current instruction field +0x4c advanced once");
Expect(Marshal.ReadInt32(vm, 0x6c) == 0, "condition stack depth reset to zero");
Expect(Marshal.ReadInt32(instance.Ai, 0x50) == 0, "handler result slot +0x50 cleared");
Expect(first == expectedX && second == expectedY && third == clanId, "Handler(19) writes the three referenced DWORD cells in order");
Expect(getterX == first && getterY == second && getterClan == third, "native DWORD getter reads the values written by Handler(19)");
Console.WriteLine("clan={0}: Init={1}, instruction={2}, result={3}, stack={4}, result_slot={5}, cell_words=({6:X8}, {7:X8}, {8:X8}), typed_getters=({9:X8}, {10:X8}, {11:X8})",
clanId, eventIndex, Marshal.ReadInt32(vm, 0x44), result, Marshal.ReadInt32(vm, 0x6c), Marshal.ReadInt32(instance.Ai, 0x50), first, second, third, getterX, getterY, getterClan);
}
finally { Marshal.FreeHGlobal(name); }
}
}
private static Instance BuildInstance(Package package, float x, float y, uint clanId, IntPtr handler19) {
var instance = new Instance();
try {
instance.Ai = instance.Allocate(0x8b0);
instance.EventTable = instance.Allocate(package.Events.Length * EventStride);
instance.HandlerTable = instance.Allocate((int)package.HandlerCount * 4);
instance.VarRecords = instance.Allocate(227 * VarSetStride);
instance.Cells = instance.Allocate(3 * 4);
IntPtr vm = IntPtr.Add(instance.Ai, 8);
Marshal.WriteInt32(vm, 0, package.Events.Length);
Marshal.WriteInt32(vm, 4, instance.EventTable.ToInt32());
Marshal.WriteInt32(vm, 8, (int)package.HandlerCount);
Marshal.WriteInt32(vm, 0x0c, instance.HandlerTable.ToInt32());
Marshal.WriteInt32(vm, 0x3c, instance.Ai.ToInt32());
Marshal.WriteInt32(instance.HandlerTable, 19 * 4, handler19.ToInt32());
for (int eventIndex = 0; eventIndex < package.Events.Length; eventIndex++) {
Event sourceEvent = package.Events[eventIndex];
IntPtr name = instance.Allocate(sourceEvent.Name.Length);
Marshal.Copy(sourceEvent.Name, 0, name, sourceEvent.Name.Length);
IntPtr records = sourceEvent.Instructions.Length == 0 ? IntPtr.Zero : instance.Allocate(sourceEvent.Instructions.Length * InstructionStride);
int eventOffset = eventIndex * EventStride;
Marshal.WriteInt32(instance.EventTable, eventOffset, name.ToInt32());
Marshal.WriteInt32(instance.EventTable, eventOffset + 4, sourceEvent.Name.Length - 1);
Marshal.WriteInt32(instance.EventTable, eventOffset + 8, unchecked((int)sourceEvent.EventWord));
Marshal.WriteInt32(instance.EventTable, eventOffset + 0x0c, sourceEvent.Instructions.Length);
Marshal.WriteInt32(instance.EventTable, eventOffset + 0x10, records.ToInt32());
for (int instructionIndex = 0; instructionIndex < sourceEvent.Instructions.Length; instructionIndex++) {
Instruction sourceInstruction = sourceEvent.Instructions[instructionIndex];
IntPtr refs = sourceInstruction.References.Length == 0 ? IntPtr.Zero : instance.Allocate(sourceInstruction.References.Length * 4);
for (int referenceIndex = 0; referenceIndex < sourceInstruction.References.Length; referenceIndex++)
Marshal.WriteInt32(refs, referenceIndex * 4, unchecked((int)sourceInstruction.References[referenceIndex]));
IntPtr instruction = IntPtr.Add(records, instructionIndex * InstructionStride);
for (int word = 0; word < 5; word++) Marshal.WriteInt32(instruction, word * 4, unchecked((int)sourceInstruction.Words[word]));
Marshal.WriteInt32(instruction, 0x14, refs.ToInt32());
Marshal.WriteInt32(instruction, 0x18, sourceInstruction.References.Length);
Marshal.WriteInt32(instruction, 0x1c, unchecked((int)sourceInstruction.Words[6]));
}
}
Marshal.WriteInt32(instance.Ai, 0x7c, unchecked((int)clanId));
Marshal.WriteInt32(instance.Ai, 0x80, BitConverter.ToInt32(BitConverter.GetBytes(x), 0));
Marshal.WriteInt32(instance.Ai, 0x84, BitConverter.ToInt32(BitConverter.GetBytes(y), 0));
// AI+0x18 is the original varset vector object: +4=count and +0xc=record array.
Marshal.WriteInt32(instance.Ai, 0x1c, 227);
Marshal.WriteInt32(instance.Ai, 0x24, instance.VarRecords.ToInt32());
uint[] targets = { 224, 225, 226 };
for (int i = 0; i < targets.Length; i++) {
IntPtr record = IntPtr.Add(instance.VarRecords, checked((int)targets[i] * VarSetStride));
IntPtr cell = IntPtr.Add(instance.Cells, i * 4);
Marshal.WriteInt32(record, 8, 5); // original parser's DWORD type tag
Marshal.WriteInt32(record, 0x20, cell.ToInt32());
}
return instance;
}
catch { instance.Dispose(); throw; }
}
private static Package ReadPackage(string path) {
using (BinaryReader reader = new BinaryReader(File.OpenRead(path), Encoding.ASCII)) {
uint handlerCount = reader.ReadUInt32();
uint eventCount = reader.ReadUInt32();
if (eventCount > 4096 || handlerCount > 4096) throw new InvalidDataException("Unreasonable package header.");
var events = new Event[eventCount];
for (int e = 0; e < events.Length; e++) {
uint nameLength = reader.ReadUInt32();
if (nameLength > 1024) throw new InvalidDataException("Unreasonable event name length.");
byte[] name = reader.ReadBytes(checked((int)nameLength + 1));
if (name.Length != nameLength + 1 || name[name.Length - 1] != 0) throw new InvalidDataException("Bad event name.");
uint eventWord = reader.ReadUInt32();
uint instructionCount = reader.ReadUInt32();
if (instructionCount > 65536) throw new InvalidDataException("Unreasonable event instruction count.");
var instructions = new Instruction[instructionCount];
for (int i = 0; i < instructions.Length; i++) {
uint[] words = new uint[6];
for (int w = 0; w < words.Length; w++) words[w] = reader.ReadUInt32();
if (words[5] > 4096) throw new InvalidDataException("Unreasonable reference count.");
uint[] refs = new uint[words[5]];
for (int r = 0; r < refs.Length; r++) refs[r] = reader.ReadUInt32();
uint trailing = reader.ReadUInt32();
instructions[i] = new Instruction(new uint[] { words[0], words[1], words[2], words[3], words[4], words[5], trailing }, refs);
}
events[e] = new Event(name, eventWord, instructions);
}
if (reader.BaseStream.Position != reader.BaseStream.Length) throw new InvalidDataException("Trailing bytes in default.scr.");
return new Package(handlerCount, events);
}
}
private static uint ReadWord(IntPtr cells, int index) { return unchecked((uint)Marshal.ReadInt32(cells, index * 4)); }
private static string Hash(string path) {
using (SHA256 sha = SHA256.Create()) using (FileStream input = File.OpenRead(path)) {
byte[] digest = sha.ComputeHash(input); var 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 dispatcher oracle failed: " + message); }
private static void Zero(IntPtr buffer, int length) { for (int offset = 0; offset < length; offset++) Marshal.WriteByte(buffer, offset, 0); }
}
|