Skip to content

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_H
  • AOWL_UX_CONFIRM_ACCEPT_RVA
  • AOWL_UX_EXITAPP_RVA
  • AOWL_UX_FORCESHUTDOWN_RVA
  • AOWL_UX_PRELOADER_AWAKE_RVA
  • AOWL_UX_VERSION_LABEL_OFF

Functions

SignatureLine
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

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.