Skip to content

aowlspt_cursor.h

Source: abi/aowlspt_cursor.h — 669 lines, 43 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_cursor.h -- free the mouse cursor while an aowlspt overlay panel is
open, and put back EXACTLY what the game had when the last one closes.

THE PROBLEM
-----------
In a raid the client holds the cursor with
`UnityEngine.Cursor.lockState = CursorLockMode.Locked` and
`Cursor.visible = false`. Unity implements `Locked` by calling `SetCursorPos`
back to the window centre from inside the player loop, every frame. Our D3D11
overlay panels (F12 manager/settings, F6 admin menu) draw fine on top of that
-- they are rasterised in `Present` and owe nothing to Unity -- but the
pointer is pinned to the middle of the screen, so nothing can be clicked.

Today it appears to work only by accident: if the player already has a game
menu open, the game has ALREADY unlocked the cursor and our panel inherits
that. That accident is also why guessing the restore value is wrong (below).

WHY NOT ClipCursor / ShowCursor (the OS route)
----------------------------------------------
It was considered and REJECTED, and the reason is mechanical rather than
stylistic. `CursorLockMode.Locked` is not a clip: the Unity player calls
`SetCursorPos(centre)` each frame. `ClipCursor` only bounds where the cursor
MAY go -- it does not stop somebody else from moving it, and the centre is
inside any clip rectangle we would set, so the recentring is unaffected.
`ShowCursor(TRUE)` is a REFERENCE COUNT that the player's own
`ShowCursor(FALSE)` decrements again on the next frame; racing it from
another thread is a coin flip that also leaks the count on the way out.
So the OS route cannot fix a cursor that is being teleported, and the managed
route -- turning the lock off at its source -- is the only one that can.

That claim is DERIVED, not measured on this build: it follows from what
`CursorLockMode.Locked` means, not from an experiment run here. It is stated
as reasoning so the next reader can falsify it, and the feature's own
`reasserts` counter (below) is the instrument that settles the related
question of whether the client re-asserts the lock behind us.

WHAT IT DOES
------------
Calls four ordinary managed statics on `UnityEngine.Cursor`, each at a static
RVA byte-verified against the STARTUP PROLOGUE SNAPSHOT (`aowlspt_prologue.h`),
never against live memory:

  UnityEngine.Cursor::get_visible    @0x5291AB0   arity 0
  UnityEngine.Cursor::set_visible    @0x5291B00   arity 1 (bool)
  UnityEngine.Cursor::get_lockState  @0x5291B50   arity 0
  UnityEngine.Cursor::set_lockState  @0x5291BA0   arity 1 (CursorLockMode)

RESOLVED OFFLINE, 2026-08-28, with
  `tools/il2cpp_resolve.py <GameAssembly.dll> <metadata.dec> type UnityEngine.Cursor --shared`
on build 1.1.0.1.46777. All four are in the `il2cpp` section, all four are
real bodies (a stack frame plus the standard `mov rax,[rip+..]; test; jnz`
class-init check -- NOT the universal empty-body stub at 0x628110), and
`--shared` annotated NONE of them, so no RVA here is shared with another
method. Nothing here is DETOURED in any case: every one of the four is
CALLED, which is safe even on a shared address because it is correct code for
the receiver passed. There is no by-name binding anywhere in this file.

`CursorLockMode` (resolved with `... enum CursorLockMode`):
  None = 0, Locked = 1, Confined = 2.

Both setters are non-generic statics, so a NULL trailing `MethodInfo*` is
fine. Static + arity 0 puts the `MethodInfo*` in RCX; static + arity 1 puts
the value in ECX and the `MethodInfo*` in RDX.

WHERE IT RUNS
-------------
On the `EFT.TarkovApplication::Update` drain -- the host's validated
main-thread bridge (RVA 0x977B10) -- as an ALIAS on a slot the bridge already
claimed. NO SECOND DETOUR is installed; a second detour on one function
overwrites the first's trampoline and silently kills it.

`PreloaderUI::Update` would have been the obvious rider and is WRONG here:
it only ticks in the menu (see `abi/aowlspt_region.h`, and the live
inspector's `gInspSlot2` alias which exists for exactly this reason), and a
raid is the whole point of this feature. `TarkovApplication::Update` ticks
for the entire session, menu and raid alike.

The Present hook was also rejected as the carrier: it is the RENDER thread,
and calling managed code off Unity's main thread is not something this host
does. The overlay only PUBLISHES a bitmask from there (a plain interlocked
store); the managed calls all happen on the main thread.

---------------------------------------------------------------------------
THE REF COUNT, AND WHY IT IS A MASK AND NOT A COUNTER
---------------------------------------------------------------------------
F12, F6 and the F3 layout editor can be open at once. "Free on open, lock on
close" would strand the others: closing F6 while F12 is still up would relock
the cursor under an open panel.

A hand-incremented counter is the obvious fix and it is the WRONG one here,
because it is a check that cannot fail: if a panel goes away without
releasing -- a module unloads, a fault path returns early, a scene change
tears a panel down -- the count never reaches zero and the player is left
with a freed cursor in a firefight, permanently, with nothing to notice it.

So the count is derived, every tick, from a LEVEL rather than from EDGES.
Each source republishes its own bitmask of currently-open panels every frame
it runs; a source's publication EXPIRES after `AOWL_CUR_STALE_MS` of silence.
The live mask is the union of the unexpired publications and the "ref count"
is its popcount. A source that stops running therefore releases itself
within 400 ms without anyone having to remember to, and a source that keeps
running keeps its claim. That is the property a counter cannot have.

Sources (bounded, `AOWL_CUR_SRC_COUNT` = 2, so every loop here is capped):
  [0] HOST     -- the F3 debug-overlay layout editor, Unity thread.
  [1] OVERLAY  -- the D3D11 overlay DLL, render thread, via the host export
                  `aowl_cursor_panels_x`. Publishes F12 and the F6 menu.

Panel bits are for diagnostics and for the mask union; nothing keys behaviour
off WHICH panel is open, only off how many.

---------------------------------------------------------------------------
SAVE AND RESTORE -- against a captured value, never a constant
---------------------------------------------------------------------------
On the rising edge (0 panels -> >0) the CURRENT `lockState` and `visible` are
READ FROM THE GAME and stored. On the falling edge (>0 -> 0) exactly those
two values are written back.

Assuming "it was Locked and invisible" would be wrong in the common case,
which is precisely the case that works today: a player who opens a panel from
inside a game menu had `None`/visible, and restoring `Locked`/invisible would
take the cursor away from a menu that is still open. The restore is asserted
against the SAVED value, so the assertion can fail; asserting it against the
constant `Locked` could not.

---------------------------------------------------------------------------
DOES THE GAME FIGHT US? -- measured, not assumed
---------------------------------------------------------------------------
This is written NOT to assume either answer. Every tick while a panel is open
it reads the live state back, and if the game has put the lock back on it
re-applies and increments `reasserts`. So:

  reasserts == 0 over a long panel session  -> a one-shot set survives.
  reasserts ~= ticks                        -> the client re-asserts every
                                               frame and the hold is what
                                               makes the feature work.

Either way the behaviour is correct; the counter turns the open question into
a number in the diag line rather than a guess in a comment. The hold is
bounded by construction -- it exists only while the live mask is non-empty,
and the live mask empties itself on silence.

MOUSELOOK is deliberately NOT suppressed. See `cursorfree.nim` for the
decision and what would settle it.

---------------------------------------------------------------------------
THE EIGHT RULES
---------------------------------------------------------------------------
 1. Prologue byte-verify (16 bytes, against the startup snapshot) before any
    call: `aowl_cur_fn`.
 2. `VirtualQuery` on the target page before the compare, and MEM_COMMIT +
    executable insisted on. There are no pointer HOPS in this feature at all
    -- it never dereferences a game object -- which is most of why it is
    small.
 3. ONE `aowl_p_p_seh`, installed by the caller in `cursorfree.nim`, around
    `aowl_cur_tick_body`. Nothing in this file adds a nested guard.
 4. Every loop is over `AOWL_CUR_SRC_COUNT` (2) or
    `AOWL_CUR_TARGET_COUNT` (4).
 5. Flag-gated `overlayCursorFree`, DEFAULT OFF.
 6. Self-disables after `AOWL_CUR_MAX_FAULTS` faults -- and RESTORES on the
    way out, so switching itself off can never be what leaves the cursor
    freed.
 7. No managed allocation anywhere; the per-tick cost when idle is two
    integer compares.
 8. Never blind-writes: every write is preceded by a read of the same state.

---------------------------------------------------------------------------
OFFLINE TESTABILITY
---------------------------------------------------------------------------
The whole decision -- staleness, the union, the popcount, the save/restore
edges, the re-assert hold, the unwind-on-disable -- is pure arithmetic over
an `AowlCurState` with no pointer to anything and no call into the game.
Compile this header with `AOWL_CUR_PURE` defined and the live half is
omitted entirely, which is what `tests/overlayhost/cursortest.c` does.

Constants

  • AOWLSPT_CURSOR_H
  • AOWL_CUR_ACT_FREE
  • AOWL_CUR_ACT_HOLD
  • AOWL_CUR_ACT_NONE
  • AOWL_CUR_ACT_RESTORE
  • AOWL_CUR_LOCK_CONFINED
  • AOWL_CUR_LOCK_LOCKED
  • AOWL_CUR_LOCK_NONE
  • AOWL_CUR_MAX_FAULTS
  • AOWL_CUR_P_ADMIN
  • AOWL_CUR_P_ALL
  • AOWL_CUR_P_DEBUGUI
  • AOWL_CUR_P_OVERLAY
  • AOWL_CUR_P_SETTINGS
  • AOWL_CUR_SRC_COUNT
  • AOWL_CUR_SRC_HOST
  • AOWL_CUR_SRC_OVERLAY
  • AOWL_CUR_STALE_MS
  • AOWL_CUR_TARGET_COUNT
  • AOWL_CUR_T_GETLOCK
  • AOWL_CUR_T_GETVIS
  • AOWL_CUR_T_SETLOCK
  • AOWL_CUR_T_SETVIS

Types

  • struct AowlCurAct
  • struct AowlCurPub
  • struct AowlCurState
  • struct AowlCurTarget

Functions

SignatureLine
void aowl_cur_reset(AowlCurState* st)254
void aowl_cur_publish(AowlCurState* st, int32_t src, uint32_t mask, uint64_t nowMs)262
uint32_t aowl_cur_live_mask(const AowlCurState* st, uint64_t nowMs)271
int32_t aowl_cur_popcount(uint32_t m)288
int32_t aowl_cur_panels(const AowlCurState* st, uint64_t nowMs)296
void aowl_cur_decide(AowlCurState* st, uint64_t nowMs, int32_t curLock, int32_t curVis, AowlCurAct* out)306
void aowl_cur_did_free(AowlCurState* st, int32_t savedLock, int32_t savedVis)355
void aowl_cur_did_hold(AowlCurState* st)363
void aowl_cur_did_restore(AowlCurState* st)366
void aowl_cur_fault(AowlCurState* st)371
void aowl_cur_fn(int32_t i)429
char aowl_cur_name(int32_t i)461
uint32_t aowl_cur_rva(int32_t i)465
int32_t aowl_cur_target_count(void)469
int32_t aowl_cur_base_ok(void)470
int32_t aowl_cur_ok_count(void)471
int32_t aowl_cur_bad_count(void)472
int32_t aowl_cur_profull_count(void)473
int32_t aowl_cur_get_lock(void)481
int32_t aowl_cur_get_vis(void)486
int32_t aowl_cur_set_lock(int32_t v)492
int32_t aowl_cur_set_vis(int32_t v)498
void aowl_cursor_panels(int32_t mask, uint32_t nowMs)520
void aowl_cur_publish_host(uint32_t mask, uint64_t nowMs)532
int32_t aowl_cur_st_panels(void)537
int32_t aowl_cur_st_mask(void)538
uint32_t aowl_cur_live_mask_now(void)543
int32_t aowl_cur_st_have_saved(void)551
int32_t aowl_cur_st_saved_lock(void)552
int32_t aowl_cur_st_saved_vis(void)553
int32_t aowl_cur_st_off(void)554
int32_t aowl_cur_st_faults(void)555
int64_t aowl_cur_st_frees(void)556
int64_t aowl_cur_st_restores(void)557
int64_t aowl_cur_st_reasserts(void)558
int64_t aowl_cur_st_ticks(void)559
void aowl_cur_st_enable(int32_t on)560
int32_t aowl_cur_st_enabled(void)561
int32_t aowl_cur_act(void)570
int32_t aowl_cur_cur_lock(void)571
int32_t aowl_cur_cur_vis(void)572
int32_t aowl_cur_readback(void)573
void aowl_cur_tick_body(void* a)582

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