Appearance
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_HAOWL_BN_BOTSTATE_OFFAOWL_BN_GOTOPOINT_RVAAOWL_BN_ID_OFFAOWL_BN_ISDEAD_OFFAOWL_BN_MC_PREVPOS_OFFAOWL_BN_MOVER_OFFAOWL_BN_MSM_STATES_OFFAOWL_BN_MV_MOVING_OFFAOWL_BN_MV_MSM_OFFAOWL_BN_MV_SDIST_OFFAOWL_BN_MV_SPEED_OFFAOWL_BN_PLAYER_OFFAOWL_BN_PL_MOVECTX_OFFAOWL_BN_PROFILEID_OFFAOWL_BN_SETSPEED_RVAAOWL_BN_SETTINGS_OFFAOWL_BN_SET_DIFF_OFFAOWL_BN_SET_ROLE_OFFAOWL_BN_STOPMOVE_RVAAOWL_BN_STR_CHARS_OFFAOWL_BN_STR_LEN_OFFAOWL_BOTNAV_TARGET_COUNT
Types
struct AowlBnV3struct AowlBotNavTarget
Functions
| Signature | Line |
|---|---|
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 |

