fix(runtime): execute the first supported Init event
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s

This commit is contained in:
Valentin Popov committed 2026-10-20 04:11:35 +04:00
1 parent dc8be18458
commit 31fd9d2ea9
8 files changed
+1448 -106

No files matched your search

+395 -50
View File
@@ -16,8 +16,8 @@ use fparkan_prototype::{
};
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_script::{
Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptInstruction,
ScriptPackage, VarSet, VarSetDefault, VmHostCallbackCommand,
Handler19DwordWrite, Handler19InitInput, Handler19InitResolution, Handler19ResolveError,
ScriptInstruction, ScriptPackage, VarSet, VarSetDefault, VmHostCallbackCommand,
};
use fparkan_terrain::SurfaceQuery;
use fparkan_vfs::{Vfs, VfsError};
@@ -166,6 +166,8 @@ pub struct MissionScriptBundle {
/// Losslessly decoded compiled package; only proven `Handler(19)` Init
/// writes are resolved and retained by runtime.
pub package: ScriptPackage,
/// Outcome of the restricted first-`Init` resolution for this bundle.
pub init_execution: MissionScriptInitExecution,
}
/// Numeric script defaults selected for the mission's first script bundle.
@@ -192,12 +194,36 @@ pub struct MissionScriptVarSet {
pub struct MissionScriptInitState {
/// TMA clan index that owns the `SuperAI` instance and varset.
pub clan_index: usize,
/// Zero-based first case-sensitive C-string `Init` event selected by native lookup.
pub event_index: usize,
/// Raw event word preserved from the compiled `Init` event.
pub event_word: u32,
/// Exact three DWORD writes made by each proven `Handler(19)` instruction.
pub writes: Vec<Handler19DwordWrite>,
}
/// Execution status of the first event whose case-sensitive C-string name is `Init`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MissionScriptInitExecution {
/// Native name lookup found no event named `Init` and made no handler call.
Missing,
/// The selected event completed within the currently recovered action subset.
Executed {
/// Zero-based event table index selected by native lookup.
event_index: usize,
},
/// A preview retained the decoded package but stopped before the first
/// instruction outside the recovered `Handler(19)` action subset.
Unsupported {
/// Zero-based event table index selected by native lookup.
event_index: usize,
/// Zero-based instruction index where restricted resolution stopped.
instruction_index: usize,
/// Typed selector, control, or result-save shape that is not modeled.
error: Handler19ResolveError,
},
}
/// Per-clan mutable-value model of one instantiated mission varset.
///
/// Declarations remain shared source metadata; these values are the runtime
@@ -418,6 +444,19 @@ pub enum EngineError {
/// Proven-contract diagnostic.
message: String,
},
/// A full mission load reached an unsupported instruction in its first Init event.
ScriptInitUnsupported {
/// Owning TMA clan index.
clan_index: usize,
/// Resolved `.scr` package path.
path: String,
/// Zero-based first exact Init event index.
event_index: usize,
/// Zero-based instruction index within that event.
instruction_index: usize,
/// Typed unsupported instruction shape.
source: Handler19ResolveError,
},
/// A recovered script handler could not resolve against runtime state.
ScriptRuntime {
/// Owning TMA clan index.
@@ -558,6 +597,16 @@ impl std::fmt::Display for EngineError {
clan_index,
message,
} => write!(f, "clan {clan_index}: script Init failed: {message}"),
Self::ScriptInitUnsupported {
clan_index,
path,
event_index,
instruction_index,
source,
} => write!(
f,
"{path}: clan {clan_index} Init event {event_index} instruction {instruction_index} is unsupported: {source}"
),
Self::ScriptRuntime {
clan_index,
message,
@@ -594,6 +643,7 @@ impl std::error::Error for EngineError {
Self::Nres { source, .. } => Some(source),
Self::Mission { source, .. } => Some(source),
Self::TerrainFormat { source, .. } => Some(source),
Self::ScriptInitUnsupported { source, .. } => Some(source),
Self::Terrain(source) => Some(source),
Self::World(source) => Some(source),
Self::AssetPreparation { source, .. } => Some(source),
@@ -736,7 +786,7 @@ fn load_mission_with_options(
source,
}
})?;
let loaded_scripts = load_mission_scripts(&vfs, &mission)?;
let loaded_scripts = load_mission_scripts(&vfs, &mission, options.asset_scope)?;
let repository = CachedResourceRepository::with_payload_cache_budget(
vfs.clone(),
MISSION_DECODED_PAYLOAD_CACHE_ENTRIES,
@@ -1195,6 +1245,7 @@ fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPa
fn load_mission_scripts(
vfs: &Arc<dyn Vfs>,
mission: &MissionDocument,
asset_scope: MissionAssetScope,
) -> Result<LoadedMissionScripts, EngineError> {
let script_bases = mission_script_bundle_bases(mission);
let mut bundles = Vec::with_capacity(script_bases.len());
@@ -1220,6 +1271,7 @@ fn load_mission_scripts(
base_path,
script_path: normalized.as_str().to_string(),
package,
init_execution: MissionScriptInitExecution::Missing,
});
}
let varset = first_base_path
@@ -1233,8 +1285,9 @@ fn load_mission_scripts(
.iter()
.map(|clan| clan.anchor)
.collect::<Vec<_>>(),
&bundles,
&mut bundles,
varset,
asset_scope,
)?,
None => Vec::new(),
};
@@ -1292,11 +1345,21 @@ fn materialize_script_varset_states(
fn resolve_handler19_init_states(
clan_anchors: &[[f32; 2]],
bundles: &[MissionScriptBundle],
bundles: &mut [MissionScriptBundle],
varset: &MissionScriptVarSet,
asset_scope: MissionAssetScope,
) -> Result<Vec<MissionScriptInitState>, EngineError> {
let mut states = Vec::new();
for bundle in bundles {
if !bundle
.package
.events
.iter()
.any(|event| event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..]))
{
bundle.init_execution = MissionScriptInitExecution::Missing;
continue;
}
let Some(anchor) = clan_anchors.get(bundle.clan_index) else {
return Err(EngineError::ScriptInit {
clan_index: bundle.clan_index,
@@ -1304,41 +1367,60 @@ fn resolve_handler19_init_states(
});
};
let input = Handler19InitInput {
first_x87_word: handler19_x87_truncated_u32(anchor[0], bundle.clan_index, "x")?,
second_x87_word: handler19_x87_truncated_u32(anchor[1], bundle.clan_index, "y")?,
first_x87_word: handler19_anchor_word(anchor[0], bundle.clan_index, "x")?,
second_x87_word: handler19_anchor_word(anchor[1], bundle.clan_index, "y")?,
third_word: u32::try_from(bundle.clan_index).map_err(|_| EngineError::ScriptInit {
clan_index: bundle.clan_index,
message: "clan index does not fit the recovered DWORD ClanID field".to_string(),
})?,
};
for event in &bundle.package.events {
if event.name_raw.as_slice() != b"Init\0" {
continue;
match varset
.declarations
.resolve_handler19_init(&bundle.package, input)
.map_err(|source| EngineError::ScriptInit {
clan_index: bundle.clan_index,
message: source.to_string(),
})? {
Handler19InitResolution::Missing => {
bundle.init_execution = MissionScriptInitExecution::Missing;
}
let writes: Vec<Handler19DwordWrite> = event
.instructions
.iter()
.filter(|instruction| {
instruction.dispatch_selector() == ScriptDispatchSelector::Handler(19)
})
.map(|instruction| {
varset
.declarations
.resolve_handler19(instruction, input)
.map_err(|source| EngineError::ScriptInit {
clan_index: bundle.clan_index,
message: source.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect();
if !writes.is_empty() {
states.push(MissionScriptInitState {
Handler19InitResolution::Executed {
event_index,
event_word,
writes,
} => {
bundle.init_execution = MissionScriptInitExecution::Executed { event_index };
if !writes.is_empty() {
states.push(MissionScriptInitState {
clan_index: bundle.clan_index,
event_index,
event_word,
writes,
});
}
}
Handler19InitResolution::Unsupported {
event_index,
instruction_index,
error,
} if asset_scope != MissionAssetScope::Full => {
bundle.init_execution = MissionScriptInitExecution::Unsupported {
event_index,
instruction_index,
error,
};
}
Handler19InitResolution::Unsupported {
event_index,
instruction_index,
error,
} => {
return Err(EngineError::ScriptInitUnsupported {
clan_index: bundle.clan_index,
event_word: event.event_word,
writes,
path: bundle.script_path.clone(),
event_index,
instruction_index,
source: error,
});
}
}
@@ -1346,24 +1428,29 @@ fn resolve_handler19_init_states(
Ok(states)
}
fn handler19_x87_truncated_u32(
fn handler19_anchor_word(
value: f32,
clan_index: usize,
axis: &'static str,
) -> Result<u32, EngineError> {
if !value.is_finite() || !(0.0..=10_000.0).contains(&value) {
const SIGNED_QWORD_LIMIT: f32 = 9_223_372_036_854_775_808.0;
let rounded = value.round_ties_even();
if !value.is_finite() || rounded < -SIGNED_QWORD_LIMIT || rounded >= SIGNED_QWORD_LIMIT {
return Err(EngineError::ScriptInit {
clan_index,
message: format!(
"ClanBase{axis}={value:?} is outside the recovered CreateSuperAI base range"
"ClanBase{axis}={value:?} is outside the modeled x87 FISTP qword range"
),
});
}
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
// to truncate, performs `fistp qword`, and restores the control word.
let word = value.trunc() as u32;
Ok(word)
// Iron3D FISTP qword keeps its low DWORD; AI FUN_10005c70 then round-trips
// that unsigned word through f32 before Handler(19)'s truncating __ftol.
let creation_word = (rounded as i64) as u32;
let ai_field = creation_word as f32;
let handler_qword = ai_field.trunc() as i64;
Ok(handler_qword as u32)
}
fn load_script_varset(
@@ -1412,6 +1499,7 @@ fn read_vfs(vfs: &Arc<dyn Vfs>, path: &NormalizedPath) -> Result<Arc<[u8]>, Engi
#[cfg(test)]
mod tests {
use super::*;
use fparkan_script::ScriptDispatchSelector;
use fparkan_vfs::{DirectoryVfs, MemoryVfs, VfsEntry, VfsMetadata};
use std::path::{Path, PathBuf};
@@ -1544,7 +1632,7 @@ mod tests {
used_fallback: true,
declarations,
};
let bundles = [MissionScriptBundle {
let mut bundles = [MissionScriptBundle {
clan_index: 1,
base_path: "MISSIONS/SCRIPTS/default".to_string(),
script_path: "MISSIONS/SCRIPTS/default.scr".to_string(),
@@ -1555,22 +1643,28 @@ mod tests {
name_raw: b"Init\0".to_vec(),
event_word: 0x1234_5678,
instructions: vec![fparkan_script::ScriptInstruction {
header_words: [19, 0, 0, 0, 0, 3, 0],
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
references: vec![0, 1, 2],
}],
}],
trailing_bytes: Vec::new(),
raw: Arc::from(&b""[..]),
},
init_execution: MissionScriptInitExecution::Missing,
}];
let states =
resolve_handler19_init_states(&[[500.0, 752.0], [728.0, 449.0]], &bundles, &varset)
.expect("exact Init execution");
let states = resolve_handler19_init_states(
&[[500.0, 752.0], [728.0, 449.0]],
&mut bundles,
&varset,
MissionAssetScope::Full,
)
.expect("exact Init execution");
assert_eq!(
states,
vec![MissionScriptInitState {
clan_index: 1,
event_index: 0,
event_word: 0x1234_5678,
writes: vec![
Handler19DwordWrite {
@@ -1618,13 +1712,159 @@ mod tests {
}
#[test]
fn handler_nineteen_init_rejects_anchor_outside_recovered_base_range() {
let error = handler19_x87_truncated_u32(-0.5, 0, "x").expect_err("negative anchor");
fn handler_nineteen_preview_keeps_unsupported_package_without_partial_writes() {
let varset = MissionScriptVarSet {
path: FALLBACK_SCRIPT_VARSET_PATH.to_string(),
used_fallback: true,
declarations: fparkan_script::parse_varset(
b"VAR( DWORD, x, 950)\nVAR( DWORD, y, 1000)\nVAR( DWORD, id, 0)\n",
)
.expect("varset"),
};
let package = ScriptPackage {
opcode_handler_count: 73,
events: vec![fparkan_script::ScriptEvent {
name_len: 4,
name_raw: b"Init\0".to_vec(),
event_word: 0x1234,
instructions: vec![
fparkan_script::ScriptInstruction {
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
references: vec![0, 1, 2],
},
fparkan_script::ScriptInstruction {
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
references: Vec::new(),
},
],
}],
trailing_bytes: Vec::new(),
raw: Arc::from(&b""[..]),
};
let bundle = || MissionScriptBundle {
clan_index: 0,
base_path: "MISSIONS/SCRIPTS/default".to_string(),
script_path: "MISSIONS/SCRIPTS/default.scr".to_string(),
package: package.clone(),
init_execution: MissionScriptInitExecution::Missing,
};
let anchors = [[500.0, 752.0]];
let mut preview_bundles = vec![bundle()];
let preview_states = resolve_handler19_init_states(
&anchors,
&mut preview_bundles,
&varset,
MissionAssetScope::FirstMeshPreview,
)
.expect("static preview retains unsupported script");
assert!(preview_states.is_empty(), "no partial writes are retained");
assert_eq!(
preview_bundles[0].init_execution,
MissionScriptInitExecution::Unsupported {
event_index: 0,
instruction_index: 1,
error: Handler19ResolveError::UnsupportedSelector {
found: ScriptDispatchSelector::Handler(18),
},
}
);
assert_eq!(
materialize_script_varset_states(&preview_bundles, &varset, &preview_states)
.expect("preview keeps default varset values"),
vec![MissionScriptVarSetState {
clan_index: 0,
values: vec![
VarSetDefault::Dword(950),
VarSetDefault::Dword(1000),
VarSetDefault::Dword(0),
],
}]
);
assert_eq!(preview_bundles[0].package, package);
let mut full_bundles = vec![bundle()];
assert!(matches!(
error,
EngineError::ScriptInit { clan_index: 0, message }
if message.contains("outside the recovered CreateSuperAI base range")
resolve_handler19_init_states(
&anchors,
&mut full_bundles,
&varset,
MissionAssetScope::Full,
),
Err(EngineError::ScriptInitUnsupported {
clan_index: 0,
event_index: 0,
instruction_index: 1,
source: Handler19ResolveError::UnsupportedSelector {
found: ScriptDispatchSelector::Handler(18),
},
..
})
));
let mut malformed_package = package.clone();
malformed_package.events[0].instructions[0]
.references
.clear();
let mut malformed_bundle = vec![MissionScriptBundle {
package: malformed_package,
..bundle()
}];
assert!(matches!(
resolve_handler19_init_states(
&anchors,
&mut malformed_bundle,
&varset,
MissionAssetScope::FirstMeshPreview,
),
Err(EngineError::ScriptInit { message, .. })
if message.contains("Handler(19) is missing reference 0")
));
}
#[test]
fn handler_nineteen_init_matches_original_x87_rounding_and_dword_path() {
for (value, expected) in [
(500.2857, 500),
(751.8217, 752),
(500.5, 500),
(501.5, 502),
(-0.5, 0),
(-0.4, 0),
(-0.6, 0),
(10_000.4, 10_000),
(10_000.5, 10_000),
(10_000.6, 10_001),
(10_001.0, 10_001),
(4_294_967_296.0, 0),
(u32::MAX as f32, 0),
(4_294_967_808.0, 512),
] {
assert_eq!(
handler19_anchor_word(value, 0, "x").expect("supported anchor"),
expected,
"anchor {value:?}"
);
}
assert_eq!(u32::MAX as f32, 4_294_967_296.0);
}
#[test]
fn handler_nineteen_init_rejects_unmodeled_x87_qword_conversions() {
let qword_limit = 9_223_372_036_854_775_808.0_f32;
assert_eq!(
handler19_anchor_word(-qword_limit, 0, "x").expect("valid signed qword minimum"),
0,
"the signed qword minimum is representable before low-DWORD conversion"
);
for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, qword_limit] {
let error =
handler19_anchor_word(value, 0, "x").expect_err("unmodeled FISTP qword input");
assert!(matches!(
error,
EngineError::ScriptInit { clan_index: 0, message }
if message.contains("outside the modeled x87 FISTP qword range")
));
}
}
#[test]
@@ -1750,6 +1990,111 @@ mod tests {
}
}
#[test]
#[ignore = "requires licensed corpus"]
fn licensed_autodemo_load_resolves_native_handler19_init_for_both_clans() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
let mut engine = create(EngineServices::new(vfs)).expect("engine");
let loaded = load_mission(
&mut engine,
MissionRequest {
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
mode: MissionMode::AutoDemo,
},
)
.expect("load licensed AutoDemo");
assert_eq!(
loaded_mission_document(&engine)
.expect("mission document")
.clans
.iter()
.map(|clan| clan.anchor)
.collect::<Vec<_>>(),
vec![[500.2857, 751.8217], [728.0, 449.0]]
);
assert_eq!(loaded.script_bundle_count, 2);
assert_eq!(loaded.script_init_state_count, 2);
assert_eq!(loaded.script_varset_state_count, 2);
assert!(loaded_mission_script_bundles(&engine)
.expect("script bundles")
.iter()
.all(|bundle| bundle.init_execution
== MissionScriptInitExecution::Executed { event_index: 0 }));
let varsets = loaded_mission_script_varset_states(&engine).expect("per-clan varsets");
let clan_zero = varsets
.iter()
.find(|state| state.clan_index == 0)
.expect("first clan varset");
let clan_one = varsets
.iter()
.find(|state| state.clan_index == 1)
.expect("second clan varset");
assert_eq!(
&clan_zero.values[224..227],
&[
VarSetDefault::Dword(500),
VarSetDefault::Dword(752),
VarSetDefault::Dword(0),
]
);
assert_eq!(
&clan_one.values[224..227],
&[
VarSetDefault::Dword(728),
VarSetDefault::Dword(449),
VarSetDefault::Dword(1),
]
);
}
#[test]
#[ignore = "requires licensed corpus"]
fn licensed_campaign_preview_keeps_unsupported_init_without_partial_writes() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
let mut engine = create(EngineServices::new(vfs)).expect("engine");
let loaded = load_mission_static_preview_roots(
&mut engine,
MissionRequest {
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
mode: MissionMode::SingleMission,
},
NonZeroUsize::new(1).expect("non-zero root count"),
)
.expect("load licensed campaign preview");
let bundles = loaded_mission_script_bundles(&engine).expect("script bundles");
let unsupported = bundles
.iter()
.find(|bundle| {
matches!(
&bundle.init_execution,
MissionScriptInitExecution::Unsupported { .. }
)
})
.expect("campaign package with an unsupported Init instruction");
let clan_index = unsupported.clan_index;
let states = loaded_mission_script_init_states(&engine).expect("Init writes");
assert!(states.iter().all(|state| state.clan_index != clan_index));
let varset = loaded_mission_script_varset_states(&engine)
.expect("per-clan varsets")
.iter()
.find(|state| state.clan_index == clan_index)
.expect("unsupported clan varset");
assert_eq!(
&varset.values[224..227],
&[
VarSetDefault::Dword(950),
VarSetDefault::Dword(1000),
VarSetDefault::Dword(0),
]
);
assert!(loaded.script_bundle_count >= 1);
}
#[test]
#[ignore = "requires licensed corpus"]
fn loaded_host_callback_uses_requested_mode_and_constructor_status() {
@@ -0,0 +1,158 @@
using System;
using System.IO;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
internal static class Iron3dAnchorOracle {
private const uint LoadWithAlteredSearchPath = 0x00000008;
private const uint PageExecuteReadWrite = 0x40;
private const uint MemCommitReserve = 0x3000;
private const uint MemRelease = 0x8000;
private const int CallerRva = 0x392e0;
private const int GameGlobalRva = 0x10a5f8;
private const int CreateSuperAiIatRva = 0xe4010;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr LoadLibraryEx(string path, IntPtr file, uint flags);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeLibrary(IntPtr module);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAlloc(IntPtr address, UIntPtr size, uint allocationType, uint protect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(IntPtr address, UIntPtr size, uint freeType);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint protect, out uint oldProtect);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void CreateAiDelegate(IntPtr record, int clanIndex, uint networkPlayer, int networkFlag);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr CreateSuperAiDelegate(uint p0, uint p1, uint p2, uint clanIndex, uint x, uint y, uint p6, uint p7, uint p8, uint callback);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void FpuWordOperation(ref ushort value);
private static FpuWordOperation ReadControlWord;
private static uint CapturedX;
private static uint CapturedY;
private static ushort CapturedControlWord;
private static CreateSuperAiDelegate CaptureDelegate;
private static IntPtr CaptureCreateSuperAi(uint p0, uint p1, uint p2, uint clanIndex, uint x, uint y, uint p6, uint p7, uint p8, uint callback) {
CapturedX = x;
CapturedY = y;
ReadControlWord(ref CapturedControlWord);
return IntPtr.Zero;
}
private static void Require(bool value, string message) {
if (!value) throw new InvalidOperationException(message);
}
private static void Expect(CreateAiDelegate caller, IntPtr record, FpuWordOperation setControlWord, ushort mode, float x, float y, uint expectedX, uint expectedY, string label) {
ushort requested = mode;
setControlWord(ref requested);
Marshal.WriteInt32(record, 4, BitConverter.ToInt32(BitConverter.GetBytes(x), 0));
Marshal.WriteInt32(record, 8, BitConverter.ToInt32(BitConverter.GetBytes(y), 0));
CapturedX = 0xdeadbeef;
CapturedY = 0xdeadbeef;
CapturedControlWord = 0;
caller(record, 0, 0, 0);
string actual = String.Format(CultureInfo.InvariantCulture, "{0}: cw=0x{1:x4} rc={2}, ({3:R}, {4:R}) -> (0x{5:x8}, 0x{6:x8})", label, CapturedControlWord, (CapturedControlWord >> 10) & 3, x, y, CapturedX, CapturedY);
Console.WriteLine(actual);
Require(CapturedX == expectedX && CapturedY == expectedY, label + " expected (0x" + expectedX.ToString("x8") + ",0x" + expectedY.ToString("x8") + ")");
Require(((CapturedControlWord >> 10) & 3) == ((requested >> 10) & 3), label + " x87 RC changed before the native callback");
}
private static IntPtr BuildInstruction(byte[] bytes) {
IntPtr memory = VirtualAlloc(IntPtr.Zero, new UIntPtr(4096), MemCommitReserve, PageExecuteReadWrite);
if (memory == IntPtr.Zero) throw new InvalidOperationException("VirtualAlloc failed: " + Marshal.GetLastWin32Error());
Marshal.Copy(bytes, 0, memory, bytes.Length);
return memory;
}
private static void ProtectWrite(IntPtr address, int value, out uint oldProtect) {
if (!VirtualProtect(address, new UIntPtr(4), PageExecuteReadWrite, out oldProtect)) throw new InvalidOperationException("VirtualProtect failed: " + Marshal.GetLastWin32Error());
Marshal.WriteInt32(address, value);
}
private static void RestoreProtection(IntPtr address, uint oldProtect) {
uint ignored;
if (!VirtualProtect(address, new UIntPtr(4), oldProtect, out ignored)) throw new InvalidOperationException("VirtualProtect restore failed: " + Marshal.GetLastWin32Error());
}
private static void Main() {
Require(IntPtr.Size == 4, "This oracle must run as an x86 process");
string dll = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_IRON3D_DLL");
Require(!String.IsNullOrEmpty(dll) && File.Exists(dll), "Set FPARKAN_ORIGINAL_IRON3D_DLL to the installed iron3d.dll");
string hash;
using (SHA256 sha = SHA256.Create()) using (FileStream stream = File.OpenRead(dll)) hash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
Require(hash == "d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa", "unexpected iron3d.dll hash: " + hash);
IntPtr module = LoadLibraryEx(dll, IntPtr.Zero, LoadWithAlteredSearchPath);
if (module == IntPtr.Zero) throw new InvalidOperationException("LoadLibraryEx failed: " + Marshal.GetLastWin32Error());
IntPtr readCode = IntPtr.Zero;
IntPtr writeCode = IntPtr.Zero;
IntPtr game = Marshal.AllocHGlobal(0x40);
IntPtr manager = Marshal.AllocHGlobal(0x40);
IntPtr record = Marshal.AllocHGlobal(0x68);
uint oldIatProtection = 0;
int oldGlobal = Marshal.ReadInt32(IntPtr.Add(module, GameGlobalRva));
int oldIat = Marshal.ReadInt32(IntPtr.Add(module, CreateSuperAiIatRva));
bool iatChanged = false;
bool controlWordCaptured = false;
ushort originalControlWord = 0;
FpuWordOperation setControlWord = null;
try {
byte[] readBytes = { 0x8b, 0x44, 0x24, 0x04, 0xd9, 0x38, 0xc2, 0x04, 0x00 }; // mov eax,[esp+4]; fnstcw [eax]; ret 4
byte[] writeBytes = { 0x8b, 0x44, 0x24, 0x04, 0xd9, 0x28, 0xc2, 0x04, 0x00 }; // mov eax,[esp+4]; fldcw [eax]; ret 4
readCode = BuildInstruction(readBytes);
writeCode = BuildInstruction(writeBytes);
ReadControlWord = (FpuWordOperation)Marshal.GetDelegateForFunctionPointer(readCode, typeof(FpuWordOperation));
setControlWord = (FpuWordOperation)Marshal.GetDelegateForFunctionPointer(writeCode, typeof(FpuWordOperation));
ReadControlWord(ref originalControlWord);
controlWordCaptured = true;
Console.WriteLine("iron3d.dll sha256=" + hash + " loadedBase=0x" + module.ToInt32().ToString("x8") + " defaultCW=0x" + originalControlWord.ToString("x4") + " defaultRC=" + ((originalControlWord >> 10) & 3));
for (int offset = 0; offset < 0x40; offset += 4) { Marshal.WriteInt32(game, offset, 0); Marshal.WriteInt32(manager, offset, 0); }
for (int offset = 0; offset < 0x68; offset += 4) Marshal.WriteInt32(record, offset, 0);
Marshal.WriteInt32(game, 0x1c, manager.ToInt32());
Marshal.WriteInt32(IntPtr.Add(module, GameGlobalRva), game.ToInt32());
CaptureDelegate = CaptureCreateSuperAi;
IntPtr callbackPointer = Marshal.GetFunctionPointerForDelegate(CaptureDelegate);
IntPtr iat = IntPtr.Add(module, CreateSuperAiIatRva);
ProtectWrite(iat, callbackPointer.ToInt32(), out oldIatProtection);
iatChanged = true;
CreateAiDelegate caller = (CreateAiDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, CallerRva), typeof(CreateAiDelegate));
ushort nearest = (ushort)(originalControlWord & ~0x0c00);
ushort down = (ushort)((originalControlWord & ~0x0c00) | 0x0400);
ushort up = (ushort)((originalControlWord & ~0x0c00) | 0x0800);
ushort truncate = (ushort)((originalControlWord & ~0x0c00) | 0x0c00);
Expect(caller, record, setControlWord, nearest, 500.2857f, 751.8217f, 500, 752, "Autodemo anchors / nearest-even");
Expect(caller, record, setControlWord, nearest, 500.5f, 501.5f, 500, 502, "nearest tie pair");
Expect(caller, record, setControlWord, down, 500.5f, 501.5f, 500, 501, "down tie pair");
Expect(caller, record, setControlWord, up, 500.5f, 501.5f, 501, 502, "up tie pair");
Expect(caller, record, setControlWord, truncate, 500.5f, 501.5f, 500, 501, "truncate tie pair");
Expect(caller, record, setControlWord, nearest, -0.4f, 10000.4f, 0, 10000, "round-in lower/upper boundary");
Expect(caller, record, setControlWord, nearest, -0.6f, 10000.6f, 0xffffffff, 10001, "round-out lower/upper boundary");
Expect(caller, record, setControlWord, nearest, 4294967296.0f, 4294967808.0f, 0, 512, "low-DWORD wrap boundary");
}
finally {
if (iatChanged) {
IntPtr iat = IntPtr.Add(module, CreateSuperAiIatRva);
Marshal.WriteInt32(iat, oldIat);
RestoreProtection(iat, oldIatProtection);
}
Marshal.WriteInt32(IntPtr.Add(module, GameGlobalRva), oldGlobal);
if (controlWordCaptured) setControlWord(ref originalControlWord);
Marshal.FreeHGlobal(record);
Marshal.FreeHGlobal(manager);
Marshal.FreeHGlobal(game);
if (readCode != IntPtr.Zero) VirtualFree(readCode, UIntPtr.Zero, MemRelease);
if (writeCode != IntPtr.Zero) VirtualFree(writeCode, UIntPtr.Zero, MemRelease);
FreeLibrary(module);
}
}
}
@@ -0,0 +1,284 @@
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 ExpectedScriptSha256 = "db1c6fde57aff43181619f19e19e1cacf7301ebb859e8cb883db6b4b7181eced";
private const int EventLookupRva = 0x11f40;
private const int EventDispatchRva = 0x11f10;
private const int Handler19Rva = 0x0aa38;
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);
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 scriptPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_DEFAULT_SCR");
if (String.IsNullOrEmpty(aiPath) || !File.Exists(aiPath) || String.IsNullOrEmpty(scriptPath) || !File.Exists(scriptPath)) {
Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AI_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(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);
Console.WriteLine("original ai.dll dispatcher passed for default.scr Init instances and DWORD-to-handler round-trip; code bytes were not modified");
return 0;
}
finally { FreeLibrary(module); }
}
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); }
}