Appearance
aowlspt_botai.h
Source: abi/aowlspt_botai.h — 273 lines, 12 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_botai.h -- BOT AI ACTIVATION RESCUE for the post-1.0 EFT host.
One Unity-thread bool field poke that releases the gate which keeps our
offline scavs standing still at their spawn with a knife in their hands and
their primary slung on their back. Same proven, reflection-free shape as
`aowlspt_botcap.h` (static-RVA detour, prologue byte-verified, VirtualQuery
guarded raw hops, guarded write, flag-gated, fail-safe, whole body under the
VEH/SEH guard). No runtime_invoke, no reflection -- both are DEAD on this
build.
## The activation chain (RE, byte-verified against build 1.1.0.1.46777)
BotSpawner::ActivateBots 0x2565970
-> IBotCreator.ActivateBot 0x1E0F6E0 / 0x1E0FA60 (tail-jumps to
BotCreatorClient::SpawnBot 0x1E0EFE0, with a Comfort Callback)
-> <>c__DisplayClass1{6,7}_0::<ActivateBot>b__0 (the callback)
-> BotCreatorClient::ActivationFaze 0x1E0FDB0
(the ONLY caller of PreActivate: E8 at +0x1E0FFFD)
-> EFT.BotOwner::PreActivate 0x813F60
... builds BotMover / BotWeaponPresetCollection / BotMemory ...
calls BotWeaponSelector::TakeMainWeapon 0xD40100 (at +0x8142DE)
and FINALLY, at +0x8145C4: mov dword ptr [BotOwner+0x30], 1
i.e. `_botState = 1` (PreActivated)
Then, EVERY FRAME:
BotsList::UpdateByUnity 0x1BD6510
-> EFT.BotOwner::UpdateManual 0x81B7C0 (the ONLY caller, E8 @0x1BD6656)
`BotOwner::UpdateManual` is a three-way switch on `_botState`:
+0x81B82A cmp dword ptr [rbx+0x30], 2 ; jne 0x81C440 <- 2 = ACTIVE: the
whole brain runs (BotStandBy::Update, CalcGoal, ShootData,
BotDogFight, BotHeadData, ... the bot thinks and moves)
+0x81C440 cmp dword ptr [rbx+0x30], 1 ; jne <return> <- 1 = PreActivated
+0x81C44A mov rax, [rbx+0x308] <- WeaponManager
+0x81C45A cmp byte ptr [rax+0x80], 0 ; je <return> <- **THE GATE**
(then a NavMesh.SamplePosition; on failure it waits out a
cooldown and teleports the bot to a BotZone spawn point)
+0x81C65F call 0x818F50 <- BotOwner::Activate
which at +0x81A855 does `mov dword ptr [rsi+0x30], 2`.
So a bot only ever leaves state 1 for state 2 -- only ever starts thinking --
if `BotOwner.WeaponManager.IsReady` is true.
## The gate field (byte-verified accessors)
EFT.BotOwner.WeaponManager = pointer at offset **+0x308**
get_WeaponManager @0x80F040 = `48 8B 81 08 03 00 00 C3`
(mov rax,[rcx+0x308]; ret)
set_WeaponManager @0x80F057 writes the same slot; the only other writer is
EFT.BotOwner::Create @0x812825 -- which runs BEFORE PreActivate, so the
slot is already populated at our hook point.
BotWeaponManager.IsReady = bool at offset **+0x80**
get_IsReady @0xA003D0 = `0F B6 81 80 00 00 00 C3` (movzx eax,byte[rcx+0x80])
set_IsReady @0xA003E0 = `88 91 80 00 00 00 C3` (mov byte[rcx+0x80],dl)
EFT.BotOwner._botState = int32 at offset **+0x30**
get_BotState @0x6D2DD0 = `8B 41 30 C3`
set_BotState @0x80FD40 = `... 89 51 30 ...`
## Who sets IsReady, and why ours never do
The ONLY writer of `IsReady = true` in the image is inside
`BotWeaponManager::UpdateFirearmsController` @0xD3CFE0, at +0xD3D45F
(`mov byte ptr [rbp+0x80], r15b`, r15b == 1, immediately after
`LookSensor::Init`). Its callers are, exhaustively:
BotWeaponManager::CheckCurMainWeapon 0xD3C939
BotWeaponManager::UpdateHandsController 0xD3CF48
BotStationaryWeaponData::ImplementStationary
and `UpdateHandsController` is reached ONLY from
BotWeaponSelector::OnWeaponTaken 0xD41073
-- the completion callback of the weapon change that PreActivate kicks off
with `TakeMainWeapon`. If that weapon change never completes, `OnWeaponTaken`
never fires, `IsReady` stays false, and the bot is pinned in state 1 forever:
it stands at its spawn, it never thinks, and its hands still hold the default
Scabbard item (the knife) while the primary stays slung. That is EXACTLY the
reported symptom, and it is one gate, not two.
## What this does
Hooks `EFT.BotOwner::PreActivate` (once per bot, on the Unity thread, RCX =
the BotOwner) and sets `WeaponManager.IsReady = 1`. It does NOT call
Activate, does NOT touch `_botState`, and does NOT skip the original: the
game's own `UpdateManual` still runs its NavMesh check and its own
`Activate()` on its own schedule. All this removes is the weapon-readiness
veto. For a bot whose weapon change would have completed normally the poke
is a no-op in effect (OnWeaponTaken sets the same byte to the same value a
frame or two later); for a bot whose weapon change never completes it is the
difference between a statue and a working scav.
## Hook target: EFT.BotOwner::PreActivate @ RVA 0x813F60
Prologue (byte-verified) -- 16 bytes of clean, whole instructions, no
RIP-relative operand and no branch in the stolen region, comfortably more
than the engine's 14-byte `jmp [rip+0]`:
48 89 5C 24 10 mov [rsp+0x10], rbx (5)
48 89 6C 24 18 mov [rsp+0x18], rbp (5)
48 89 74 24 20 mov [rsp+0x20], rsi (5)
57 push rdi (1)
48 83 EC 40 sub rsp, 0x40 (4)
80 3D 4D 47 .. cmp byte [rip+..],0 (disp32 pins the build; NOT stolen)
On any other build the bytes differ, the guard fails, NULL is returned -- a
missed bind, never a corrupted game.Constants
AOWLSPT_BOTAI_HAOWL_BA_BOTSTATE_OFFAOWL_BA_ISREADY_OFFAOWL_BA_WEAPONMGR_OFFAOWL_BOTAI_TARGET_COUNT
Types
struct AowlBotAiTarget
Functions
| Signature | Line |
|---|---|
int32_t aowl_ba_off_botstate(void) | 116 |
int32_t aowl_ba_off_weaponmgr(void) | 117 |
int32_t aowl_ba_off_isready(void) | 118 |
void aowl_botai_target_at(int32_t i) | 145 |
char aowl_botai_target_name(int32_t i) | 171 |
int32_t aowl_botai_target_count(void) | 176 |
int32_t aowl_ba_slot_readable(void* at, size_t n) | 180 |
int32_t aowl_ba_slot_writable(void* at, size_t n) | 200 |
int32_t aowl_botai_read_i32(void* p, int32_t off, int32_t* ok) | 221 |
void aowl_botai_read_ptr(void* p, int32_t off, int32_t* ok) | 234 |
int32_t aowl_botai_read_u8(void* p, int32_t off, int32_t* ok) | 247 |
int32_t aowl_botai_write_u8(void* p, int32_t off, int32_t value) | 262 |

