fix(runtime): preserve native mission outcome guards
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
6 files changed
+524
-68
No files matched your search
@@ -49,13 +49,26 @@ impl EngineServices {
|
||||
}
|
||||
}
|
||||
|
||||
/// Native mission mode used by the recovered host callback.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MissionMode {
|
||||
/// Native application mode 1, used by the selected `missions/single.*` path.
|
||||
SingleMission,
|
||||
/// Native application mode 3, selected by the main-menu idle timer.
|
||||
AutoDemo,
|
||||
}
|
||||
|
||||
/// Mission request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MissionRequest {
|
||||
/// Mission key/path.
|
||||
pub key: String,
|
||||
/// Explicit host mode; this is not inferred from the mission path.
|
||||
pub mode: MissionMode,
|
||||
}
|
||||
|
||||
const INITIAL_SCRIPT_HOST_STATUS: u32 = 5;
|
||||
|
||||
/// Ordered mission property preserved for later runtime stages.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MissionObjectProperty {
|
||||
@@ -197,10 +210,11 @@ pub struct MissionScriptVarSetState {
|
||||
pub values: Vec<VarSetDefault>,
|
||||
}
|
||||
|
||||
/// Rust-side outcome selected by the recovered `Iron3D` host-callback mapper.
|
||||
/// Rust-side outcome selected by the recovered `Iron3D` host callback.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum MissionScriptOutcome {
|
||||
/// No outcome has been recorded in the Rust model.
|
||||
/// No host callback outcome has been recorded; this does not mean that the
|
||||
/// native `IGame` has entered its running state.
|
||||
#[default]
|
||||
Active,
|
||||
/// Rust outcome corresponding to completion payload `1`.
|
||||
@@ -218,7 +232,7 @@ pub enum MissionScriptHostEffect {
|
||||
MissionCompleted,
|
||||
/// The Rust outcome changed to failed.
|
||||
MissionFailed,
|
||||
/// The Rust outcome already held this target state.
|
||||
/// The native status guard rejected a repeated callback without changing state.
|
||||
NoChange,
|
||||
}
|
||||
|
||||
@@ -357,6 +371,8 @@ struct LoadedMissionState {
|
||||
script_varset: Option<MissionScriptVarSet>,
|
||||
script_init_states: Vec<MissionScriptInitState>,
|
||||
script_varset_states: Vec<MissionScriptVarSetState>,
|
||||
script_host_mode: MissionMode,
|
||||
script_host_status: u32,
|
||||
script_outcome: MissionScriptOutcome,
|
||||
}
|
||||
|
||||
@@ -899,6 +915,10 @@ fn load_mission_with_options(
|
||||
script_varset: loaded_scripts.varset,
|
||||
script_init_states: loaded_scripts.init_states,
|
||||
script_varset_states: loaded_scripts.varset_states,
|
||||
script_host_mode: request.mode,
|
||||
// Fresh IGame construction seeds this field with 5; this is not the
|
||||
// later status-4 transition performed after native game initialization.
|
||||
script_host_status: INITIAL_SCRIPT_HOST_STATUS,
|
||||
script_outcome: MissionScriptOutcome::Active,
|
||||
});
|
||||
Ok(summary)
|
||||
@@ -997,8 +1017,9 @@ pub fn loaded_mission_script_varset_states(engine: &Engine) -> Option<&[MissionS
|
||||
///
|
||||
/// Unlike the loader-only resolver, operands are read from the selected
|
||||
/// clan's instantiated varset cells. Dispatching the returned host callback
|
||||
/// remains separate from this function; the Rust outcome mapper does not model
|
||||
/// the native `IGame` guards or initial status word.
|
||||
/// remains separate from this function. The runtime currently models the
|
||||
/// callback's recovered mode and status guards, but not the full game startup
|
||||
/// transition or frame/event execution that reaches it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -1040,13 +1061,16 @@ pub fn loaded_mission_script_outcome(engine: &Engine) -> Option<MissionScriptOut
|
||||
engine.loaded.as_ref().map(|state| state.script_outcome)
|
||||
}
|
||||
|
||||
/// Records the recovered `Iron3D` host-callback target in the Rust outcome.
|
||||
/// Applies the recovered `Iron3D` host-callback guards and records its outcome.
|
||||
///
|
||||
/// `(mode=0, command=0, payload=0|1)` maps payload zero to failure and one to
|
||||
/// completion. Iron3D also checks the gate byte at `IGame + 0xe5` and the
|
||||
/// status word at `IGame + 8`; those native guards and the initial status are
|
||||
/// not represented here. Other callback commands return
|
||||
/// [`MissionScriptHostEffect::Unhandled`].
|
||||
/// Native mission mode `3` closes the callback gate. Otherwise the callback
|
||||
/// must have `(mode=0, command=0)` and payload `0` or `1`. Payload `0` writes
|
||||
/// status `1` and records failure only when the current status is not `1`;
|
||||
/// payload `1` writes status `0` and records completion only when the current
|
||||
/// status is not `0`. A duplicate returns [`MissionScriptHostEffect::NoChange`]
|
||||
/// without repairing any other state. Loaded mission state seeds the raw
|
||||
/// status from the native constructor value `5`; it does not model the later
|
||||
/// game-initialization transition to status `4`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -1063,31 +1087,36 @@ pub fn apply_loaded_script_host_callback(
|
||||
message: "mission script host state is unavailable".to_string(),
|
||||
})?;
|
||||
Ok(apply_recovered_script_host_callback(
|
||||
state.script_host_mode,
|
||||
&mut state.script_host_status,
|
||||
&mut state.script_outcome,
|
||||
command,
|
||||
))
|
||||
}
|
||||
|
||||
fn apply_recovered_script_host_callback(
|
||||
native_mode: MissionMode,
|
||||
status: &mut u32,
|
||||
outcome: &mut MissionScriptOutcome,
|
||||
command: VmHostCallbackCommand,
|
||||
) -> MissionScriptHostEffect {
|
||||
if command.mode != 0 || command.first != 0 {
|
||||
if native_mode == MissionMode::AutoDemo || command.mode != 0 || command.first != 0 {
|
||||
return MissionScriptHostEffect::Unhandled;
|
||||
}
|
||||
let next = match command.second {
|
||||
0 => MissionScriptOutcome::Failed,
|
||||
1 => MissionScriptOutcome::Completed,
|
||||
_ => return MissionScriptHostEffect::Unhandled,
|
||||
};
|
||||
if *outcome == next {
|
||||
return MissionScriptHostEffect::NoChange;
|
||||
}
|
||||
*outcome = next;
|
||||
match next {
|
||||
MissionScriptOutcome::Completed => MissionScriptHostEffect::MissionCompleted,
|
||||
MissionScriptOutcome::Failed => MissionScriptHostEffect::MissionFailed,
|
||||
MissionScriptOutcome::Active => MissionScriptHostEffect::Unhandled,
|
||||
match command.second {
|
||||
0 if *status != 1 => {
|
||||
*status = 1;
|
||||
*outcome = MissionScriptOutcome::Failed;
|
||||
MissionScriptHostEffect::MissionFailed
|
||||
}
|
||||
0 => MissionScriptHostEffect::NoChange,
|
||||
1 if *status != 0 => {
|
||||
*status = 0;
|
||||
*outcome = MissionScriptOutcome::Completed;
|
||||
MissionScriptHostEffect::MissionCompleted
|
||||
}
|
||||
1 => MissionScriptHostEffect::NoChange,
|
||||
_ => MissionScriptHostEffect::Unhandled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,6 +1475,7 @@ mod tests {
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
)
|
||||
.expect_err("missing VFS");
|
||||
@@ -1598,54 +1628,212 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_handler_thirty_host_callback_transitions_mission_outcome() {
|
||||
fn handler_thirty_host_callback_obeys_native_mode_and_status_guards() {
|
||||
fn command(second: u32) -> VmHostCallbackCommand {
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 0,
|
||||
second,
|
||||
}
|
||||
}
|
||||
|
||||
let mut status = 5;
|
||||
let mut outcome = MissionScriptOutcome::Active;
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(0),
|
||||
),
|
||||
MissionScriptHostEffect::MissionFailed
|
||||
);
|
||||
assert_eq!((status, outcome), (1, MissionScriptOutcome::Failed));
|
||||
outcome = MissionScriptOutcome::Completed;
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(0),
|
||||
),
|
||||
MissionScriptHostEffect::NoChange
|
||||
);
|
||||
assert_eq!((status, outcome), (1, MissionScriptOutcome::Completed));
|
||||
outcome = MissionScriptOutcome::Failed;
|
||||
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(1),
|
||||
),
|
||||
MissionScriptHostEffect::MissionCompleted
|
||||
);
|
||||
assert_eq!((status, outcome), (0, MissionScriptOutcome::Completed));
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(1),
|
||||
),
|
||||
MissionScriptHostEffect::NoChange
|
||||
);
|
||||
assert_eq!((status, outcome), (0, MissionScriptOutcome::Completed));
|
||||
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(0),
|
||||
),
|
||||
MissionScriptHostEffect::MissionFailed
|
||||
);
|
||||
assert_eq!((status, outcome), (1, MissionScriptOutcome::Failed));
|
||||
|
||||
status = 4;
|
||||
outcome = MissionScriptOutcome::Active;
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(1),
|
||||
),
|
||||
MissionScriptHostEffect::MissionCompleted
|
||||
);
|
||||
assert_eq!((status, outcome), (0, MissionScriptOutcome::Completed));
|
||||
|
||||
status = 5;
|
||||
outcome = MissionScriptOutcome::Active;
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::AutoDemo,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command(0),
|
||||
),
|
||||
MissionScriptHostEffect::Unhandled
|
||||
);
|
||||
assert_eq!((status, outcome), (5, MissionScriptOutcome::Active));
|
||||
|
||||
for command in [
|
||||
VmHostCallbackCommand {
|
||||
mode: 1,
|
||||
first: 0,
|
||||
second: 0,
|
||||
},
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 3,
|
||||
second: 0,
|
||||
},
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 0,
|
||||
second: 42,
|
||||
},
|
||||
] {
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
MissionMode::SingleMission,
|
||||
&mut status,
|
||||
&mut outcome,
|
||||
command,
|
||||
),
|
||||
MissionScriptHostEffect::Unhandled
|
||||
);
|
||||
assert_eq!((status, outcome), (5, MissionScriptOutcome::Active));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires licensed corpus"]
|
||||
fn loaded_host_callback_uses_requested_mode_and_constructor_status() {
|
||||
let root = licensed_root("IS");
|
||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||
let key = "MISSIONS/Autodemo.00/data.tma".to_string();
|
||||
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||
|
||||
load_mission_static_preview_roots(
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: key.clone(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
NonZeroUsize::new(1).expect("non-zero root count"),
|
||||
)
|
||||
.expect("load single-mission preview");
|
||||
let state = engine.loaded.as_mut().expect("loaded state");
|
||||
assert_eq!(state.script_host_status, INITIAL_SCRIPT_HOST_STATUS);
|
||||
assert_eq!(state.script_outcome, MissionScriptOutcome::Active);
|
||||
assert_eq!(
|
||||
apply_loaded_script_host_callback(
|
||||
&mut engine,
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 0,
|
||||
second: 0,
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("apply failure callback"),
|
||||
MissionScriptHostEffect::MissionFailed
|
||||
);
|
||||
assert_eq!(outcome, MissionScriptOutcome::Failed);
|
||||
assert_eq!(engine.loaded.as_ref().unwrap().script_host_status, 1);
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
&mut outcome,
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 0,
|
||||
second: 1,
|
||||
},
|
||||
),
|
||||
MissionScriptHostEffect::MissionCompleted
|
||||
loaded_mission_script_outcome(&engine),
|
||||
Some(MissionScriptOutcome::Failed)
|
||||
);
|
||||
assert_eq!(outcome, MissionScriptOutcome::Completed);
|
||||
|
||||
engine.loaded.as_mut().unwrap().script_outcome = MissionScriptOutcome::Completed;
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
&mut outcome,
|
||||
apply_loaded_script_host_callback(
|
||||
&mut engine,
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 0,
|
||||
second: 1,
|
||||
second: 0,
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("apply repeated failure callback"),
|
||||
MissionScriptHostEffect::NoChange
|
||||
);
|
||||
assert_eq!(engine.loaded.as_ref().unwrap().script_host_status, 1);
|
||||
assert_eq!(
|
||||
apply_recovered_script_host_callback(
|
||||
&mut outcome,
|
||||
loaded_mission_script_outcome(&engine),
|
||||
Some(MissionScriptOutcome::Completed)
|
||||
);
|
||||
|
||||
load_mission_static_preview_roots(
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key,
|
||||
mode: MissionMode::AutoDemo,
|
||||
},
|
||||
NonZeroUsize::new(1).expect("non-zero root count"),
|
||||
)
|
||||
.expect("reload AutoDemo preview");
|
||||
assert_eq!(engine.loaded.as_ref().unwrap().script_host_status, 5);
|
||||
assert_eq!(
|
||||
apply_loaded_script_host_callback(
|
||||
&mut engine,
|
||||
VmHostCallbackCommand {
|
||||
mode: 0,
|
||||
first: 3,
|
||||
second: 42,
|
||||
first: 0,
|
||||
second: 1,
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("apply AutoDemo callback"),
|
||||
MissionScriptHostEffect::Unhandled
|
||||
);
|
||||
assert_eq!(engine.loaded.as_ref().unwrap().script_host_status, 5);
|
||||
assert_eq!(
|
||||
loaded_mission_script_outcome(&engine),
|
||||
Some(MissionScriptOutcome::Active)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1676,6 +1864,7 @@ mod tests {
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
)
|
||||
.expect("load mission");
|
||||
@@ -1732,12 +1921,14 @@ mod tests {
|
||||
DenyRule::Suffix("Land.map"),
|
||||
MissionRequest {
|
||||
key: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
),
|
||||
(
|
||||
DenyRule::Suffix("objects.rlb"),
|
||||
MissionRequest {
|
||||
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
),
|
||||
] {
|
||||
@@ -1773,6 +1964,7 @@ mod tests {
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
MissionLoadOptions {
|
||||
fail_after_registered_objects: Some(1),
|
||||
@@ -1842,6 +2034,7 @@ mod tests {
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: case.1.to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
)
|
||||
.expect("load mission");
|
||||
@@ -1954,6 +2147,7 @@ mod tests {
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: case.mission.to_string(),
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
)
|
||||
.expect("load selected mission");
|
||||
@@ -1978,8 +2172,14 @@ mod tests {
|
||||
let mut totals = LoadTotals::default();
|
||||
for mission in missions {
|
||||
let mut engine = create(EngineServices::new(vfs.clone())).expect("engine");
|
||||
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
||||
.expect("load mission foundation");
|
||||
let loaded = load_mission(
|
||||
&mut engine,
|
||||
MissionRequest {
|
||||
key: mission,
|
||||
mode: MissionMode::SingleMission,
|
||||
},
|
||||
)
|
||||
.expect("load mission foundation");
|
||||
assert_eq!(loaded.object_count, loaded.registered_objects);
|
||||
assert_eq!(loaded.object_count, loaded.graph_root_count);
|
||||
assert_eq!(
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
internal static class HostCallbackOracle {
|
||||
private const uint LoadWithAlteredSearchPath = 0x00000008;
|
||||
private const uint MemCommitReserve = 0x00003000;
|
||||
private const uint PageExecuteReadWrite = 0x40;
|
||||
private const int CallbackRva = 0x611d0;
|
||||
private const int GameGlobalRva = 0x10a5f8;
|
||||
private const int SoundServerIatRva = 0xe42cc;
|
||||
private const string ExpectedSha256 = "d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa";
|
||||
|
||||
[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);
|
||||
[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 newProtect, out uint oldProtect);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int HostCallback(int gameMode, int command, int payload);
|
||||
|
||||
private static int Main() {
|
||||
if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
|
||||
string dllPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_IRON3D_DLL");
|
||||
if (String.IsNullOrEmpty(dllPath) || !File.Exists(dllPath)) {
|
||||
Console.Error.WriteLine("Set FPARKAN_ORIGINAL_IRON3D_DLL to the original GOG iron3d.dll."); return 3;
|
||||
}
|
||||
string hash;
|
||||
using (SHA256 sha = SHA256.Create()) using (FileStream input = File.OpenRead(dllPath)) {
|
||||
byte[] digest = sha.ComputeHash(input); StringBuilder hex = new StringBuilder();
|
||||
foreach (byte value in digest) hex.Append(value.ToString("x2")); hash = hex.ToString();
|
||||
}
|
||||
if (hash != ExpectedSha256) { Console.Error.WriteLine("Unexpected iron3d.dll SHA-256: " + hash); return 4; }
|
||||
IntPtr module = LoadLibraryEx(dllPath, IntPtr.Zero, LoadWithAlteredSearchPath);
|
||||
if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx failed: " + Marshal.GetLastWin32Error()); return 5; }
|
||||
|
||||
IntPtr game = IntPtr.Zero, app = IntPtr.Zero, manager = IntPtr.Zero, sound = IntPtr.Zero, vtable = IntPtr.Zero;
|
||||
IntPtr callStub = IntPtr.Zero, returnStub = IntPtr.Zero;
|
||||
IntPtr gameGlobal = IntPtr.Zero; int originalGameGlobal = 0; bool gameGlobalPatched = false;
|
||||
uint oldProtect = 0; bool iatPatched = false; int originalSoundPointer = 0;
|
||||
try {
|
||||
game = Marshal.AllocHGlobal(0x200); app = Marshal.AllocHGlobal(0x200); manager = Marshal.AllocHGlobal(0x40);
|
||||
sound = Marshal.AllocHGlobal(4); vtable = Marshal.AllocHGlobal(0x14);
|
||||
IntPtr observations = Marshal.AllocHGlobal(56);
|
||||
try {
|
||||
Zero(game, 0x200); Zero(app, 0x200); Zero(manager, 0x40); Zero(sound, 4); Zero(vtable, 0x14); Zero(observations, 56);
|
||||
Marshal.WriteInt32(game, 4, app.ToInt32()); Marshal.WriteInt32(game, 0x2c, manager.ToInt32());
|
||||
Marshal.WriteInt32(manager, 0x10, IntPtr.Zero.ToInt32());
|
||||
Marshal.WriteInt32(sound, vtable.ToInt32());
|
||||
IntPtr codePage = VirtualAlloc(IntPtr.Zero, new UIntPtr(0x1000), MemCommitReserve, PageExecuteReadWrite);
|
||||
if (codePage == IntPtr.Zero) throw new InvalidOperationException("VirtualAlloc failed: " + Marshal.GetLastWin32Error());
|
||||
callStub = codePage;
|
||||
byte[] soundMethod = BuildSoundMethod(observations, game, app);
|
||||
Marshal.Copy(soundMethod, 0, callStub, soundMethod.Length);
|
||||
Marshal.WriteInt32(vtable, 0x10, callStub.ToInt32());
|
||||
returnStub = IntPtr.Add(codePage, 0x100);
|
||||
byte[] returnCode = { 0xb8, 0, 0, 0, 0, 0xc3 };
|
||||
WriteImm32(returnCode, 1, sound.ToInt32()); Marshal.Copy(returnCode, 0, returnStub, returnCode.Length);
|
||||
|
||||
IntPtr iat = IntPtr.Add(module, SoundServerIatRva);
|
||||
originalSoundPointer = Marshal.ReadInt32(iat);
|
||||
if (!VirtualProtect(iat, new UIntPtr(4), PageExecuteReadWrite, out oldProtect))
|
||||
throw new InvalidOperationException("VirtualProtect(IAT) failed: " + Marshal.GetLastWin32Error());
|
||||
Marshal.WriteInt32(iat, returnStub.ToInt32());
|
||||
iatPatched = true;
|
||||
uint ignored;
|
||||
if (!VirtualProtect(iat, new UIntPtr(4), oldProtect, out ignored))
|
||||
throw new InvalidOperationException("VirtualProtect(IAT restore protection) failed: " + Marshal.GetLastWin32Error());
|
||||
|
||||
gameGlobal = IntPtr.Add(module, GameGlobalRva);
|
||||
originalGameGlobal = Marshal.ReadInt32(gameGlobal);
|
||||
Marshal.WriteInt32(gameGlobal, game.ToInt32());
|
||||
gameGlobalPatched = true;
|
||||
var callback = (HostCallback)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, CallbackRva), typeof(HostCallback));
|
||||
RunAllowedFailure(callback, game, app, observations);
|
||||
RunFailureRepeat(callback, game, app, observations);
|
||||
RunOppositeCompletion(callback, game, app, observations);
|
||||
RunCompletionRepeat(callback, game, app, observations);
|
||||
RunFailureAfterCompletion(callback, game, app, observations);
|
||||
RunAutoDemoGate(callback, game, app, observations);
|
||||
RunNonZeroGameMode(callback, game, app, observations);
|
||||
RunUnknownPayload(callback, game, app, observations);
|
||||
Console.WriteLine("native callback oracle passed; audio endpoint is an isolated no-op recorder");
|
||||
return 0;
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(observations); }
|
||||
}
|
||||
finally {
|
||||
if (gameGlobalPatched) Marshal.WriteInt32(gameGlobal, originalGameGlobal);
|
||||
if (iatPatched) {
|
||||
IntPtr iat = IntPtr.Add(module, SoundServerIatRva); uint restore;
|
||||
if (!VirtualProtect(iat, new UIntPtr(4), PageExecuteReadWrite, out restore))
|
||||
throw new InvalidOperationException("VirtualProtect(IAT cleanup) failed: " + Marshal.GetLastWin32Error());
|
||||
Marshal.WriteInt32(iat, originalSoundPointer);
|
||||
uint ignored;
|
||||
if (!VirtualProtect(iat, new UIntPtr(4), restore, out ignored))
|
||||
throw new InvalidOperationException("VirtualProtect(IAT cleanup restore protection) failed: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
if (callStub != IntPtr.Zero) VirtualFree(callStub, UIntPtr.Zero, 0x8000);
|
||||
if (vtable != IntPtr.Zero) Marshal.FreeHGlobal(vtable);
|
||||
if (sound != IntPtr.Zero) Marshal.FreeHGlobal(sound);
|
||||
if (manager != IntPtr.Zero) Marshal.FreeHGlobal(manager);
|
||||
if (app != IntPtr.Zero) Marshal.FreeHGlobal(app);
|
||||
FreeLibrary(module);
|
||||
if (game != IntPtr.Zero) Marshal.FreeHGlobal(game);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildSoundMethod(IntPtr observations, IntPtr game, IntPtr app) {
|
||||
var code = new List<byte>();
|
||||
code.AddRange(new byte[] { 0x50, 0x52, 0x51, 0x56, 0x57 });
|
||||
code.Add(0xa1); AddImm32(code, observations.ToInt32()); code.Add(0x40); code.Add(0xa3); AddImm32(code, observations.ToInt32());
|
||||
code.AddRange(new byte[] { 0x8b, 0x74, 0x24, 0x18, 0x8b, 0x06, 0x8b, 0x56, 0x04, 0x2b, 0xd0, 0x89, 0x15 }); AddImm32(code, observations.ToInt32() + 4);
|
||||
code.AddRange(new byte[] { 0x83, 0xfa, 0x17, 0x76, 0x09, 0xc6, 0x05 }); AddImm32(code, observations.ToInt32() + 48);
|
||||
code.AddRange(new byte[] { 0x01, 0xeb, 0x0f, 0xbf }); AddImm32(code, observations.ToInt32() + 12);
|
||||
code.AddRange(new byte[] { 0x8b, 0xca, 0x8b, 0xf0, 0xfc, 0xf3, 0xa4, 0xc6, 0x07, 0x00 });
|
||||
code.AddRange(new byte[] { 0x8b, 0x44, 0x24, 0x1c, 0xa3 }); AddImm32(code, observations.ToInt32() + 8);
|
||||
code.Add(0xb8); AddImm32(code, game.ToInt32());
|
||||
code.AddRange(new byte[] { 0x8b, 0x40, 0x08, 0xa3 }); AddImm32(code, observations.ToInt32() + 40);
|
||||
code.Add(0xb8); AddImm32(code, app.ToInt32());
|
||||
code.AddRange(new byte[] { 0x0f, 0xb6, 0x80, 0x57, 0x01, 0x00, 0x00, 0xa3 }); AddImm32(code, observations.ToInt32() + 44);
|
||||
code.AddRange(new byte[] { 0x5f, 0x5e, 0x59, 0x5a, 0x58, 0xc2, 0x08, 0x00 });
|
||||
return code.ToArray();
|
||||
}
|
||||
|
||||
private static void RunAllowedFailure(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Setup(game, app, obs, 0, 5, 0x7f); int result = callback(0, 0, 0);
|
||||
Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0, "failure state writes");
|
||||
Expect(Marshal.ReadInt32(obs, 0) == 1 && Marshal.ReadInt32(obs, 40) == 5 && Marshal.ReadInt32(obs, 44) == 0x7f, "failure sound before writes");
|
||||
Console.WriteLine("fail: status=1 mission_flag=0 sound={0}", SoundId(obs));
|
||||
Expect(SoundId(obs) == "VOICE_MISSION_FAIL" && Marshal.ReadInt32(obs, 8) == 0, "failure voice id and argument");
|
||||
}
|
||||
private static void RunFailureRepeat(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Marshal.WriteByte(app, 0x157, 0x7f);
|
||||
int result = callback(0, 0, 0);
|
||||
Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 1, "repeat failure preserves mismatched mission flag");
|
||||
}
|
||||
private static void RunOppositeCompletion(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
int result = callback(0, 0, 1); Expect(result == 1 && Marshal.ReadInt32(game, 8) == 0 && Marshal.ReadByte(app, 0x157) == 1 && Marshal.ReadInt32(obs, 0) == 2, "completion after failure");
|
||||
Expect(Marshal.ReadInt32(obs, 40) == 1 && Marshal.ReadInt32(obs, 44) == 0x7f && SoundId(obs) == "VOICE_MISSION_COMPLETE" && Marshal.ReadInt32(obs, 8) == 0, "completion sound before writes");
|
||||
}
|
||||
private static void RunCompletionRepeat(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Marshal.WriteByte(app, 0x157, 0x7f);
|
||||
int result = callback(0, 0, 1);
|
||||
Expect(result == 1 && Marshal.ReadInt32(game, 8) == 0 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 2, "repeat completion preserves mismatched mission flag");
|
||||
}
|
||||
private static void RunFailureAfterCompletion(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
int result = callback(0, 0, 0);
|
||||
Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0 && Marshal.ReadInt32(obs, 0) == 3, "failure after completion");
|
||||
Expect(Marshal.ReadInt32(obs, 40) == 0 && Marshal.ReadInt32(obs, 44) == 0x7f && SoundId(obs) == "VOICE_MISSION_FAIL" && Marshal.ReadInt32(obs, 8) == 0, "reverse transition sound before writes");
|
||||
Console.WriteLine("fail-after-complete: status=1 mission_flag=0 sound={0}", SoundId(obs));
|
||||
}
|
||||
private static void RunAutoDemoGate(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Setup(game, app, obs, 1, 5, 0x7f); callback(0, 0, 0);
|
||||
Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "AutoDemo gate suppresses callback");
|
||||
}
|
||||
private static void RunNonZeroGameMode(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Setup(game, app, obs, 0, 5, 0x7f); callback(1, 0, 0);
|
||||
Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "nonzero callback mode suppressed");
|
||||
}
|
||||
private static void RunUnknownPayload(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
|
||||
Setup(game, app, obs, 0, 5, 0x7f); callback(0, 0, 2);
|
||||
Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "unknown payload suppressed");
|
||||
}
|
||||
private static void Setup(IntPtr game, IntPtr app, IntPtr obs, byte gate, int status, byte missionFlag) {
|
||||
Marshal.WriteByte(game, 0xe5, gate); Marshal.WriteInt32(game, 8, status); Marshal.WriteByte(app, 0x157, missionFlag);
|
||||
Zero(obs, 56);
|
||||
}
|
||||
private static void Expect(bool value, string description) { if (!value) throw new InvalidOperationException("native oracle assertion failed: " + description); }
|
||||
private static string SoundId(IntPtr observations) {
|
||||
int length = Marshal.ReadInt32(observations, 4);
|
||||
if (Marshal.ReadByte(observations, 48) != 0) throw new InvalidOperationException("native sound identifier exceeded recorder capacity");
|
||||
if (length < 0 || length > 23) throw new InvalidOperationException("unexpected sound identifier length: " + length);
|
||||
byte[] bytes = new byte[length]; Marshal.Copy(IntPtr.Add(observations, 12), bytes, 0, length);
|
||||
return Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
private static void Zero(IntPtr buffer, int length) { for (int offset = 0; offset < length; offset++) Marshal.WriteByte(buffer, offset, 0); }
|
||||
private static void AddImm32(List<byte> code, int value) { byte[] bytes = BitConverter.GetBytes(value); code.AddRange(bytes); }
|
||||
private static void WriteImm32(byte[] bytes, int offset, int value) { byte[] immediate = BitConverter.GetBytes(value); Buffer.BlockCopy(immediate, 0, bytes, offset, 4); }
|
||||
}
|
||||
Reference in new issue
Block a user