Appearance
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_HAOWL_MSK_CTOR_RVAAOWL_MSK_PD_NICKNAME_OFFAOWL_MSK_PD_PROFILEID_OFFAOWL_MSK_PD_SIDE_OFFAOWL_MSK_PD_STATUS_OFFAOWL_MSK_SHOWSLOT_RVAAOWL_MSK_STR_CHARS_OFFAOWL_MSK_STR_LEN_OFFAOWL_MSK_STR_MAXAOWL_MSK_SUBMIT_RVAAOWL_MSK_TARGET_COUNTAOWL_MSK_T_CTORAOWL_MSK_T_SHOWSLOTAOWL_MSK_T_SUBMIT
Types
struct AowlMskTarget
Functions
| Signature | Line |
|---|---|
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 |

