Appearance
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_HAOWL_CUR_ACT_FREEAOWL_CUR_ACT_HOLDAOWL_CUR_ACT_NONEAOWL_CUR_ACT_RESTOREAOWL_CUR_LOCK_CONFINEDAOWL_CUR_LOCK_LOCKEDAOWL_CUR_LOCK_NONEAOWL_CUR_MAX_FAULTSAOWL_CUR_P_ADMINAOWL_CUR_P_ALLAOWL_CUR_P_DEBUGUIAOWL_CUR_P_OVERLAYAOWL_CUR_P_SETTINGSAOWL_CUR_SRC_COUNTAOWL_CUR_SRC_HOSTAOWL_CUR_SRC_OVERLAYAOWL_CUR_STALE_MSAOWL_CUR_TARGET_COUNTAOWL_CUR_T_GETLOCKAOWL_CUR_T_GETVISAOWL_CUR_T_SETLOCKAOWL_CUR_T_SETVIS
Types
struct AowlCurActstruct AowlCurPubstruct AowlCurStatestruct AowlCurTarget
Functions
| Signature | Line |
|---|---|
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 |

