Appearance
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_HAOWL_PACT_CMD_CHANGE_WEAPON_MODEAOWL_PACT_CMD_CHECK_AMMOAOWL_PACT_CMD_END_SHOOTINGAOWL_PACT_CMD_EXAMINE_WEAPONAOWL_PACT_CMD_JUMPAOWL_PACT_CMD_QUICK_RELOADAOWL_PACT_CMD_RELOAD_WEAPONAOWL_PACT_CMD_SEL_PRIMARY1AOWL_PACT_CMD_SEL_PRIMARY2AOWL_PACT_CMD_SEL_SECONDARYAOWL_PACT_CMD_TOGGLE_DUCKAOWL_PACT_CMD_TOGGLE_INVENTORYAOWL_PACT_CMD_TOGGLE_PRONEAOWL_PACT_CMD_TOGGLE_SHOOTINGAOWL_PACT_CMD_TOGGLE_SPRINTINGAOWL_PACT_FC_CHANGE_FIREMODEAOWL_PACT_FC_CHECK_AMMOAOWL_PACT_FC_GET_AIMINGAOWL_PACT_FC_GET_ITEMAOWL_PACT_FC_SET_AIMINGAOWL_PACT_FC_SET_TRIGGERAOWL_PACT_FC_TOGGLE_AIMAOWL_PACT_GPO_CAN_TRANSLATEAOWL_PACT_GPO_GET_MYPLAYERAOWL_PACT_GPO_TRANSLATEAOWL_PACT_GW_MAINPLAYERAOWL_PACT_MC_LOOKAT_DIRAOWL_PACT_MG_FIRST_AMMOAOWL_PACT_MG_GET_COUNTAOWL_PACT_PIC_LOAD_MAGAOWL_PACT_PIC_UNLOAD_MAGAOWL_PACT_PL_AIDATAAOWL_PACT_PL_CHANGE_POSEAOWL_PACT_PL_ENABLE_SPRINTAOWL_PACT_PL_GET_HANDSAOWL_PACT_PL_GET_INVCTRLAOWL_PACT_PL_GET_ISAIAOWL_PACT_PL_GET_ISYOURPLAYERAOWL_PACT_PL_GET_POSITIONAOWL_PACT_PL_GET_SPRINTINGAOWL_PACT_PL_HAS_FIREARMAOWL_PACT_PL_ISYOUAOWL_PACT_PL_JUMPAOWL_PACT_PL_MOVEAOWL_PACT_PL_MOVECTXAOWL_PACT_PL_ROTATEAOWL_PACT_PL_TOGGLE_LEANAOWL_PACT_PL_TOGGLE_PRONEAOWL_PACT_PL_TOGGLE_SPRINTAOWL_PACT_TARGET_COUNTAOWL_PACT_WP_CUR_MAGAZINE
Types
struct AowlPactTarget
Functions
| Signature | Line |
|---|---|
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 |

