Appearance
aowlspt_uxpatch.h
Source: abi/aowlspt_uxpatch.h — 525 lines, 16 file-scope functions.
What this header owns
Reproduced verbatim from the header's own banner comment — these notes are frequently the only written record of why the subsystem is shaped the way it is.
text
aowlspt_uxpatch.h -- two small client-side UX fixes for the injected host.
Both are resolved the same proven way as `aowlspt_beclient.h` and the
`aowlspt_bridge.h` targets: a managed method's code RVA is resolved offline by
`tools/il2cpp_resolve.py` (type -> its image -> that image's
Il2CppCodeGenModule.methodPointers[token_rid-1] -> VA -> RVA), byte-verified
against GameAssembly.dll, and used at runtime relative to the mapped
GameAssembly.dll base. RVAs are for imagebase 0x180000000 and this exact
client build (1.1.0.1.46777). Every patch checks the bytes it expects before
writing, so a wrong offset is a skipped patch rather than corrupted code.
## Fix 1 -- Exit hangs the game (this file, a static .text byte patch)
Our setup runs `EscapeFromTarkov.exe` directly with no BsgLauncher, so the
client's Exit path blocks forever. LIVE FINDING: pressing Exit shows an "exit
loading" screen and hangs THERE -- i.e. the hang is in the *async shutdown
work* kicked off the instant the user confirms, and the flow never reaches
`ExitApplication`. So patching `ExitApplication` (the earlier attempt) did
nothing; the terminate has to fire at the confirm-accept point, before the
async work starts.
The menu "Exit" button flows:
EFT.TarkovApplication.ExitApplicationWithConfirm (shows the confirm dialog)
-> on accept (user clicks "Yes"), the dialog invokes the confirm lambda
<ExitApplicationWithConfirm>b__186_0 @ RVA 0x9881D0 <-- PATCHED (primary)
-> this starts the exit-loading screen + async shutdown (logout / save /
matchmaker-leave / launcher handoff) that HANGS with no launcher, and
only eventually would call
EFT.TarkovApplication.ExitApplication @ 0x982610 <-- PATCHED (backstop)
-> <ExitApplication>g__ForceShutdown|187_0 @ 0x988410
`b__186_0` is the sole compiler lambda of `ExitApplicationWithConfirm` and its
body immediately builds the exit/shutdown state (the same struct-init pattern
as ExitApplication/ForceShutdown), so it is unambiguously the "user confirmed
Exit, now do it" accept handler -- and its FIRST instruction runs before the
async work it launches. Overwriting its entry with a clean native
`ExitProcess(0)` terminates the instant the user clicks Yes, skipping the
whole async shutdown and its hang, while the confirm dialog still shows first.
`ExitApplication` is patched too as a harmless backstop (if a flow ever
reaches it, it also cleanly quits). Both are native, no managed dependency,
and cannot themselves hang.
## Fix 1b -- Closing the OS window hangs too (this file, a WndProc subclass)
The Exit-BUTTON patch (Fix 1) intercepts the confirm-accept lambda, but that
is only reached from the in-game menu "Exit" flow. Closing the window any
OTHER way -- the title-bar X, right-click-taskbar-Close, Alt+F4 -- never runs
that lambda. Windows posts WM_CLOSE to the game's top window; Unity's window
proc turns that into `Application.Quit`, which funnels into the SAME async
exit-loading shutdown (logout / save / matchmaker-leave / launcher handoff)
that hangs forever with no BsgLauncher. So the OS-close path hangs exactly
like the pre-fix Exit button did, and the .text patch above does not cover it
because a different caller (Unity's WndProc, not the confirm lambda) reaches
the hang.
RE of the path: WM_CLOSE -> Unity/GameAssembly window proc -> Application.Quit
-> EFT quit -> the async shutdown state-machine (the same one Fix 1 bypasses)
-> hang on the exit-loading screen. Rather than chase and byte-patch that
managed WndProc/Quit chain (build-specific, several methods, and the hang is
inside async continuations that are awkward to neuter), we cut it off at the
cleanest choke point: the WIN32 window message itself. `ExitProcess(0)` on the
close message terminates cleanly before Unity ever acts on it and before any
async shutdown can start, covering the X button, taskbar Close, Alt+F4 and
session logoff/shutdown.
HOW that is done has changed three times under live evidence, and the current
answer is NOT a WndProc subclass. The full history and the mechanism now in
use are documented at the implementation below ("Fix 1b" section); in short:
1. A one-shot subclass at il2cpp-init never installed at all -- the game's
top-level window does not exist that early, so EnumWindows found nothing.
2. Retrying the subclass from the host tick loop fixed the menu case but not
the raid case: entering a raid re-creates the window and re-installs
Unity's own proc, so the subclass was silently gone.
3. Re-subclassing continuously to compensate CRASHED the client -- swapping
another thread's WndProc is unsupported and races its pump.
So there is no subclass any more. Nobody's WndProc is modified. The close
messages are observed with thread-scoped `SetWindowsHookExW` hooks
(WH_GETMESSAGE + WH_CALLWNDPROC) on the game's UI thread -- the supported way
to act on the messages of a window you do not own -- with the
message-independent console-control handler beside it. The whole feature is
behind the `osCloseFix` config flag.
Each entry is overwritten with, at its first byte:
31 C9 xor ecx, ecx ; uExitCode = 0
48 B8 <ExitProcess> mov rax, imm64
FF E0 jmp rax ; tail-jump; never returns
14 bytes. The original is never called (these do not return anyway), so no
trampoline is needed. Guarded: each prologue is verified to be
`40 53 48 83 EC 70` (push rbx ; sub rsp,0x70) before writing, and an
already-patched entry (first byte 0x31) is treated as done. `ExitProcess` is
resolved from kernel32.dll at patch time. On a prologue mismatch that site is
skipped (Exit stays as it was, nothing worse).
## Fix 2 -- version-label branding (target accessor only; the rewrite is in
the Nim host, on the Unity thread)
The bottom-left version label ("1.1.0.1.46777") is composed and set once in
EFT.UI.PreloaderUI.Awake @ RVA 0x1569a20
which reads EFT.Version.get_Current (0x2532f10, its only two callers being
this Awake and the character-creation screen) and calls the label component's
`set_text` with the version. The label component is the reference field at
`this + 0x20` (from Awake's `mov rbx,rcx ; ... ; mov rbp,[rbx+0x20]` feeding
the `set_text` receiver).
The version text is a runtime-allocated managed String built from a
global-metadata string literal, present nowhere in GameAssembly.dll, so there
is nothing static to overwrite -- branding is a managed edit on the Unity
thread. This header only *locates and verifies* the PreloaderUI.Awake code
pointer (like `aowl_bridge_settings_target_at`); the host installs a POSTFIX
detour there.
LIVE FINDING (this build): a managed WRITE via `il2cpp_runtime_invoke`
(get_text/set_text) from inside the detour CRASHES the client, even though the
same postfix reaches the Unity thread and a read-only detour (the settings
probe) is fine -- runtime_invoke needs a MethodInfo/method-pointer this build
protects. So the host does NOT use runtime_invoke. Instead the postfix handler
(`versionBrandFired`) runs in two stages: a READ-ONLY probe that logs the
thread, the readability of `this + 0x20` (the label component), its class, and
the component's `System.String` fields; and an opt-in FIELD-WRITE brand that
allocates a branded String and stores its pointer straight into the
text-backing field by offset -- no runtime_invoke, no findMethod, the same
kind of raw field write `fov`/`sain` use. `aowl_uxpatch_write_ptr` below is
the VirtualQuery-guarded store for that path.
Fail-safe throughout: the prologue is verified before the pointer is handed
out, every dereference is `aowl_is_readable`-guarded, and any doubt leaves the
stock version -- a wrong offset, a missed firing, or an unwritable slot is an
unbranded version, never a crash.Constants
AOWLSPT_UXPATCH_HAOWL_UX_CONFIRM_ACCEPT_RVAAOWL_UX_EXITAPP_RVAAOWL_UX_FORCESHUTDOWN_RVAAOWL_UX_PRELOADER_AWAKE_RVAAOWL_UX_VERSION_LABEL_OFF
Functions
| Signature | Line |
|---|---|
int aowl_ux_write(unsigned char* at, const unsigned char* bytes, int n) | 154 |
int aowl_ux_patch_exit_site(unsigned char* p, FARPROC ep) | 167 |
int32_t aowl_uxpatch_exit_neuter(void) | 190 |
int aowl_ux_is_close_msg(UINT msg, WPARAM wp) | 290 |
LRESULT CALLBACK aowl_ux_getmsg_hook(int code, WPARAM wp, LPARAM lp) | 299 |
LRESULT CALLBACK aowl_ux_cwp_hook(int code, WPARAM wp, LPARAM lp) | 310 |
BOOL CALLBACK aowl_ux_close_enum(HWND h, LPARAM lp) | 323 |
int32_t aowl_uxpatch_close_arm(int32_t* rearmed) | 359 |
int32_t aowl_uxpatch_close_hook(void) | 402 |
uint64_t aowl_uxpatch_close_hwnd(void) | 408 |
uint64_t aowl_uxpatch_close_tid(void) | 413 |
BOOL WINAPI aowl_ux_close_ctrl(DWORD type) | 430 |
int32_t aowl_uxpatch_close_ctrl_arm(void) | 436 |
void aowl_uxpatch_version_target(void) | 462 |
int32_t aowl_uxpatch_version_label_offset(void) | 491 |
int32_t aowl_uxpatch_write_ptr(void* p, int32_t off, void* value) | 502 |

