Skip to content

Injection Pipeline

The injector is the seam between the desktop world and the game process. It is a faithful x86 CreateRemoteThread + LoadLibraryW implementation with bitness checks and human-readable Win32 errors.

usems-injector.exe
--exe <path> launch mode: create the game suspended, inject, resume
--pid <pid> attach mode: inject into a running process
--args "<arguments>" (launch mode) game command line after the exe path
--dll <path> the core DLL to inject (resolved to absolute)
--wait-module <name> wait until this module appears in the target
--delay <ms> fixed delay before injecting (default 0)
--timeout <ms> timeout for --wait-module (required, > 0)

Exactly one of --exe / --pid is required; --dll always.

Terminal window
usems-injector.exe `
--exe "D:\SteamApps\common\Portal\hl2.exe" `
--args "-game portal -steam -novid -console" `
--dll usems_core.dll `
--delay 8000 --timeout 5000

Creates the process with CREATE_SUSPENDED, resumes it, waits, then injects. The injector sets the game’s working directory to the exe’s own folder so two Source titles never fight over steam_appid.txt or engine mutex names.

Terminal window
usems-injector.exe --pid 39124 --dll usems_core.dll --timeout 5000

inject_into_new(args: &InjectArgs) -> Result<(), InjectError>

Section titled “inject_into_new(args: &InjectArgs) -> Result<(), InjectError>”

Launch-suspended + wait + inject, driven by the InjectArgs struct (mirrors the CLI flags one-to-one). Verifies bitness before touching the process.

let args = InjectArgs {
exe: Some(PathBuf::from(r"D:\...\Portal\hl2.exe")),
args: vec!["-game".into(), "portal".into(), "-steam".into()],
pid: None,
dll: PathBuf::from("usems_core.dll"),
wait_module: None,
delay_ms: 8000,
timeout_ms: 5000,
};
inject_into_new(&args)?;

inject_into_pid(pid: u32, dll_abs: &Path) -> Result<(), InjectError>

Section titled “inject_into_pid(pid: u32, dll_abs: &Path) -> Result<(), InjectError>”

Attach to a live process. Checks IsWow64Process first and refuses bitness mismatches with a plain-language error.

inject_into_pid(39124, &Path::new(r"F:\...\usems_core.dll"))?;
// Err: "bitness mismatch: target is 64-bit, injector is 32-bit"

inject_into_handle(hproc: HANDLE, dll_abs: &Path)

Section titled “inject_into_handle(hproc: HANDLE, dll_abs: &Path)”

The core primitive both modes bottom out in: VirtualAllocExWriteProcessMemory (UTF-16 DLL path) → remote thread at LoadLibraryW → wait → read thread exit code (the loaded HMODULE) → free the remote allocation.

// with any PROCESS_CREATE_THREAD | PROCESS_VM_* | PROCESS_QUERY_INFORMATION handle:
inject_into_handle(hproc, &dll_abs)?;

InjectArgs::validate() -> Result<(), InjectError>

Section titled “InjectArgs::validate() -> Result<(), InjectError>”

The same validation the CLI enforces: exactly one of exe/pid, dll non-empty, timeout > 0.

args.validate()?; // Err: "specify exactly one of --exe (launch) or --pid (attach)"

Every failure mode, with Win32 error text baked into Display:

Variant Meaning
Args(String) invalid CLI/struct input
Io(String) path resolution failure
Win32 { func, code } VirtualAllocEx failed (err 5): Access is denied.-style
Condition(String) semantic failures — module never appeared, process died waiting, remote LoadLibraryW returned NULL

Empirically, on the target machine:

Delay Result
500–600 ms game exits during early init — too early
~8000 ms reliable at main menu
10000 ms+ occasionally VirtualAllocEx err 5 when the game window message pump hiccups

Attach mode against an already-settled process is the stable path; the launcher (Phase 5) will prefer --wait-module engine.dll over fixed delays.