From b8ff5996f802de76608cb1ccfd09256b0595d5a6 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 8 May 2026 22:41:13 -0700 Subject: [PATCH] Flying mount fixes and self-bot (#2351) ## Pull Request Description This PR does a few things. 1. Enable Selfbots to mount up. Because they have masters, but are their own masters, they would never mount up because their master never mounted. 2. Fix flag state handling after processing the aura change. 3. Add in the Dismount packet handler. This is intended to implement fall animations and have bots touch the ground when dismounting instead of floating off the ground. (It was cleared anyway after the first move, but this should make it more seamless.) ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes self bot should mount up, and select area appropriate mounts. Bots in your team should mount up, and on your dismount properly snap to the ground. should test at low Z (<1.0 off the ground) and higher z (> 1.0 off the ground) ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [x] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) The processing of a fall path has some impact, but I dont think itll be too much. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Add natural falling when dismounting. May incurr fall damange.... - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Comparison of code bases, searching for flags, adding diagnostic logging, and processing of said logging. iterating and brainstorming. Code was also written, but fully reviewed by me, and fixed where appropriate. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Claude Opus 4.7 (1M context) --- src/Ai/Base/Actions/CheckMountStateAction.cpp | 94 +++++++++++++++---- src/Ai/Base/Actions/CheckMountStateAction.h | 2 + src/Bot/PlayerbotAI.cpp | 12 +++ 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/Ai/Base/Actions/CheckMountStateAction.cpp b/src/Ai/Base/Actions/CheckMountStateAction.cpp index 0d7fe4321..0e3abb724 100644 --- a/src/Ai/Base/Actions/CheckMountStateAction.cpp +++ b/src/Ai/Base/Actions/CheckMountStateAction.cpp @@ -4,9 +4,11 @@ */ #include "CheckMountStateAction.h" +#include "AreaDefines.h" #include "BattleGroundTactics.h" #include "BattlegroundEY.h" #include "BattlegroundWS.h" +#include "DBCStores.h" #include "Event.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" @@ -14,6 +16,8 @@ #include "ServerFacade.h" #include "SpellAuraEffects.h" +static constexpr uint32 SPELL_COLD_WEATHER_FLYING = 54197; + // Define the static map / init bool for caching bot preferred mount data globally std::unordered_map CheckMountStateAction::mountCache; bool CheckMountStateAction::preferredMountTableChecked = false; @@ -94,9 +98,10 @@ bool CheckMountStateAction::Execute(Event /*event*/) } bool inBattleground = bot->InBattleground(); + bool const noRealMaster = (!master || master == bot); // If there is a master and bot not in BG, follow master's mount state regardless of group leader - if (master && !inBattleground) + if (!noRealMaster && !inBattleground) { if (ShouldFollowMasterMountState(master, noAttackers, shouldMount)) return Mount(); @@ -110,8 +115,8 @@ bool CheckMountStateAction::Execute(Event /*event*/) return false; } - // If there is no master or bot in BG - if ((!master || inBattleground) && !bot->IsMounted() && + // No real master (random bot or self-bot) OR bot in BG + if ((noRealMaster || inBattleground) && !bot->IsMounted() && noAttackers && shouldMount && !bot->IsInCombat()) return Mount(); @@ -228,6 +233,39 @@ void CheckMountStateAction::Dismount() WorldPacket emptyPacket; bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + + bool const wantsFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); + bool const isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + bool const isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); + bool const hasGravityDisabled = bot->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + if (!wantsFly && !isWaterWalking && (isFlying || hasGravityDisabled)) + { + bot->RemoveUnitMovementFlag( + MOVEMENTFLAG_FLYING | MOVEMENTFLAG_CAN_FLY | MOVEMENTFLAG_DISABLE_GRAVITY); + if (!bot->IsRooted()) + bot->SendMovementFlagUpdate(); + } +} + +void CheckMountStateAction::CompleteDismount(Player* bot) +{ + if (!bot || !bot->IsInWorld()) + return; + + float const x = bot->GetPositionX(); + float const y = bot->GetPositionY(); + float const startZ = bot->GetPositionZ(); + + float groundZ = startZ; + bot->UpdateAllowedPositionZ(x, y, groundZ); + + bot->GetMotionMaster()->MoveFall(); + MovementInfo fallInfo = bot->m_movementInfo; + // Need to set the start of the fall, otherwise the fall may start from too high of a Z and kill the bot. + bot->SetFallInformation(0, startZ); + fallInfo.pos.Relocate(x, y, groundZ); + bot->HandleFall(fallInfo); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR); } bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int32 masterSpeed) const @@ -434,6 +472,24 @@ bool CheckMountStateAction::ShouldDismountForMaster(Player* master) const return !isMasterMounted && bot->IsMounted(); } +static bool BotCanUseFlyingMount(Player const* bot) +{ + if (bot->GetPureSkillValue(SKILL_RIDING) < 225) + return false; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetAreaId()); + if (!area || !area->IsFlyable()) + return false; + if (area->flags & AREA_FLAG_NO_FLY_ZONE) + return false; + + uint32 const vmap = GetVirtualMapForMapAndZone(bot->GetMapId(), bot->GetZoneId()); + if (vmap == MAP_NORTHREND && !bot->HasSpell(SPELL_COLD_WEATHER_FLYING)) + return false; + + return true; +} + int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const MountData& mountData) const { // Check riding skill and level requirements @@ -443,8 +499,10 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou if (ridingSkill <= 75 && botLevel < static_cast(sPlayerbotAIConfig.useFastGroundMountAtMinLevel)) return 59; - // If there is a master and bot not in BG, use master's aura effects. - if (master && !bot->InBattleground()) + // check if bot has master and if master is self + bool const noRealMaster = (!master || master == bot); + + if (!noRealMaster && !bot->InBattleground()) { auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED); if (!auraEffects.empty()) @@ -458,27 +516,27 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou return 279; else if (masterInShapeshiftForm == FORM_FLIGHT) return 149; - } - else - { - // Bots on their own. - int32 speed = mountData.maxSpeed; - if (bot->InBattleground() && speed > 99) - return 99; - - return speed; + return 59; // walk pace } - return 59; + // No real master OR battleground: pick speed by skill tier. + if (!bot->InBattleground() && BotCanUseFlyingMount(bot)) + return (ridingSkill >= 300) ? 279 : 149; + + int32 maxGround = (ridingSkill >= 150) ? 99 : 59; + if (bot->InBattleground() && maxGround > 99) + maxGround = 99; + return maxGround; } uint32 CheckMountStateAction::GetMountType(Player* master) const { - if (!master) - return 0; + bool const noRealMaster = (!master || master == bot); + + if (noRealMaster) + return (!bot->InBattleground() && BotCanUseFlyingMount(bot)) ? 1 : 0; auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED); - if (!auraEffects.empty()) { SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo(); diff --git a/src/Ai/Base/Actions/CheckMountStateAction.h b/src/Ai/Base/Actions/CheckMountStateAction.h index 9a21838e1..d1faa3798 100644 --- a/src/Ai/Base/Actions/CheckMountStateAction.h +++ b/src/Ai/Base/Actions/CheckMountStateAction.h @@ -42,6 +42,8 @@ public: bool isPossible() override { return true; } bool Mount(); + static void CompleteDismount(Player* bot); + private: Player* master; ShapeshiftForm masterInShapeshiftForm; diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index b2631f7d1..8b20b2e88 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -15,6 +15,7 @@ #include "ChannelMgr.h" #include "CharacterPackets.h" #include "ChatHelper.h" +#include "CheckMountStateAction.h" #include "Common.h" #include "CreatureData.h" #include "EmoteAction.h" @@ -1365,6 +1366,17 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) // */ return; } + case SMSG_DISMOUNT: + { + WorldPacket p(packet); + p.rpos(0); + ObjectGuid guid; + p >> guid.ReadAsPacked(); + if (guid != bot->GetGUID()) + return; + CheckMountStateAction::CompleteDismount(bot); + return; + } default: botOutgoingPacketHandlers.AddPacket(packet); }