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.
Launch mode
Section titled “Launch mode”usems-injector.exe ` --exe "D:\SteamApps\common\Portal\hl2.exe" ` --args "-game portal -steam -novid -console" ` --dll usems_core.dll ` --delay 8000 --timeout 5000Creates 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.
Attach mode
Section titled “Attach mode”usems-injector.exe --pid 39124 --dll usems_core.dll --timeout 5000Library functions
Section titled “Library functions”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: VirtualAllocEx →
WriteProcessMemory (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)"InjectError
Section titled “InjectError”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 |
Timing guidance (Portal 1)
Section titled “Timing guidance (Portal 1)”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.