Skip to content

aowlspt_botnav.h

Source: abi/aowlspt_botnav.h — 508 lines, 35 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_botnav.h -- NATIVE BOT NAVIGATION API for the post-1.0 EFT host.

A live bot registry plus ONE proven movement command, built on the same
reflection-free discipline as `aowlspt_botai.h` / `aowlspt_botcap.h`:
static-RVA target, 16-byte prologue byte-verify, VirtualQuery-guarded raw
hops, whole body under the single VEH/SEH guard, flag-gated, fail-safe.
No runtime_invoke, no reflection, no il2cpp_class_* -- all DEAD on this build.

The full recon that produced every number here, with disassembly, is in
`docs/BOTNAV.md`.

============================================================================
1. THE TICK / REGISTRY POINT:  EFT.BotOwner::UpdateManual @ RVA 0x81B7C0
============================================================================

  BotsList::UpdateByUnity 0x1BD6510 -> EFT.BotOwner::UpdateManual 0x81B7C0
  is the ONLY caller (E8 @0x1BD6656). It runs once per live bot per frame, on
  the Unity main thread, only inside a raid, with RCX = the BotOwner.

  That makes it the ideal single hook for this feature: it is simultaneously
  the census (every live bot announces itself every frame, no enumeration and
  no GameWorld walk needed) and the service tick (each bot's own pending nav
  command is issued while its own UpdateManual runs). It is also, as of kind
  14, hooked by NOTHING else -- so there is zero hook contention. Anything
  that later wants UpdateManual must ride kind 14, not re-detour it.

  Prologue (byte-verified, 16 bytes of whole instructions; the engine needs
  14 for its `jmp [rip+0]`):
    40 53                push rbx                 (2)
    48 81 EC A0 00 00 00 sub  rsp, 0xA0           (7)
    80 3D 08 CF 89 06 00 cmp  byte [rip+..], 0    (7; disp32 pins the build)
  On any other build the bytes differ, the verify fails, NULL comes back, and
  we simply do not bind. A missed feature, never a corrupted game.

============================================================================
2. THE MOVEMENT COMMAND:  EFT.BotOwner::GoToPoint @ RVA 0x81CB40
============================================================================

  NavMeshPathStatus GoToPoint(Vector3 position, bool slowAtTheEnd,
                              float reachDist, bool getUpWithCheck,
                              bool mustHaveWay, bool mustGetUp,
                              bool onlyShortTrie, bool force)

  The whole function is a thin forwarder to BotMover::GoToPoint (0x1A2EE00),
  and disassembling it settles every ABI question outright:

    18081CB40  sub  rsp, 0x68
    18081CB44  xorps xmm0, xmm0
    18081CB47  comiss xmm0, xmm3        ; 0 vs reachDist  ->  reachDist IS XMM3
    18081CB4A  jbe  18081CB6C           ; reachDist >= 0 SKIPS the Settings chain
    18081CB4C  mov  rax, [rcx + 0x68]   ; this->Settings   (only if reachDist < 0)
    18081CB55  mov  rax, [rax + 0x68]
    18081CB5E  mov  rax, [rax + 0x30]
    18081CB67  movss xmm3, [rax + 0x14] ; the default reach distance
    18081CB6C  mov  rcx, [rcx + 0x3d0]  ; this->Mover
    18081CB73  test rcx, rcx / je throw
    18081CB78  mov  eax, [rdx + 8]      ; pos.z   <-- Vector3 arrives BY POINTER
    18081CB7B  movsd xmm0, [rdx]        ; pos.x, pos.y      in RDX
    18081CB7F  lea  rdx, [rsp + 0x50]
    18081CB90  mov  qword [rsp+0x40], 0 ; <== MethodInfo* = NULL, by the GAME
    18081CBB5  mov  byte  [rsp+0x20], 1 ; getUpWithCheck := true (hardcoded)
    18081CBC0  call 181A2EE00           ; BotMover::GoToPoint
    18081CBC9  ret
    18081CBCA  call 1805D2530 / int3    ; managed NullReference throw helper

  What that proves, and why we can call it safely:

  (a) `Vector3` is 12 bytes, so Win64 passes it BY POINTER -- RDX is a
      `Vector3*` to a copy the CALLER owns. We pass a pointer to our own
      stack, so that operand can never be a bad game pointer.
  (b) `float` args land in the XMM register of their positional slot;
      `reachDist` is arg index 3, hence XMM3. Independently corroborated by
      `BotMover::SetTargetMoveSpeed` @0x1A2B4D0, whose entire body is
      `F3 0F 11 89 5C 01 00 00 C3` = `movss [rcx+0x15C], xmm1; ret` -- arg
      index 1 in XMM1, `this` in RCX. That two-instruction function is pure,
      unambiguous ABI ground truth.
  (c) `MethodInfo* = NULL` is CORRECT for this call chain -- the game itself
      stores a literal 0 into that stack slot before calling BotMover.
  (d) Passing `reachDist >= 0` skips the `Settings` pointer chain ENTIRELY, so
      with reachDist = 1.0f the ONLY game pointer this function dereferences
      before forwarding is `[this + 0x3D0]`. A one-hop attack surface.

  Return is `NavMeshPathStatus`: 0 = Complete, 1 = Partial, 2 = Invalid.
  That gives us path validation for free -- we never need NavMesh.CalculatePath
  or NavMesh.SamplePosition interop to know whether a point was reachable.

  ## Pre-flighting the callee, because every bail is a MANAGED THROW

  `BotMover::GoToPoint` @0x1A2EE00 dereferences, in order:
      [mover + 0x80]  _moverStateMachine          -> null: throw
      [msm   + 0x20]  _states (Dictionary)        -> null: throw
      Dictionary::get_Item(_states, EBotMoverState 1) -> null: throw
      [state + 0x20]                              -> null: throw
  and every one of those bails to `call 0x1805D2530; int3`, the managed
  NullReferenceException throw helper. An IL2CPP managed throw unwinding out
  through our native frame is precisely what must not happen, so
  `aowl_botnav_can_command()` walks and VirtualQuery-checks that entire chain
  BEFORE the call is made. The dictionary lookup alone cannot be proven
  statically, so the Nim side additionally requires the bot to have been
  observed already MOVING at least once (its position changed between ticks),
  which empirically proves the mover state machine is populated. Make the
  client tell you.

============================================================================
3. FIELD OFFSETS (all resolved via Il2CppMetadataRegistration.fieldOffsets,
   never guessed; see docs/BOTNAV.md for the cross-checks)
============================================================================

  EFT.BotOwner:  _botState +0x30, Settings +0x68, Mover +0x3D0,
                 ProfileId +0x400, Id +0x408, GetPlayer +0x418, IsDead +0x431
  BotSettings:   _difficulty +0x10, _role (WildSpawnType,int32) +0x14
  BotMover:      _moverStateMachine +0x80, <IsMoving> +0x138,
                 <SDistDestination> +0x148, <MoveSpeed> +0x15C
  BotMoverStateMachine: _states +0x20
  EFT.Player:    MovementContext +0x60
  MovementContext: PreviousPosition (Vector3) +0x370

  NOTE on Player -> BotOwner: we deliberately do NOT use it. `Player.AIData`
  (+0xA00) is typed `IAIData` and has two implementations with DIFFERENT
  layouts (`AIData._botOwner` @+0x28 vs `StubAIData.BotOwner` @+0x30), and
  reflection cannot tell them apart. UpdateManual hands us the BotOwner
  directly in RCX, so the ambiguity never arises. Where the reverse hop is
  unavoidable, the round-trip test `[cand + 0x418] == player` disambiguates it
  without reflection.

Constants

  • AOWLSPT_BOTNAV_H
  • AOWL_BN_BOTSTATE_OFF
  • AOWL_BN_GOTOPOINT_RVA
  • AOWL_BN_ID_OFF
  • AOWL_BN_ISDEAD_OFF
  • AOWL_BN_MC_PREVPOS_OFF
  • AOWL_BN_MOVER_OFF
  • AOWL_BN_MSM_STATES_OFF
  • AOWL_BN_MV_MOVING_OFF
  • AOWL_BN_MV_MSM_OFF
  • AOWL_BN_MV_SDIST_OFF
  • AOWL_BN_MV_SPEED_OFF
  • AOWL_BN_PLAYER_OFF
  • AOWL_BN_PL_MOVECTX_OFF
  • AOWL_BN_PROFILEID_OFF
  • AOWL_BN_SETSPEED_RVA
  • AOWL_BN_SETTINGS_OFF
  • AOWL_BN_SET_DIFF_OFF
  • AOWL_BN_SET_ROLE_OFF
  • AOWL_BN_STOPMOVE_RVA
  • AOWL_BN_STR_CHARS_OFF
  • AOWL_BN_STR_LEN_OFF
  • AOWL_BOTNAV_TARGET_COUNT

Types

  • struct AowlBnV3
  • struct AowlBotNavTarget

Functions

SignatureLine
int32_t aowl_bn_off_botstate(void)159
int32_t aowl_bn_off_mover(void)160
int32_t aowl_bn_off_settings(void)161
int32_t aowl_bn_off_profileid(void)162
int32_t aowl_bn_off_id(void)163
int32_t aowl_bn_off_player(void)164
int32_t aowl_bn_off_isdead(void)165
int32_t aowl_bn_off_role(void)166
int32_t aowl_bn_off_diff(void)167
int32_t aowl_bn_off_sdist(void)168
int32_t aowl_bn_off_moving(void)169
int32_t aowl_bn_off_movectx(void)170
int32_t aowl_bn_off_prevpos(void)171
void aowl_botnav_verify(uint32_t rva, const unsigned char* sig, int32_t siglen)200
void aowl_botnav_target_at(int32_t i)238
char aowl_botnav_target_name(int32_t i)248
int32_t aowl_botnav_target_count(void)253
int32_t aowl_botnav_profull_count(void)254
int32_t aowl_botnav_mismatch_count(void)255
int32_t aowl_bn_slot_readable(void* at, size_t n)259
int32_t aowl_bn_sane(void* p)280
int32_t aowl_botnav_read_i32(void* p, int32_t off, int32_t* ok)285
void aowl_botnav_read_ptr(void* p, int32_t off, int32_t* ok)296
int32_t aowl_botnav_read_u8(void* p, int32_t off, int32_t* ok)307
double aowl_botnav_read_f32(void* p, int32_t off, int32_t* ok)319
int32_t aowl_botnav_read_v3(void* p, int32_t off, double* out)331
int32_t aowl_botnav_read_str(void* p, int32_t off, char* out, int32_t cap)345
void aowl_bn_gotopoint_fn(void)393
void aowl_bn_stopmove_fn(void)396
void aowl_bn_setspeed_fn(void)399
int32_t aowl_botnav_can_command(void* botOwner)411
int32_t aowl_botnav_goto(void* botOwner, double x, double y, double z, double reachDist, int32_t slowAtTheEnd, int32_t force)441
int32_t aowl_botnav_stop(void* botOwner)469
int32_t aowl_botnav_set_speed(void* botOwner, double speed)484
int32_t aowl_botnav_calls_ok(void)502

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