Skip to content

aowlspt_modeskip.h

Source: abi/aowlspt_modeskip.h — 382 lines, 22 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_modeskip.h -- never show the character/mode selection screen.

===========================================================================
WHAT THIS IS
===========================================================================

The post-1.0 client stops at `EFT.UI.CharacterSelectionScreen` and waits for
a human to pick a game mode and a character slot. The user never wants to see
it: the launcher ALREADY chose a profile (`tools/aowllaunch.nim` picks one and
passes `-token=<id>`; without that token the client dies at "Client not
authenticated"), so the screen is asking a question that has already been
answered.

This feature answers it programmatically. It rides the screen's own code
path and calls the same `Submit` the slot's action button calls.

===========================================================================
THE ROUTE, AND WHY THESE THREE TARGETS
===========================================================================

Resolved OFFLINE from the decrypted `global-metadata.dat` with
`tools/il2cpp_resolve.py` (`type 14120`, `type 14118`, `bytes <RVA>`) and
`tools/fldoff.py`. Nothing here was guessed and nothing came from a string
search of the metadata (managed strings there are encrypted).

  EFT.UI.CharacterSelectionScreen           typedef 14120
      ShowSlot(CharacterSelectionSlotViewBase slotView, EGameMode gameMode,
               CharacterSelectionProfileData profileData,
               CharacterSelectionScreenController controller)
                                             RVA 0x13efae0   arity 4
  CharacterSelectionScreenController        typedef 14118
      .ctor(CharacterSelectionDataResponse, bool, string,
            Nullable<EGameMode>, SeasonalPerksData, bool)
                                             RVA 0x13f0530   arity 6
      Submit(EGameMode gameMode, CharacterSelectionProfileData profileData)
                                             RVA 0x13f0bf0   arity 2

All three are UNSHARED -- `il2cpp_resolve.py type ... --shared` annotates
neither of them, while it flags e.g. `get_ProfileId` as 185-way folded. That
matters because detouring a shared RVA fires for every method folded onto it.

WHY THE .ctor IS IN THIS LIST -- the constraint that shaped the design.
`Submit` is an INSTANCE method on the CONTROLLER, so calling it needs the
controller pointer. `ShowSlot` receives the controller, but as its FOURTH
argument: on the Win64 ABI an instance method's arguments are
`RCX=this, RDX=arg0, R8=arg1, R9=arg2`, and arg3 goes ON THE STACK. The
host's detour thunk saves only `RCX..R9` (`AowlRegs` in `aowlspt_shim.h`), so
the controller is simply NOT VISIBLE from a ShowSlot detour. That was
measured, not assumed -- the arity-4 signature above is what makes it true.

Two routes to the controller were rejected on evidence:
  * `CharacterSelectionScreen::Show(controller)` @0x13ef4d0 takes it in RDX
    and would be ideal, but its 14-byte relocation window contains a
    rip-relative `80 3D` at byte 10, so its prologue cannot be relocated.
  * `CharacterSelectionSlotViewBase` does not STORE the controller --
    `tools/fldoff.py fields CharacterSelectionSlotViewBase` lists every field
    through 0x180 and there is no controller among them.

So the controller is captured where it is unambiguously in RCX: its own
constructor. `.ctor` runs exactly once per controller, before any slot is
shown, and `this` is in RCX by definition.

FIELD OFFSETS (tools/fldoff.py, which self-checks System.String
_stringLength@0x10 / _firstChar@0x14 before printing anything):

  EFT.CharacterSelectionProfileData  typedef 8775
      0x20  Status      ECharacterSelectionProfileStatus
      0x60  Nickname    string
      0x74  Side        EPlayerSide
      0x78  ProfileId   string

===========================================================================
WHAT IS MATCHED, AND WHAT IS DELIBERATELY NOT
===========================================================================

The predicate is ProfileId EQUALITY against the launcher's `launchProfileId`,
and nothing else.

`Status` is READ and LOGGED but NOT gated on. That is deliberate and it is a
refusal to guess: `ECharacterSelectionProfileStatus`'s constant values live in
metadata `fieldDefaultValues`, which neither resolver verb exposes, so the
integer meaning "Available" is not something this host KNOWS. Gating on a
guessed enum value would be exactly the "never guess an offset" mistake in
another costume. ProfileId equality is strictly stronger anyway: an empty slot
carries no ProfileId to match, so it cannot be selected by accident, and the
one profile the launcher bound is by construction the one that is available.

===========================================================================
SAFETY
===========================================================================

Every RVA is checked to land in COMMITTED EXECUTABLE memory with
`VirtualQuery` BEFORE its 16 prologue bytes are compared, because a stale RVA
on another build can point at an uncommitted page and `memcmp` there faults.
The comparison itself is against the STARTUP SNAPSHOT (`aowl_pro_verify`,
`aowlspt_prologue.h`), never against live memory -- verifying against live
bytes after another feature has patched a function reads that feature's
trampoline and self-rejects. On any build but this one every target is
refused and the feature is a silent no-op, never a hazard.

The Nim caller (`modeskip.nim`) is flag-gated `uxSkipModeScreen`, default OFF,
runs its whole body under ONE `aowl_p_p_seh` (never nested), guards every
pointer hop, caps iteration, self-disables after a few faults, and calls
Submit from the TarkovApplication::Update drain -- never inline inside
ShowSlot, which would re-enter the screen's own code from inside its own
call.

HOW LONG IT DEFERS, AND WHY THAT IS NOT A FRAME COUNT.
Deferring two drain ticks (~16ms) was measured to BREAK THE CLIENT: the
player was left on a blank background with no menu. `TarkovApplication`
shows this screen a SECOND time from its own async
`RunInitialLobbyFlow -> RunCharacterSelectionFlow ->
 ShowCharacterSelectionScreen -> ShowScreenAsync -> DisplayScreen -> Show`,
372ms after the first slots appear. Answering before that lands makes the
late `Show` throw `NullReferenceException` inside
`EFT.UI.CharacterSelectionSeasonPanel.ShowPerks`, which aborts the whole
lobby-flow task, so `MenuScreen` is never activated. `modeskip.nim` now
waits for WALL-CLOCK QUIET on `ShowSlot` instead, and declines outright
rather than pressing if quiet never arrives.

Constants

  • AOWLSPT_MODESKIP_H
  • AOWL_MSK_CTOR_RVA
  • AOWL_MSK_PD_NICKNAME_OFF
  • AOWL_MSK_PD_PROFILEID_OFF
  • AOWL_MSK_PD_SIDE_OFF
  • AOWL_MSK_PD_STATUS_OFF
  • AOWL_MSK_SHOWSLOT_RVA
  • AOWL_MSK_STR_CHARS_OFF
  • AOWL_MSK_STR_LEN_OFF
  • AOWL_MSK_STR_MAX
  • AOWL_MSK_SUBMIT_RVA
  • AOWL_MSK_TARGET_COUNT
  • AOWL_MSK_T_CTOR
  • AOWL_MSK_T_SHOWSLOT
  • AOWL_MSK_T_SUBMIT

Types

  • struct AowlMskTarget

Functions

SignatureLine
void aowl_msk_fn(int32_t i)212
char aowl_msk_name(int32_t i)244
uint32_t aowl_msk_rva(int32_t i)248
int32_t aowl_msk_target_count(void)252
int32_t aowl_msk_base_ok(void)253
int32_t aowl_msk_ok_count(void)254
int32_t aowl_msk_bad_count(void)255
int32_t aowl_msk_profull_count(void)256
int32_t aowl_msk_off_status(void)258
int32_t aowl_msk_off_nickname(void)259
int32_t aowl_msk_off_side(void)260
int32_t aowl_msk_off_profileid(void)261
int32_t aowl_msk_readable(const void* p, size_t n)270
void aowl_msk_read_ptr(void* base, int32_t off)282
int32_t aowl_msk_read_i32(void* base, int32_t off)290
int32_t aowl_msk_str_copy(void* s, char* out, int32_t cap)307
int32_t aowl_msk_profile_is(void* profileData, const char* want)332
int32_t aowl_msk_profile_id(void* profileData, char* out, int32_t cap)344
int32_t aowl_msk_profile_nick(void* profileData, char* out, int32_t cap)348
int32_t aowl_msk_profile_status(void* profileData)352
int32_t aowl_msk_profile_side(void* profileData)355
void aowl_msk_call_submit(void* fn, void* controller, uint64_t gameMode, void* profileData)375

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