Skip to content

[DO NOT MERGE] Mount behavior v2 - testing branch, destined for upstream - #2

Open
icemansparks wants to merge 10 commits into
merkerhoodfrom
feature/mount-behavior-v2-rebased
Open

[DO NOT MERGE] Mount behavior v2 - testing branch, destined for upstream#2
icemansparks wants to merge 10 commits into
merkerhoodfrom
feature/mount-behavior-v2-rebased

Conversation

@icemansparks

@icemansparks icemansparks commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Not for merge here. This branch carries the mount behavior v2 work, rebased onto a current base so it builds again, and is kept as a staging branch until it is proposed upstream.

What is in here

Master pointer validation. Bots hold a raw Player* master that dangles when the master logs out and the Player object is destroyed between AI ticks on the map-update threads. SetMaster() now records the master GUID and GetMaster() re-resolves it through ObjectAccessor::FindConnectedPlayer, returning the freshly resolved object or nullptr. Callers that dereferenced the raw member go through the validated accessor, and UpdateAIGroupMaster re-syncs the cached pointer on any change rather than only on loss, so a same-GUID relog cannot leave a stale pointer behind.

Mount behavior. Bots stay mounted while closing distance to a dismounted master, mount up when riding actually beats running, and dismount to engage an assist target instead of riding past the fight to the master's position. Mount handling only applies while the bot is following; a bot told to stay keeps its current mount state.

The mirror rule fires when the bot is within TooCloseDistance of the master or whenever the master is actually riding. Distance alone left a band between TooCloseDistance and CalculateMountDistance() where neither the mirror nor the mount-to-close branch fired: 5 to 21 yd for melee and 5 to 38.5 yd for casters, since CalculateMountDistance() is max(21, SpellDistance + 10) and SpellDistance defaults to 28.5. CalculateMountDistance() is the break-even for closing a fixed gap, which is the wrong test against a master who is opening one, and a ground mount matches the master's speed rather than beating it, so a bot that waited for the gap to cross the threshold held that distance for the rest of the ride.

Rebase notes

Upstream replaced HasRealPlayerMaster, HasActivePlayerMaster and IsAlt with HasGameClientMaster and IsAltBot. Resolved by keeping the upstream shape and routing HasGameClientMaster through the validated accessor:

bool PlayerbotAI::HasGameClientMaster()
{
    Player* validMaster = GetMaster();
    return IsRealPlayer(validMaster) || IsSelfBot(validMaster);
}

Both helpers are null-safe, and the master == bot short circuit in GetMaster() keeps selfbots out of a recursion through IsSelfBot.

feature/mount-behavior-v2-upstream is the same commits rebased onto current upstream master, for the eventual upstream proposal. Its only conflicts were the east-const conversion touching the line above the two new declarations in CheckMountStateAction.h; apps/codestyle/codestyle-cpp.py passes all checks on it.

Status

Builds clean on both bases. Behaviour still needs review and hands-on testing before this is proposed upstream.

Bots hold a raw Player* master that dangles when the master logs out
and the Player object is destroyed between AI ticks on the map-update
threads. FollowAction::isUseful then crashes on fTarget->GetGUID()
(reproduced twice, cores captured; same UAF family as mod-playerbots#2474; the
crashing branch was introduced in mod-playerbots#2462).

SetMaster() now records the master GUID and GetMaster() re-validates it
through ObjectAccessor::FindPlayer, which never dereferences the stored
pointer, returning nullptr once the master is gone. HasRealPlayerMaster,
HasActivePlayerMaster and the GetGroupLeader fallback go through the
validated accessor, UpdateAIGroupMaster clears a stale master at entry,
and GossipHelloAction handles the now-possible null master.
Reattempt of PR mod-playerbots#1760 (reverted in mod-playerbots#1855). Bots now stay mounted when
the master dismounts far away, and mount up to reach a distant master,
dismounting at assist range when the master is in combat.

Unlike the original PR, the shared target-based mount/dismount logic is
untouched and the feature is scoped entirely to the master-following
branch, which battleground bots never enter. This removes the BG
mounted-loop regression that caused the revert. Both helpers also
require the follow strategy, so bots told to stay are unaffected.
- Gate the whole master mount-mirror block on the follow strategy: a bot
  told to stay is parked and no longer mounts when the master mounts
- Clamp the in-combat dismount range to at least 18yd: for non-warrior
  melee CalculateDismountDistance() is ~3yd, which made the bot ride to
  the master's exact position and hover at follow distance without
  engaging
- Reduce the out-of-combat proximity range to 5yd so the bot follows
  until near the master and then mirrors their mount state
A mob fighting the master resolves as the bot's dps target well before
the bot itself is in combat, but the master-follow branch decided mount
state only against the master. The bot rode past the fight to the
master's exact position and could oscillate between assisting and
re-mounting without attacking.

When an assist target exists, dismount at engage range of the target
(dismount distance + combat reach) and stop touching the mount state
while next to it, so the dps/tank assist trigger can take over. Also
drop the 18yd assist-range clamp again: dismount happens at the class
combat distance.
The mirror path had no distance condition, so a bot at 15yd mounted the
moment the master did. Now: mirror within 5yd, walk between 5 and the
mount threshold (21+ yd, where the cast time isn't worth it), and mount
to close distance beyond that. Shared proximity constant for the mirror
gate and the stay-mounted dismount range.
Replaces the hardcoded proximity constant with the existing (previously
unused) AiPlayerbot.TooCloseDistance option, default 5.
GetMaster() validated the master GUID via ObjectAccessor::FindConnectedPlayer
but then returned the cached raw pointer, which is exactly the stale pointer
the validation was meant to guard against: after a same-GUID relog the lookup
finds the new Player object yet the old (freed) pointer was handed back.
Return the resolved object instead - a logged-out master yields nullptr, a
relogged one yields the fresh Player.
…ting

Execute() already computes distToMaster once per tick; StayMountedToCloseDistance
and ShouldMountToCloseDistance each recomputed the same GetDistance2d, so a mounted
bot near a dismounted master ran the distance calc three times per tick. Thread the
value through - no behaviour change.
UpdateAIGroupMaster only cleared the raw master pointer when the master
resolved to nullptr. A same-GUID relog yields a fresh Player object, so
GetMaster() returns the new one while the cached raw pointer still points
at the destroyed one, and every raw master dereference further down the
function is a use-after-free until PlayerbotMgr happens to call SetMaster
again. Compare against the re-resolved master instead.
…ot only when near

The near-only mirror gate left a dead band between TooCloseDistance and
CalculateMountDistance() where neither the mirror branch nor the
mount-to-close branch fires: 5-21 yd for melee, and 5-38.5 yd for casters,
since CalculateMountDistance() is max(21, SpellDistance + 10) and
SpellDistance defaults to 28.5.

CalculateMountDistance() is the break-even for closing a FIXED gap, so it
is the wrong test against a master who is actively opening one. And a
ground mount matches the master's speed rather than beating it, so a bot
that waits for the gap to cross the threshold does not claw it back - it
holds that distance for the rest of the ride. A caster trailing 38 yd
behind is the visible symptom.

Mirror when near or when the master is moving. ShouldFollowMasterMountState
already requires the master mounted, the bot unmounted, no attackers and
the bot out of combat, so the added clause only fires for a safe bot on
foot behind a moving mounted master. A stationary master keeps the
deliberate near-only rule.
@icemansparks icemansparks changed the title Rebase mount behavior v2 onto merkerhood [DO NOT MERGE] Mount behavior v2 - testing branch, destined for upstream Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant