Skip to content

aowlspt_pact.h

Source: abi/aowlspt_pact.h — 852 lines, 33 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_pact.h -- IN-RAID ACTUATION OF THE LOCAL PLAYER ("pact").

The native half of `host/Aowlspt.Host.Il2Cpp/pact.nim`: a byte-verified
static-RVA call table plus one correctly-shaped C thunk per calling
convention, so that a scripted test run can walk, look, shoot, aim, reload,
swap weapons and cycle a magazine WITHOUT a human touching the keyboard.

The motivating case is verbatim from the user: "i shouldn't have to unload
the magazine and reload the ammo for something to trigger for you to
observe". `AOWL_PACT_PIC_UNLOAD_MAG` / `AOWL_PACT_PIC_LOAD_MAG` below are
that case, reachable with no inventory-grid walk at all (see MAGAZINE).

===========================================================================
1. THE BOT DRIVE PATH DOES NOT TRANSFER. MEASURED, NOT ASSUMED.
===========================================================================

`mods/sain/client/drivecalls.nim` drives BOTS through seven RVAs:

    BotMover::Sprint            0x1A2D700
    BotMover::Stop              0x1A2D600
    BotSteering::LookToPoint    0x1A3B690
    ShootData::Shoot            0x1AF7940
    EFT.BotOwner::StopMove      0x81C970
    NavMesh::SamplePosition     0x5238930
    Physics::Raycast            0x5328830

The last two are UnityEngine statics and are receiver-agnostic; a local-player
feature may call them unchanged. The FIRST FIVE cannot be pointed at the local
player, and the reason is structural rather than a matter of taste:

  - `BotMover`, `BotSteering` and `ShootData` are reached ONLY through
    `EFT.BotOwner::get_Mover` @0x80F920 (field +0x3D0), `get_Steering`
    @0x80D8E0 (+0x148) and `get_ShootData` @0x80E8D0 (+0x280). Those are
    `BotOwner` field offsets. The local player is an `EFT.Player`; it is not a
    `BotOwner` and it does not hold one. `EFT.Player::get_AIData` @0x7267B0
    reads NULL for the local player -- that is the discriminator botdiag
    already uses to tell a bot from you.
  - `EFT.BotOwner::StopMove` likewise takes a `BotOwner` receiver.
  - Feeding an `EFT.Player*` to any of the five is TYPE CONFUSION: the callee
    reads a `BotOwner` field offset off a `Player`, gets a plausible pointer
    from an unrelated field, and dereferences it. `VirtualQuery` cannot see
    that and neither can a null check. It would not refuse; it would crash,
    or worse, silently drive garbage.

So the honest answer to "can the local player ride the bot drive layer" is
NO -- the local player has its OWN, entirely separate and strictly richer
actuation surface, which is what this file binds. What CAN and SHOULD be
shared is the layer above: one verb vocabulary (moveTo / lookAt / fire /
reload) with two backends, `sain` drivecalls for a `BotOwner` and this file
for the `EFT.Player`. That is the fold-together, and it is a facade, not a
shared call table. Nothing here should ever be handed a BotOwner and nothing
in drivecalls.nim should ever be handed a Player.

===========================================================================
2. HOW EVERY ROW WAS DERIVED
===========================================================================

`python tools\il2cpp_resolve.py D:\Aowlspt\GameAssembly.dll
 .cache\global-metadata.dec.dat <verb>`, build 1.1.0.1.46777, imagebase
0x180000000. The mandatory self-check passed first, in the same session that
produced every row here: `verify-fields` / `fields System.String` printed
`_stringLength @ 0x10` and `_firstChar @ 0x14`.

For EVERY row below, three things were measured and are recorded:

  RVA         from `type <Type>` (per-image methodPointers, not a guess)
  sharedness  from `Resolver.sharedness(rva)` -- three-state. Every row is
              `unique` EXCEPT the three marked CALL-ONLY, which are folded
              one-instruction getters. Calling a shared RVA is correct code
              for the receiver passed. NOTHING IN THIS FILE IS DETOURED, so
              the shared ones are safe here; a future reader must not promote
              one to a detour target.
  prologue    the 16 bytes at the RVA, verbatim, verified at bind time
              against the STARTUP SNAPSHOT (`aowl_pro_verify`), never against
              live memory.

NOT ONE ROW IS THE UNIVERSAL EMPTY STUB. `0x628110` is `C2 00 00` (`ret 0`),
shared by 6,438 methods on this build; `EFT.Player::ProceedLocalAbsorbedDamage`
and `GamePlayerOwner::Init` both resolve there and are therefore uncallable.
Every prologue below was eyeballed for that pattern and for `B0 01 C3`
(`mov al,1; ret`), which is how `FirearmController::CanPressTrigger`
@0x6B65D0 compiles -- a folded constant-true shared by 584 methods. It is
REFUSED as a fire gate for exactly that reason and does not appear here: a
gate that can only say yes is the defect CLAUDE.md 9b is about.

===========================================================================
3. THE CALLING SHAPES, DERIVED FROM THE COMPILED BODIES
===========================================================================

Win64 + IL2CPP: instance `RCX=this`, then RDX/R8/R9, floats in XMM1..XMM3 at
the matching POSITION, then a hidden trailing `const MethodInfo*`. NULL is
fine here -- no row is a shared generic. Each shape is a separate C typedef
because a call through the wrong one is the defect that once handed
`Transform::set_localPosition` a NULL Vector3 pointer through a perfectly
valid, byte-verified function.

The two that are NOT obvious were derived from the disassembly, not inferred:

  Player::Move(Vector2)   @0x6F3B20
    ... 48 8B D9          mov  rbx, rcx        ; this
        66 48 0F 6E F2    movq xmm6, rdx       ; <-- THE VECTOR2 IS IN RDX
  So an 8-byte Vector2 travels as a PACKED INTEGER in the integer register,
  not in XMM. Win64 has no HFA rule; a 1/2/4/8-byte struct is passed as an
  integer of that size. Shape: (void* this, uint64_t packed, void* mi).
  `packed` low dword = x, high dword = y.

  Player::Rotate(Vector2, bool)  @0x6F4260
        41 0F B6 F8       movzx edi, r8b       ; the bool is in R8
        66 48 0F 6E F2    movq  xmm6, rdx      ; the Vector2 is in RDX
  Shape: (void* this, uint64_t packed, int32_t flag, void* mi).

  Player::ChangePose(float) @0x6F3BE0 -- `0F 28 F1 movaps xmm6,xmm1`: the
  float is in XMM1, i.e. argument slot 1. Ordinary.
  Player::EnableSprint(bool) @0x6F5190 -- `0F B6 FA movzx edi,dl`: DL.

  MovementContext::CalculateLookAtDirection(Vector3,Vector3,float) @0x88C080
        48 8B 41 40       mov   rax,[rcx+0x40] ; this IS in RCX -> no sret
        49 8B D8          mov   rbx, r8        ; playerPos ptr
        48 8B FA          mov   rdi, rdx       ; target ptr
        0F 28 F3          movaps xmm6, xmm3    ; relativeRootHeight
  A 12-byte Vector3 is > 8 bytes so it is passed BY ADDRESS; the 8-byte
  Vector2 return comes back PACKED IN RAX with no hidden return buffer,
  which is proved by `this` occupying RCX rather than slot 1.
  Shape: (void* this, void* pTarget, void* pPlayerPos, float h, void* mi)
         -> uint64_t.

===========================================================================
4. MAGAZINE LOAD / UNLOAD -- the user's own example, with no inventory walk
===========================================================================

The obvious route to `LoadMagazine` needs an `Ammo` stack found by walking
Inventory -> Equipment -> slots -> grids -> items and matching a template.
Every offset in that walk would have to be measured and every hop guarded,
and a wrong one reads a plausible pointer. It is NOT the route taken.

The route taken reaches a type-correct `Ammo` in four hops from a live,
already-validated `EFT.Player`, each hop a byte-verified call whose return
type the metadata states:

  Player.HasFirearmInHands()        @0x727770  -- THE TYPE GUARD, see below
  Player.get_HandsController()      @0x727850  -> AbstractHandsController
  FirearmController.get_Item()      @0x7771D0  -> Weapon
  Weapon.GetCurrentMagazine()       @0x10B48F0 -> Magazine
  Magazine.FirstRealAmmo()          @0x10982F0 -> Item (an Ammo stack)
  Magazine.get_Count()              @0x10982C0 -> int   (the READBACK)

then

  PlayerInventoryController.UnloadMagazine(Magazine, bool)          @0x766BC0
  PlayerInventoryController.LoadMagazine(Ammo, Magazine, int, bool) @0x766930

with the receiver from `Player.get_InventoryController()` @0x727B20.

THE TYPE GUARD IS LOAD-BEARING AND IS NOT OPTIONAL. `get_HandsController`
returns the ABSTRACT base. If the player is holding a knife, a grenade or
nothing, calling `FirearmController::get_Item` on that pointer reads a
FirearmController field offset off an unrelated object -- textbook type
confusion, which `aowl_is_readable` cannot see and a null check passes.
`EFT.Player::HasFirearmInHands()` @0x727770 is the GAME'S OWN answer to
exactly that question, it is arity-0, unique, and it takes the Player we
already trust. pact.nim calls it and refuses out loud on false. There is no
path in this file that reaches `get_Item` without it.

BOTH INVENTORY CALLS RETURN `Task<IResult>` -- they are ASYNCHRONOUS. The
returned pointer is ignored (nothing is allocated by us and nothing managed
is retained). Completion is therefore NOT observable at the call site, which
is why pact.nim POLLS `Magazine.get_Count()` over a bounded number of frames
and reports three outcomes -- took / issued-but-count-never-changed /
never-issued -- rather than echoing the fact that it made the call.

`PlayerInventoryController::LoadMagazine` and `::UnloadMagazine` are both
`VIRTUAL` and NOT sealed. A direct call at the RVA runs THIS body even for a
derived receiver that overrides it. That residual risk is REAL and is stated
rather than hidden: the receiver comes from `Player::get_InventoryController`
on the local player, which in an offline raid is a `PlayerInventoryController`,
and this build gives us no ungated way to ask an object its class. pact.nim
PINS the klass word on first acquisition and refuses on any change, which
catches a swap but not a wrong first answer.

===========================================================================
5. THE COMMAND CHANNEL, AND WHY IT IS THE MOST VALUABLE ROW HERE
===========================================================================

`EFT.GamePlayerOwner::TranslateCommand(ECommand) -> ETranslateResult`
@0xB2B2F0, unique, real body. This is the single funnel every in-raid keybind
goes through, so ONE correctly-shaped call covers 88 verbs including
ReloadWeapon(16), QuickReloadWeapon(19), ChangeWeaponMode(20),
SelectFirstPrimaryWeapon(41) / SelectSecondPrimaryWeapon(42) /
SelectSecondaryWeapon(43), ToggleInventory(37), CheckAmmo(58),
ToggleShooting(1) / EndShooting(2), Jump(40), ToggleDuck(22), ToggleProne(28).
`CanTranslateCommand` @0xB2A830 is the game's own pre-check and is called
first, so a command the game would refuse is reported as REFUSED rather than
issued into the void.

REACHING THE RECEIVER IS THE PROBLEM, AND THE SOLUTION IS EMPIRICAL.
`GamePlayerOwner` is a MonoBehaviour. There is NO field on `EFT.Player` that
points at it -- `holdersof GamePlayerOwner` returns only compiler closure
classes, `RaidDialogEntryPoint._gamePlayerOwner` and
`EftBattleUIScreenController.<Owner>k__BackingField`, none of which we hold.
A `GetComponent` by name is token-gated on this build and its failure mode is
a uniform random non-zero pointer, so that route is not merely awkward, it is
lethal.

So the instance is CAPTURED, not walked: a read-only detour on
`EFT.GamePlayerOwner::LateUpdate` @0xB29130 (unique, owners=1, real body)
records RCX and returns. It writes nothing, allocates nothing, and calls
nothing. It runs on the Unity main thread by construction, because that is
where LateUpdate runs. Nothing else in this repo detours 0xB29130 -- checked
before binding -- so the double-detour trampoline hazard does not apply.
Until that detour has fired at least once the command channel reports
NOT-YET-CAPTURED, which is INCONCLUSIVE and is never reported as a refusal
about the build.

===========================================================================
6. WHAT IS DELIBERATELY ABSENT
===========================================================================

  FirearmController::CanPressTrigger @0x6B65D0
      `B0 01 C3` = `mov al,1; ret`, SHARED by 584 methods. A folded
      constant-true. Bindable, verifiable, and completely useless as a gate.
      REFUSED.
  FirearmController::ReloadMag / QuickReloadMag / ReloadWithAmmo / ReloadBarrels
      every overload takes one or two managed `Callback` delegates. Building
      one needs runtime managed type/delegate construction, which this repo
      records as PLAUSIBLE BUT UNVERIFIED. Passing NULL is not obviously safe
      and was not measured. REFUSED -- use ECommand ReloadWeapon(16) instead,
      which is the same operation through the game's own entry point.
  EFT.Player::Proceed(Weapon, Callback<IFirearmHandsController>, bool)
      @0x742710 and its ten siblings -- same Callback problem. REFUSED for
      weapon swap; use ECommand SelectFirstPrimaryWeapon(41) et al.
  EFT.Player::Create, MovementContext::Create -- RVA=None in the metadata
      (generic/`mvar`); not resolvable offline and not wanted.

===========================================================================
7. SAFETY
===========================================================================

Everything in this header is either a pure accessor over a static table or a
thunk that (a) null-checks fn and self, (b) rejects NaN and absurd magnitudes
before they reach the game, and (c) makes exactly one call. There is no loop,
no allocation and no state machine here. The guard (`aowl_p_p_seh`), the flag
gate, the fault budget, the pointer walk and the readback all live in
pact.nim, under ONE guard for the whole body, because that guard is not
re-entrant.

Depends on `aowlspt_prologue.h` (aowl_pro_verify) and windows.h, both already
included by aowlhost.nim ahead of this file -- the same arrangement
aowlspt_camera.h uses.

Constants

  • AOWLSPT_PACT_H
  • AOWL_PACT_CMD_CHANGE_WEAPON_MODE
  • AOWL_PACT_CMD_CHECK_AMMO
  • AOWL_PACT_CMD_END_SHOOTING
  • AOWL_PACT_CMD_EXAMINE_WEAPON
  • AOWL_PACT_CMD_JUMP
  • AOWL_PACT_CMD_QUICK_RELOAD
  • AOWL_PACT_CMD_RELOAD_WEAPON
  • AOWL_PACT_CMD_SEL_PRIMARY1
  • AOWL_PACT_CMD_SEL_PRIMARY2
  • AOWL_PACT_CMD_SEL_SECONDARY
  • AOWL_PACT_CMD_TOGGLE_DUCK
  • AOWL_PACT_CMD_TOGGLE_INVENTORY
  • AOWL_PACT_CMD_TOGGLE_PRONE
  • AOWL_PACT_CMD_TOGGLE_SHOOTING
  • AOWL_PACT_CMD_TOGGLE_SPRINTING
  • AOWL_PACT_FC_CHANGE_FIREMODE
  • AOWL_PACT_FC_CHECK_AMMO
  • AOWL_PACT_FC_GET_AIMING
  • AOWL_PACT_FC_GET_ITEM
  • AOWL_PACT_FC_SET_AIMING
  • AOWL_PACT_FC_SET_TRIGGER
  • AOWL_PACT_FC_TOGGLE_AIM
  • AOWL_PACT_GPO_CAN_TRANSLATE
  • AOWL_PACT_GPO_GET_MYPLAYER
  • AOWL_PACT_GPO_TRANSLATE
  • AOWL_PACT_GW_MAINPLAYER
  • AOWL_PACT_MC_LOOKAT_DIR
  • AOWL_PACT_MG_FIRST_AMMO
  • AOWL_PACT_MG_GET_COUNT
  • AOWL_PACT_PIC_LOAD_MAG
  • AOWL_PACT_PIC_UNLOAD_MAG
  • AOWL_PACT_PL_AIDATA
  • AOWL_PACT_PL_CHANGE_POSE
  • AOWL_PACT_PL_ENABLE_SPRINT
  • AOWL_PACT_PL_GET_HANDS
  • AOWL_PACT_PL_GET_INVCTRL
  • AOWL_PACT_PL_GET_ISAI
  • AOWL_PACT_PL_GET_ISYOURPLAYER
  • AOWL_PACT_PL_GET_POSITION
  • AOWL_PACT_PL_GET_SPRINTING
  • AOWL_PACT_PL_HAS_FIREARM
  • AOWL_PACT_PL_ISYOU
  • AOWL_PACT_PL_JUMP
  • AOWL_PACT_PL_MOVE
  • AOWL_PACT_PL_MOVECTX
  • AOWL_PACT_PL_ROTATE
  • AOWL_PACT_PL_TOGGLE_LEAN
  • AOWL_PACT_PL_TOGGLE_PRONE
  • AOWL_PACT_PL_TOGGLE_SPRINT
  • AOWL_PACT_TARGET_COUNT
  • AOWL_PACT_WP_CUR_MAGAZINE

Types

  • struct AowlPactTarget

Functions

SignatureLine
int32_t aowl_pact_off_gw_mainplayer(void)531
int32_t aowl_pact_off_pl_isyou(void)532
int32_t aowl_pact_off_pl_aidata(void)533
int32_t aowl_pact_off_pl_movectx(void)534
HMODULE aowl_pact_ga(void)569
int32_t aowl_pact_module_ready(void)570
int32_t aowl_pact_wait_count(void)571
void aowl_pact_fn(int32_t i)573
char aowl_pact_name(int32_t i)634
uint32_t aowl_pact_rva(int32_t i)638
int32_t aowl_pact_target_count(void)642
int32_t aowl_pact_ok_count(void)643
int32_t aowl_pact_bad_count(void)644
int32_t aowl_pact_reason(void)645
char aowl_pact_reason_text(void)646
int32_t aowl_pact_v_p(void* fn, void* self)676
int32_t aowl_pact_b_p(void* fn, void* self)687
void aowl_pact_p_p(void* fn, void* self)694
int32_t aowl_pact_i_p(void* fn, void* self)702
int32_t aowl_pact_v_pb(void* fn, void* self, int32_t b)709
int32_t aowl_pact_v_pf(void* fn, void* self, double v)717
int32_t aowl_pact_v_pu(void* fn, void* self, double x, double y)730
int32_t aowl_pact_v_pub(void* fn, void* self, double x, double y, int32_t flag)745
int32_t aowl_pact_i_pi(void* fn, void* self, int32_t a)762
int32_t aowl_pact_get_v3(void* fn, void* self)773
double aowl_pact_v3x(void)790
double aowl_pact_v3y(void)791
double aowl_pact_v3z(void)792
int32_t aowl_pact_lookat(void* fn, void* self, double tx, double ty, double tz, double px, double py, double pz, double h)800
double aowl_pact_look_x(void)824
double aowl_pact_look_y(void)825
int32_t aowl_pact_load_mag(void* fn, void* self, void* ammo, void* mag, int32_t count, int32_t ignoreRestrictions)833
int32_t aowl_pact_unload_mag(void* fn, void* self, void* mag, int32_t equipmentBlocked)844

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