Compare commits

...

7 Commits

Author SHA1 Message Date
bash
6620def962 auto-format 2025-11-24 11:59:06 +01:00
bash
cddd406b1c removed movementAction changed made due core movement changes 2025-11-24 11:55:03 +01:00
Crow
cf743a186a Fix Wrong Misdirection Spell ID for Gruul's Lair and Magtheridon Strategies (#1867)
Lol oops.

Confirmed with logs/in-game that the prior one was wrong (and thus
always returning false) and current one is correct.
2025-11-23 10:06:19 +01:00
blinkysc
10213d8381 Add thread safety for group operations (#1816)
Fixes crashes and race conditions when bots perform group/guild/arena
operations by moving thread-unsafe code to world thread.

Potentially fixes #1124

## Changes

- Added operation queue system that runs in world thread
- Group operations (invite, remove, convert to raid, set leader) now
queued
- Arena formation refactored to use queue
- Guild operations changed to use packet queueing

## Testing

Set `MapUpdate.Threads` > 1 in worldserver.conf to enable multiple map
threads, then test:
- Group formation and disbanding
- Arena team formation
- Guild operations (invite, promote, demote, remove)

- Run with TSAN
cmake ../ \
  -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \
  -DCMAKE_INSTALL_PREFIX=/path/to/install \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo

build

export
TSAN_OPTIONS="log_path=tsan_report:halt_on_error=0:second_deadlock_stack=1"
./worldserver

The crashes/race conditions should no longer occur with concurrent map
threads.

## New Files

- `PlayerbotOperation.h` - Base class defining the operation interface
(Execute, IsValid, GetPriority)
- `PlayerbotOperations.h` - Concrete implementations:
GroupInviteOperation, GroupRemoveMemberOperation,
GroupConvertToRaidOperation, GroupSetLeaderOperation,
ArenaGroupFormationOperation
- `PlayerbotWorldThreadProcessor.h/cpp` - Singleton processor with
mutex-protected queue, processes operations in WorldScript::OnUpdate
hook, handles batch processing and validation

---------

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: SaW <swerkhoven@outlook.com>
Co-authored-by: bash <hermensb@gmail.com>
2025-11-21 21:55:55 +01:00
bashermens
d97870facd fix: warning updating movement flags while rooted (#1858)
fixes https://github.com/mod-playerbots/mod-playerbots/issues/1854
-----
Also includes fixes for:
-----
* Bots swimming with waterWalk kept switching between swimming and
walking, as result jittering effect swimming under water when water
walking active
* Bots flying close above water they would land on water and start
walking, now they stay flying unless on solid ground they will land and
start walking by design

-----
Moved all flag setting to updateMovementState:
* So all movement flag are handled in updateMovementState which also
contains the restricted movement logic.
* Handle restricted movement logic and preventing SendMovementFlagUpdate
while being restricted.


-----
Known issue when flying the following bots feel a bit jittering, wont
touch for now at least till core movement changes quirks has been dealt
with.

The current code is the extended version of what is originally was
before core merge with refactored movements. Once the core movement
refactors are settled a bit more i would like to revisit this code; as i
would expect more imperative code and less manual flag setting e.g.
bot->SetWaterWalking, SetGravitiy..SetCanFly etc.
2025-11-21 20:45:23 +01:00
Alex Dcnh
0c1700c117 CORE - Improved language detection for bots (#1784)
I've had this problem for a long time, my bots only speak English even
though I'm playing on a French client.
I suppose this must be the case for some other people who do not have a
large number of players with the same local client.
If we use French DBCs, the bots bug because they only recognize US DBCs.
From what I understand, the language is chosen as follows:
On load, the module reads the entire `ai_playerbot_texts` table and
stores each text variant in a dictionary indexed by the locale ID: the
`text` column remains the default value (English), and the `text_loc1`
to `text_loc8` columns fill slots 1 through 8.

Whenever a real player connects, the module increments a counter for
that player's DBC locale using
`AddLocalePriority(player->GetSession()->GetSessionDbcLocale())`.

When a bot needs a text, `GetLocalePriority()` returns the most
frequently used locale index among currently connected players. The
corresponding string is then retrieved. if the box is empty, we fall
back to the English version (text[0]).

### This PR improve language detection.
**Summary**

- log both the client DBC locale and the account database locale when a
player logs in
- fall back to the account locale when the client reports enUS but the
account is configured for another locale
- keep the existing vote-based selection so bots always speak the
majority language among connected players

**Therefore, the original behavior is maintained. Bots still choose the
most represented language among connected players (the counter is simply
more efficient by prioritizing the account's locale when it differs from
the client's English). For example, if more English-speaking players are
connected, the language will revert to English, as the bots always share
the majority locale.**
2025-11-21 15:56:03 +01:00
Alex Dcnh
0b1b0eaecc Core - Fix RTSC SeeSpellAction crash on malformed WorldPacket (#1841)
## Summary

This PR fixes the Crash 1 Source from Issue
[#1840](https://github.com/mod-playerbots/mod-playerbots/issues/1840)
posted in a @Regrad posted logs, in `SeeSpellAction::Execute` when an
RTSC "see spell" event arrives with an empty or malformed `WorldPacket`.
In that case the code used to read from the packet without any
validation, causing a `ByteBufferException` and a crash in the map
thread.

## Fix

- Reset the packet read position and check that the RTSC header
(castCount + spellId + castFlags) fits into the packet before reading.
- Wrap `SpellCastTargets::Read` in a `try { } catch (ByteBufferException
const&) { }` block so truncated RTSC payloads are handled gracefully.
- Check that `targets.GetDst()` is not `nullptr` before accessing its
position.

For valid RTSC packets the behavior is unchanged; malformed packets are
now safely ignored instead of crashing the server.

## Testing

- Sent bots to multiple locations using RTSC and verified they still
move as before.
- Reproduced the previous crash scenario with malformed RTSC packets:
the worldserver no longer crashes and the event is simply ignored.

---------

Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2025-11-21 13:18:14 +01:00
16 changed files with 1153 additions and 203 deletions

View File

@@ -725,8 +725,8 @@ std::string const PlayerbotAIConfig::GetTimestampStr()
// HH hour (2 digits 00-23)
// MM minutes (2 digits 00-59)
// SS seconds (2 digits 00-59)
char buf[20];
snprintf(buf, 20, "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour,
char buf[32];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour,
aTm->tm_min, aTm->tm_sec);
return std::string(buf);
}

View File

@@ -27,7 +27,9 @@
#include "PlayerbotAIConfig.h"
#include "PlayerbotDbStore.h"
#include "PlayerbotFactory.h"
#include "PlayerbotOperations.h"
#include "PlayerbotSecurity.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "Playerbots.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
@@ -85,7 +87,6 @@ public:
void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId)
{
// bot is loading
if (botLoading.find(playerGuid) != botLoading.end())
return;
@@ -195,7 +196,9 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con
}
sRandomPlayerbotMgr->OnPlayerLogin(bot);
OnBotLogin(bot);
auto op = std::make_unique<OnBotLoginOperation>(bot->GetGUID(), this);
sPlayerbotWorldProcessor->QueueOperation(std::move(op));
botLoading.erase(holder.GetGuid());
}
@@ -316,11 +319,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid)
if (!botAI)
return;
Group* group = bot->GetGroup();
if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster())
{
sPlayerbotDbStore->Save(botAI);
}
// Queue group cleanup operation for world thread
auto cleanupOp = std::make_unique<BotLogoutGroupCleanupOperation>(guid);
sPlayerbotWorldProcessor->QueueOperation(std::move(cleanupOp));
LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str());
bot->SaveToDB(false, false);
@@ -549,6 +550,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot)
botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK);
// Queue group operations for world thread
if (master && master->GetGroup() && !group)
{
Group* mgroup = master->GetGroup();
@@ -556,24 +558,29 @@ void PlayerbotHolder::OnBotLogin(Player* const bot)
{
if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup())
{
mgroup->ConvertToRaid();
// Queue ConvertToRaid operation
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(master->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
if (mgroup->isRaidGroup())
{
mgroup->AddMember(bot);
// Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(addOp));
}
}
else
{
mgroup->AddMember(bot);
// Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(addOp));
}
}
else if (master && !group)
{
Group* newGroup = new Group();
newGroup->Create(master);
sGroupMgr->AddGroup(newGroup);
newGroup->AddMember(bot);
// Queue group creation and AddMember operation
auto inviteOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(inviteOp));
}
// if (master)
// {
@@ -1602,8 +1609,26 @@ void PlayerbotMgr::OnBotLoginInternal(Player* const bot)
void PlayerbotMgr::OnPlayerLogin(Player* player)
{
if (!player)
return;
WorldSession* session = player->GetSession();
if (!session)
{
LOG_WARN("playerbots", "Unable to register locale priority for player {} because the session is missing", player->GetName());
return;
}
// DB locale (source of bot text translation)
LocaleConstant const databaseLocale = session->GetSessionDbLocaleIndex();
// For bot texts (DB-driven), prefer the database locale with a safe fallback.
LocaleConstant usedLocale = databaseLocale;
if (usedLocale >= MAX_LOCALES)
usedLocale = LOCALE_enUS; // fallback
// set locale priority for bot texts
sPlayerbotTextMgr->AddLocalePriority(player->GetSession()->GetSessionDbcLocale());
sPlayerbotTextMgr->AddLocalePriority(usedLocale);
if (sPlayerbotAIConfig->selfBotLevel > 2)
HandlePlayerbotCommand("self", player);
@@ -1611,7 +1636,7 @@ void PlayerbotMgr::OnPlayerLogin(Player* player)
if (!sPlayerbotAIConfig->botAutologin)
return;
uint32 accountId = player->GetSession()->GetAccountId();
uint32 accountId = session->GetAccountId();
QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId);
if (results)
{

93
src/PlayerbotOperation.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_OPERATION_H
#define _PLAYERBOT_OPERATION_H
#include "Common.h"
#include "ObjectGuid.h"
#include <memory>
/**
* @brief Base class for thread-unsafe operations that must be executed in the world thread
*
* PlayerbotOperation represents an operation that needs to be deferred from a map thread
* to the world thread for safe execution. Examples include group modifications, LFG operations,
* guild operations, etc.
*
* Thread Safety:
* - The constructor and data members must be thread-safe (use copies, not pointers)
* - Execute() is called in the world thread and can safely perform thread-unsafe operations
* - Subclasses must not store raw pointers to (core/world thread) game object (use ObjectGuid instead)
*/
class PlayerbotOperation
{
public:
virtual ~PlayerbotOperation() = default;
/**
* @brief Execute this operation in the world thread
*
* This method is called by PlayerbotWorldThreadProcessor::Update() which runs in the world thread.
* It's safe to perform any thread-unsafe operation here (Group, LFG, Guild, etc.)
*
* @return true if operation succeeded, false if it failed
*/
virtual bool Execute() = 0;
/**
* @brief Get the bot GUID this operation is for (optional)
*
* Used for logging and debugging purposes.
*
* @return ObjectGuid of the bot, or ObjectGuid::Empty if not applicable
*/
virtual ObjectGuid GetBotGuid() const { return ObjectGuid::Empty; }
/**
* @brief Get the operation priority (higher = more urgent)
*
* Priority levels:
* - 100: Critical (crash prevention, cleanup operations)
* - 50: High (player-facing operations like group invites)
* - 10: Normal (background operations)
* - 0: Low (statistics, logging)
*
* @return Priority value (0-100)
*/
virtual uint32 GetPriority() const { return 10; }
/**
* @brief Get a human-readable name for this operation
*
* Used for logging and debugging.
*
* @return Operation name
*/
virtual std::string GetName() const { return "Unknown Operation"; }
/**
* @brief Check if this operation is still valid
*
* Called before Execute() to check if the operation should still be executed.
* For example, if a bot logged out, group invite operations for that bot can be skipped.
*
* @return true if operation should be executed, false to skip
*/
virtual bool IsValid() const { return true; }
};
/**
* @brief Comparison operator for priority queue (higher priority first)
*/
struct PlayerbotOperationComparator
{
bool operator()(const std::unique_ptr<PlayerbotOperation>& a, const std::unique_ptr<PlayerbotOperation>& b) const
{
return a->GetPriority() < b->GetPriority(); // Lower priority goes to back of queue
}
};
#endif

500
src/PlayerbotOperations.h Normal file
View File

@@ -0,0 +1,500 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_OPERATIONS_H
#define _PLAYERBOT_OPERATIONS_H
#include "Group.h"
#include "GroupMgr.h"
#include "GuildMgr.h"
#include "ObjectAccessor.h"
#include "PlayerbotOperation.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotMgr.h"
#include "PlayerbotDbStore.h"
#include "RandomPlayerbotMgr.h"
// Group invite operation
class GroupInviteOperation : public PlayerbotOperation
{
public:
GroupInviteOperation(ObjectGuid botGuid, ObjectGuid targetGuid)
: m_botGuid(botGuid), m_targetGuid(targetGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
if (!bot || !target)
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Bot or target not found");
return false;
}
// Check if target is already in a group
if (target->GetGroup())
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Target {} is already in a group", target->GetName());
return false;
}
Group* group = bot->GetGroup();
// Create group if bot doesn't have one
if (!group)
{
group = new Group;
if (!group->Create(bot))
{
delete group;
LOG_ERROR("playerbots", "GroupInviteOperation: Failed to create group for bot {}", bot->GetName());
return false;
}
sGroupMgr->AddGroup(group);
LOG_DEBUG("playerbots", "GroupInviteOperation: Created new group for bot {}", bot->GetName());
}
// Convert to raid if needed (more than 5 members)
if (!group->isRaidGroup() && group->GetMembersCount() >= 5)
{
group->ConvertToRaid();
LOG_DEBUG("playerbots", "GroupInviteOperation: Converted group to raid");
}
// Add member to group
if (group->AddMember(target))
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName());
return true;
}
else
{
LOG_ERROR("playerbots", "GroupInviteOperation: Failed to add {} to group", target->GetName());
return false;
}
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; } // High priority (player-facing)
std::string GetName() const override { return "GroupInvite"; }
bool IsValid() const override
{
// Check if bot still exists and is online
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
return bot && target;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_targetGuid;
};
// Remove member from group
class GroupRemoveMemberOperation : public PlayerbotOperation
{
public:
GroupRemoveMemberOperation(ObjectGuid botGuid, ObjectGuid targetGuid)
: m_botGuid(botGuid), m_targetGuid(targetGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
if (!bot || !target)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Bot is not in a group");
return false;
}
if (!group->IsMember(target->GetGUID()))
{
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Target is not in bot's group");
return false;
}
group->RemoveMember(target->GetGUID());
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Removed {} from group", target->GetName());
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupRemoveMember"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_targetGuid;
};
// Convert group to raid
class GroupConvertToRaidOperation : public PlayerbotOperation
{
public:
GroupConvertToRaidOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
if (!bot)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Bot is not in a group");
return false;
}
if (group->isRaidGroup())
{
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Group is already a raid");
return true; // Success - already in desired state
}
group->ConvertToRaid();
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Converted group to raid");
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupConvertToRaid"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
};
// Set group leader
class GroupSetLeaderOperation : public PlayerbotOperation
{
public:
GroupSetLeaderOperation(ObjectGuid botGuid, ObjectGuid newLeaderGuid)
: m_botGuid(botGuid), m_newLeaderGuid(newLeaderGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid);
if (!bot || !newLeader)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Bot is not in a group");
return false;
}
if (!group->IsMember(newLeader->GetGUID()))
{
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: New leader is not in the group");
return false;
}
group->ChangeLeader(newLeader->GetGUID());
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Changed leader to {}", newLeader->GetName());
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupSetLeader"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid);
return bot && newLeader;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_newLeaderGuid;
};
// Form arena group
class ArenaGroupFormationOperation : public PlayerbotOperation
{
public:
ArenaGroupFormationOperation(ObjectGuid leaderGuid, std::vector<ObjectGuid> memberGuids,
uint32 requiredSize, uint32 arenaTeamId, std::string arenaTeamName)
: m_leaderGuid(leaderGuid), m_memberGuids(memberGuids),
m_requiredSize(requiredSize), m_arenaTeamId(arenaTeamId), m_arenaTeamName(arenaTeamName)
{
}
bool Execute() override
{
Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid);
if (!leader)
{
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Leader not found");
return false;
}
// Step 1: Remove all members from their existing groups
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member)
continue;
Group* memberGroup = member->GetGroup();
if (memberGroup)
{
memberGroup->RemoveMember(memberGuid);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Removed {} from their existing group",
member->GetName());
}
}
// Step 2: Disband leader's existing group
Group* leaderGroup = leader->GetGroup();
if (leaderGroup)
{
leaderGroup->Disband(true);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Disbanded leader's existing group");
}
// Step 3: Create new group with leader
Group* newGroup = new Group();
if (!newGroup->Create(leader))
{
delete newGroup;
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to create arena group for leader {}",
leader->GetName());
return false;
}
sGroupMgr->AddGroup(newGroup);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Created new arena group with leader {}",
leader->GetName());
// Step 4: Add members to the new group
uint32 addedMembers = 0;
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member)
{
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} not found, skipping",
memberGuid.ToString());
continue;
}
if (member->GetLevel() < 70)
{
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} is below level 70, skipping",
member->GetName());
continue;
}
if (newGroup->AddMember(member))
{
addedMembers++;
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Added {} to arena group",
member->GetName());
}
else
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to add {} to arena group",
member->GetName());
}
if (addedMembers == 0)
{
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: No members were added to the arena group");
newGroup->Disband();
return false;
}
// Step 5: Teleport members to leader and reset AI
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member || !newGroup->IsMember(memberGuid))
continue;
PlayerbotAI* memberBotAI = sPlayerbotsMgr->GetPlayerbotAI(member);
if (memberBotAI)
memberBotAI->Reset();
member->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
member->TeleportTo(leader->GetMapId(), leader->GetPositionX(), leader->GetPositionY(),
leader->GetPositionZ(), 0);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Teleported {} to leader", member->GetName());
}
// Check if we have enough members
if (newGroup->GetMembersCount() < m_requiredSize)
{
LOG_INFO("playerbots", "Team #{} <{}> Group is not ready for match (not enough members: {}/{})",
m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount(), m_requiredSize);
newGroup->Disband();
return false;
}
LOG_INFO("playerbots", "Team #{} <{}> Group is ready for match with {} members",
m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount());
return true;
}
ObjectGuid GetBotGuid() const override { return m_leaderGuid; }
uint32 GetPriority() const override { return 60; } // Very high priority (arena/BG operations)
std::string GetName() const override { return "ArenaGroupFormation"; }
bool IsValid() const override
{
Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid);
return leader != nullptr;
}
private:
ObjectGuid m_leaderGuid;
std::vector<ObjectGuid> m_memberGuids;
uint32 m_requiredSize;
uint32 m_arenaTeamId;
std::string m_arenaTeamName;
};
// Bot logout group cleanup operation
class BotLogoutGroupCleanupOperation : public PlayerbotOperation
{
public:
BotLogoutGroupCleanupOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
if (!bot)
return false;
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
if (!botAI)
return false;
Group* group = bot->GetGroup();
if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster())
sPlayerbotDbStore->Save(botAI);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 70; }
std::string GetName() const override { return "BotLogoutGroupCleanup"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
};
// Add player bot operation (for logging in bots from map threads)
class AddPlayerBotOperation : public PlayerbotOperation
{
public:
AddPlayerBotOperation(ObjectGuid botGuid, uint32 masterAccountId)
: m_botGuid(botGuid), m_masterAccountId(masterAccountId)
{
}
bool Execute() override
{
sRandomPlayerbotMgr->AddPlayerBot(m_botGuid, m_masterAccountId);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; } // High priority
std::string GetName() const override { return "AddPlayerBot"; }
bool IsValid() const override
{
return !ObjectAccessor::FindConnectedPlayer(m_botGuid);
}
private:
ObjectGuid m_botGuid;
uint32 m_masterAccountId;
};
class OnBotLoginOperation : public PlayerbotOperation
{
public:
OnBotLoginOperation(ObjectGuid botGuid, PlayerbotHolder* holder)
: m_botGuid(botGuid), m_holder(holder)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindConnectedPlayer(m_botGuid);
if (!bot || !m_holder)
return false;
m_holder->OnBotLogin(bot);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 100; }
std::string GetName() const override { return "OnBotLogin"; }
bool IsValid() const override
{
return ObjectAccessor::FindConnectedPlayer(m_botGuid) != nullptr;
}
private:
ObjectGuid m_botGuid;
PlayerbotHolder* m_holder;
};
#endif

View File

@@ -190,26 +190,29 @@ bool PlayerbotTextMgr::GetBotText(std::string name, std::string& text, std::map<
void PlayerbotTextMgr::AddLocalePriority(uint32 locale)
{
if (!locale)
if (locale >= MAX_LOCALES)
{
LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds MAX_LOCALES ({})", locale, MAX_LOCALES - 1);
return;
}
botTextLocalePriority[locale]++;
}
uint32 PlayerbotTextMgr::GetLocalePriority()
{
uint32 topLocale = 0;
// if no real players online, reset top locale
if (!sWorldSessionMgr->GetActiveSessionCount())
uint32 const activeSessions = sWorldSessionMgr->GetActiveSessionCount();
if (!activeSessions)
{
ResetLocalePriority();
return 0;
}
uint32 topLocale = 0;
for (uint8 i = 0; i < MAX_LOCALES; ++i)
{
if (botTextLocalePriority[i] > topLocale)
if (botTextLocalePriority[i] > botTextLocalePriority[topLocale])
topLocale = i;
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "PlayerbotWorldThreadProcessor.h"
#include "Log.h"
#include "PlayerbotAIConfig.h"
#include <algorithm>
PlayerbotWorldThreadProcessor::PlayerbotWorldThreadProcessor()
: m_enabled(true), m_maxQueueSize(10000), m_batchSize(100), m_queueWarningThreshold(80),
m_timeSinceLastUpdate(0), m_updateInterval(50) // Process at least every 50ms
{
LOG_INFO("playerbots", "PlayerbotWorldThreadProcessor initialized");
}
PlayerbotWorldThreadProcessor::~PlayerbotWorldThreadProcessor() { ClearQueue(); }
PlayerbotWorldThreadProcessor* PlayerbotWorldThreadProcessor::instance()
{
static PlayerbotWorldThreadProcessor instance;
return &instance;
}
void PlayerbotWorldThreadProcessor::Update(uint32 diff)
{
if (!m_enabled)
return;
// Accumulate time
m_timeSinceLastUpdate += diff;
// Don't process too frequently to reduce overhead
if (m_timeSinceLastUpdate < m_updateInterval)
return;
m_timeSinceLastUpdate = 0;
// Check queue health (warn if getting full)
CheckQueueHealth();
// Process a batch of operations
ProcessBatch();
}
bool PlayerbotWorldThreadProcessor::QueueOperation(std::unique_ptr<PlayerbotOperation> operation)
{
if (!operation)
{
LOG_ERROR("playerbots", "Attempted to queue null operation");
return false;
}
std::lock_guard<std::mutex> lock(m_queueMutex);
// Check if queue is full
if (m_operationQueue.size() >= m_maxQueueSize)
{
LOG_ERROR("playerbots",
"PlayerbotWorldThreadProcessor queue is full ({} operations). Dropping operation: {}",
m_maxQueueSize, operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsSkipped++;
return false;
}
// Queue the operation
m_operationQueue.push(std::move(operation));
// Update statistics
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = static_cast<uint32>(m_operationQueue.size());
m_stats.maxQueueSize = std::max(m_stats.maxQueueSize, m_stats.currentQueueSize);
}
return true;
}
void PlayerbotWorldThreadProcessor::ProcessBatch()
{
// Extract a batch of operations from the queue
std::vector<std::unique_ptr<PlayerbotOperation>> batch;
batch.reserve(m_batchSize);
{
std::lock_guard<std::mutex> lock(m_queueMutex);
// Extract up to batchSize operations
while (!m_operationQueue.empty() && batch.size() < m_batchSize)
{
batch.push_back(std::move(m_operationQueue.front()));
m_operationQueue.pop();
}
// Update current queue size stat
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = static_cast<uint32>(m_operationQueue.size());
}
// Execute operations outside of lock to avoid blocking queue
uint32 totalExecutionTime = 0;
for (auto& operation : batch)
{
if (!operation)
continue;
try
{
// Check if operation is still valid
if (!operation->IsValid())
{
LOG_DEBUG("playerbots", "Skipping invalid operation: {}", operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsSkipped++;
continue;
}
// Time the execution
uint32 startTime = getMSTime();
// Execute the operation
bool success = operation->Execute();
uint32 executionTime = GetMSTimeDiffToNow(startTime);
totalExecutionTime += executionTime;
// Log slow operations
if (executionTime > 100)
LOG_WARN("playerbots", "Slow operation: {} took {}ms", operation->GetName(), executionTime);
// Update statistics
std::lock_guard<std::mutex> statsLock(m_statsMutex);
if (success)
m_stats.totalOperationsProcessed++;
else
{
m_stats.totalOperationsFailed++;
LOG_DEBUG("playerbots", "Operation failed: {}", operation->GetName());
}
}
catch (std::exception const& e)
{
LOG_ERROR("playerbots", "Exception in operation {}: {}", operation->GetName(), e.what());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsFailed++;
}
catch (...)
{
LOG_ERROR("playerbots", "Unknown exception in operation {}", operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsFailed++;
}
}
// Update average execution time
if (!batch.empty())
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
uint32 avgTime = totalExecutionTime / static_cast<uint32>(batch.size());
// Exponential moving average
m_stats.averageExecutionTimeMs =
(m_stats.averageExecutionTimeMs * 9 + avgTime) / 10; // 90% old, 10% new
}
}
void PlayerbotWorldThreadProcessor::CheckQueueHealth()
{
uint32 queueSize = GetQueueSize();
uint32 threshold = (m_maxQueueSize * m_queueWarningThreshold) / 100;
if (queueSize >= threshold)
{
LOG_WARN("playerbots",
"PlayerbotWorldThreadProcessor queue is {}% full ({}/{}). "
"Consider increasing update frequency or batch size.",
(queueSize * 100) / m_maxQueueSize, queueSize, m_maxQueueSize);
}
}
uint32 PlayerbotWorldThreadProcessor::GetQueueSize() const
{
std::lock_guard<std::mutex> lock(m_queueMutex);
return static_cast<uint32>(m_operationQueue.size());
}
void PlayerbotWorldThreadProcessor::ClearQueue()
{
std::lock_guard<std::mutex> lock(m_queueMutex);
uint32 cleared = static_cast<uint32>(m_operationQueue.size());
if (cleared > 0)
LOG_INFO("playerbots", "Clearing {} queued operations", cleared);
// Clear the queue
while (!m_operationQueue.empty())
{
m_operationQueue.pop();
}
// Reset queue size stat
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = 0;
}
PlayerbotWorldThreadProcessor::Statistics PlayerbotWorldThreadProcessor::GetStatistics() const
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
return m_stats; // Return a copy
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_WORLD_THREAD_PROCESSOR_H
#define _PLAYERBOT_WORLD_THREAD_PROCESSOR_H
#include "Common.h"
#include "PlayerbotOperation.h"
#include <memory>
#include <mutex>
#include <queue>
/**
* @brief Processes thread-unsafe bot operations in the world thread
*
* The PlayerbotWorldThreadProcessor manages a queue of operations that must be executed
* in the world thread rather than map threads. This ensures thread safety for operations
* like group modifications, LFG, guilds, battlegrounds, etc.
*
* Architecture:
* - Map threads queue operations via QueueOperation()
* - World thread processes operations via Update() (called from WorldScript::OnUpdate)
* - Operations are processed in priority order
* - Thread-safe queue protected by mutex
*
* Usage:
* auto op = std::make_unique<MyOperation>(botGuid, params);
* sPlayerbotWorldProcessor->QueueOperation(std::move(op));
*/
class PlayerbotWorldThreadProcessor
{
public:
PlayerbotWorldThreadProcessor();
~PlayerbotWorldThreadProcessor();
static PlayerbotWorldThreadProcessor* instance();
/**
* @brief Update and process queued operations (called from world thread)
*
* This method should be called from WorldScript::OnUpdate hook, which runs in the world thread.
* It processes a batch of queued operations.
*
* @param diff Time since last update in milliseconds
*/
void Update(uint32 diff);
/**
* @brief Queue an operation for execution in the world thread
*
* Thread-safe method that can be called from any thread (typically map threads).
* The operation will be executed later during Update().
*
* @param operation Unique pointer to the operation (ownership is transferred)
* @return true if operation was queued, false if queue is full
*/
bool QueueOperation(std::unique_ptr<PlayerbotOperation> operation);
/**
* @brief Get current queue size
*
* Thread-safe method for monitoring queue size.
*
* @return Number of operations waiting to be processed
*/
uint32 GetQueueSize() const;
/**
* @brief Clear all queued operations
*
* Used during shutdown or emergency situations.
*/
void ClearQueue();
/**
* @brief Get statistics about operation processing
*/
struct Statistics
{
uint64 totalOperationsProcessed = 0;
uint64 totalOperationsFailed = 0;
uint64 totalOperationsSkipped = 0;
uint32 currentQueueSize = 0;
uint32 maxQueueSize = 0;
uint32 averageExecutionTimeMs = 0;
};
Statistics GetStatistics() const;
/**
* @brief Enable/disable operation processing
*
* When disabled, operations are still queued but not processed.
* Useful for testing or temporary suspension.
*
* @param enabled true to enable processing, false to disable
*/
void SetEnabled(bool enabled) { m_enabled = enabled; }
bool IsEnabled() const { return m_enabled; }
private:
/**
* @brief Process a single batch of operations
*
* Extracts operations from queue and executes them.
* Called internally by Update().
*/
void ProcessBatch();
/**
* @brief Check if queue is approaching capacity
*
* Logs warning if queue is getting full.
*/
void CheckQueueHealth();
// Thread-safe queue
mutable std::mutex m_queueMutex;
std::queue<std::unique_ptr<PlayerbotOperation>> m_operationQueue;
// Configuration
bool m_enabled;
uint32 m_maxQueueSize; // Maximum operations in queue
uint32 m_batchSize; // Operations to process per Update()
uint32 m_queueWarningThreshold; // Warn when queue reaches this percentage
// Statistics
mutable std::mutex m_statsMutex;
Statistics m_stats;
// Timing
uint32 m_timeSinceLastUpdate;
uint32 m_updateInterval; // Minimum ms between updates
};
#define sPlayerbotWorldProcessor PlayerbotWorldThreadProcessor::instance()
#endif

View File

@@ -25,6 +25,7 @@
#include "Metric.h"
#include "PlayerScript.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h"
#include "cs_playerbots.h"
@@ -300,7 +301,8 @@ class PlayerbotsWorldScript : public WorldScript
{
public:
PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", {
WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED
WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED,
WORLDHOOK_ON_UPDATE
}) {}
void OnBeforeWorldInitialized() override
@@ -329,6 +331,13 @@ public:
LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
LOG_INFO("server.loading", "Playerbots World Thread Processor initialized");
}
void OnUpdate(uint32 diff) override
{
sPlayerbotWorldProcessor->Update(diff);
sRandomPlayerbotMgr->UpdateAI(diff); // World thread only
}
};
@@ -390,8 +399,7 @@ public:
void OnPlayerbotUpdate(uint32 diff) override
{
sRandomPlayerbotMgr->UpdateAI(diff);
sRandomPlayerbotMgr->UpdateSessions();
sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only
}
void OnPlayerbotUpdateSessions(Player* player) override

View File

@@ -58,6 +58,14 @@ Player* GuidManageAction::GetPlayer(Event event)
return nullptr;
}
void GuidManageAction::SendPacket(WorldPacket const& packet)
{
// make a heap copy because QueuePacket takes ownership
WorldPacket* data = new WorldPacket(packet);
bot->GetSession()->QueuePacket(data);
}
bool GuidManageAction::Execute(Event event)
{
Player* player = GetPlayer(event);
@@ -84,12 +92,6 @@ bool GuildInviteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_INVITE);
}
void GuildInviteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildInviteByName data = WorldPacket(packet);
bot->GetSession()->HandleGuildInviteOpcode(data);
}
bool GuildInviteAction::PlayerIsValid(Player* member)
{
return !member->GetGuildId() && (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) ||
@@ -101,12 +103,6 @@ bool GuildPromoteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_PROMOTE);
}
void GuildPromoteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildPromoteMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildPromoteOpcode(data);
}
bool GuildPromoteAction::PlayerIsValid(Player* member)
{
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member) - 1;
@@ -117,12 +113,6 @@ bool GuildDemoteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_DEMOTE);
}
void GuildDemoteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildDemoteMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildDemoteOpcode(data);
}
bool GuildDemoteAction::PlayerIsValid(Player* member)
{
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member);
@@ -133,12 +123,6 @@ bool GuildRemoveAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_REMOVE);
}
void GuildRemoveAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildOfficerRemoveMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildRemoveOpcode(data);
}
bool GuildRemoveAction::PlayerIsValid(Player* member)
{
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member);

View File

@@ -25,7 +25,7 @@ public:
bool isUseful() override { return false; }
protected:
virtual void SendPacket(WorldPacket data){};
virtual void SendPacket(WorldPacket const& packet);
virtual Player* GetPlayer(Event event);
virtual bool PlayerIsValid(Player* member);
virtual uint8 GetRankId(Player* member);
@@ -44,7 +44,6 @@ public:
bool isUseful() override;
protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override;
};
@@ -59,7 +58,6 @@ public:
bool isUseful() override;
protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override;
};
@@ -74,7 +72,6 @@ public:
bool isUseful() override;
protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override;
};
@@ -89,7 +86,6 @@ public:
bool isUseful() override;
protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override;
};

View File

@@ -9,7 +9,9 @@
#include "Event.h"
#include "GuildMgr.h"
#include "Log.h"
#include "PlayerbotOperations.h"
#include "Playerbots.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "ServerFacade.h"
bool InviteToGroupAction::Invite(Player* inviter, Player* player)
@@ -27,7 +29,10 @@ bool InviteToGroupAction::Invite(Player* inviter, Player* player)
{
if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer())
if (!group->isRaidGroup() && group->GetMembersCount() > 4)
group->ConvertToRaid();
{
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(inviter->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
}
WorldPacket p;
@@ -89,7 +94,10 @@ bool InviteNearbyToGroupAction::Execute(Event event)
// When inviting the 5th member of the group convert to raid for future invites.
if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() &&
bot->GetGroup()->GetMembersCount() > 3)
group->ConvertToRaid();
{
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot))
{
@@ -221,7 +229,8 @@ bool InviteGuildToGroupAction::Execute(Event event)
if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() &&
bot->GetGroup()->GetMembersCount() > 3)
{
group->ConvertToRaid();
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
if (sPlayerbotAIConfig->inviteChat &&
@@ -362,7 +371,10 @@ bool LfgAction::Execute(Event event)
if (param.empty() || param == "5" || group->isRaidGroup())
return false; // Group or raid is full so stop trying.
else
group->ConvertToRaid(); // We want a raid but are in a group so convert and continue.
{
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(requester->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
}
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();

View File

@@ -192,23 +192,30 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
{
return false;
}
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
bool disableMoveSplinePath =
sPlayerbotAIConfig->disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig->disableMoveSplinePath == 1 && bot->InBattleground());
bool disableMoveSplinePath = sPlayerbotAIConfig->disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig->disableMoveSplinePath == 1 && bot->InBattleground());
if (Vehicle* vehicle = bot->GetVehicle())
{
VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
Unit* vehicleBase = vehicle->GetBase();
generatePath = !vehicleBase || !vehicleBase->CanFly();
generatePath = vehicleBase->CanFly();
if (!vehicleBase || !seat || !seat->CanControl()) // is passenger and cant move anyway
return false;
float distance = vehicleBase->GetExactDist(x, y, z); // use vehicle distance, not bot
if (distance > 0.01f)
{
DoMovePoint(vehicleBase, x, y, z, generatePath, backwards);
MotionMaster& mm = *vehicleBase->GetMotionMaster(); // need to move vehicle, not bot
mm.Clear();
if (!backwards)
{
mm.MovePoint(0, x, y, z, generatePath);
}
else
{
mm.MovePointBackwards(0, x, y, z, generatePath);
}
float speed = backwards ? vehicleBase->GetSpeed(MOVE_RUN_BACK) : vehicleBase->GetSpeed(MOVE_RUN);
float delay = 1000.0f * (distance / speed);
if (lessDelay)
@@ -234,7 +241,16 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
// bot->CastStop();
// botAI->InterruptSpell();
// }
DoMovePoint(bot, x, y, z, generatePath, backwards);
MotionMaster& mm = *bot->GetMotionMaster();
mm.Clear();
if (!backwards)
{
mm.MovePoint(0, x, y, z, generatePath);
}
else
{
mm.MovePointBackwards(0, x, y, z, generatePath);
}
float delay = 1000.0f * MoveDelay(distance, backwards);
if (lessDelay)
{
@@ -252,7 +268,9 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
Movement::PointsArray path =
SearchForBestPath(x, y, z, modifiedZ, sPlayerbotAIConfig->maxMovementSearchTime, normal_only);
if (modifiedZ == INVALID_HEIGHT)
{
return false;
}
float distance = bot->GetExactDist(x, y, modifiedZ);
if (distance > 0.01f)
{
@@ -264,8 +282,17 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
// bot->CastStop();
// botAI->InterruptSpell();
// }
MotionMaster& mm = *bot->GetMotionMaster();
G3D::Vector3 endP = path.back();
DoMovePoint(bot, x, y, z, generatePath, backwards);
mm.Clear();
if (!backwards)
{
mm.MovePoint(0, x, y, z, generatePath);
}
else
{
mm.MovePointBackwards(0, x, y, z, generatePath);
}
float delay = 1000.0f * MoveDelay(distance, backwards);
if (lessDelay)
{
@@ -482,8 +509,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
// {
// AI_VALUE(LastMovement&, "last area trigger").lastAreaTrigger = entry;
// }
// else
// {
// else {
// LOG_DEBUG("playerbots", "!entry");
// return bot->TeleportTo(movePosition.getMapId(), movePosition.getX(), movePosition.getY(),
// movePosition.getZ(), movePosition.getO(), 0);
@@ -533,7 +559,9 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
// bool goTaxi = bot->ActivateTaxiPathTo({ tEntry->from, tEntry->to }, unit, 1);
// if (botAI->HasCheat(BotCheatMask::gold))
// {
// bot->SetMoney(botMoney);
// }
// LOG_DEBUG("playerbots", "goTaxi");
// return goTaxi;
// }
@@ -960,8 +988,7 @@ bool MovementAction::IsMovingAllowed()
return false;
}
// if (bot->HasUnitMovementFlag(MOVEMENTFLAG_FALLING))
// {
// if (bot->HasUnitMovementFlag(MOVEMENTFLAG_FALLING)) {
// return false;
// }
return bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FLIGHT_MOTION_TYPE;
@@ -971,56 +998,31 @@ bool MovementAction::Follow(Unit* target, float distance) { return Follow(target
void MovementAction::UpdateMovementState()
{
// state flags
const float gLvlZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
const bool onGround = bot->GetPositionZ() < gLvlZ + 1.f;
const bool wantsToFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura();
const auto master = botAI ? botAI->GetMaster() : nullptr; // real or not
const bool masterIsFlying = master && master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING);
const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING);
const auto liquidState = bot->GetLiquidData().Status; // default LIQUID_MAP_NO_WATER
const bool isWaterArea = liquidState != LIQUID_MAP_NO_WATER;
const bool isUnderWater = liquidState == LIQUID_MAP_UNDER_WATER;
const bool isInWater = liquidState == LIQUID_MAP_IN_WATER;
const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING);
const bool wantsToWaterWalk = bot->HasWaterWalkAura();
const bool wantsToSwim = isInWater || isUnderWater;
int8 botInLiquidState = bot->GetLiquidData().Status;
// handle water state
if (isWaterArea)
if (botInLiquidState == LIQUID_MAP_IN_WATER || botInLiquidState == LIQUID_MAP_UNDER_WATER)
{
// water walking
if (wantsToWaterWalk && !isWaterWalking && !isUnderWater && !isFlying)
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING);
bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
bot->SendMovementFlagUpdate();
}
// swimming
else if (wantsToSwim && !isSwimming && !wantsToWaterWalk && !isFlying)
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
bot->AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING);
bot->SendMovementFlagUpdate();
}
bot->SetSwim(true);
}
else
{
// reset flags, if not will inherit incorrect walk speed here and there
// when transistions between land and water.
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
bot->SendMovementFlagUpdate();
bot->SetSwim(false);
}
// handle flying state
if (wantsToFly && !isFlying && masterIsFlying)
bool onGround = bot->GetPositionZ() <
bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()) + 1.0f;
// Keep bot->SendMovementFlagUpdate() withing the if statements to not intefere with bot behavior on
// ground/(shallow) waters
if (!bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) &&
bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) && !onGround)
{
bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
bot->SendMovementFlagUpdate();
}
else if ((!wantsToFly || onGround) && isFlying)
else if (bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) &&
(!bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) || onGround))
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING);
bot->SendMovementFlagUpdate();
@@ -1041,12 +1043,9 @@ void MovementAction::UpdateMovementState()
wasMovementRestricted = isCurrentlyRestricted;
// Temporary speed increase in group
// if (botAI->HasRealPlayerMaster())
// {
// if (botAI->HasRealPlayerMaster()) {
// bot->SetSpeedRate(MOVE_RUN, 1.1f);
// }
// else
// {
// } else {
// bot->SetSpeedRate(MOVE_RUN, 1.0f);
// }
// check if target is not reachable (from Vmangos)
@@ -1055,7 +1054,7 @@ void MovementAction::UpdateMovementState()
// {
// if (Unit* pTarget = sServerFacade->GetChaseTarget(bot))
// {
// if (!bot->IsWithinMeleeRange(pTarget) && pTarget->IsCreature())
// if (!bot->IsWithinMeleeRange(pTarget) && pTarget->GetTypeId() == TYPEID_UNIT)
// {
// float angle = bot->GetAngle(pTarget);
// float distance = 5.0f;
@@ -1683,8 +1682,7 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d
// float MovementAction::SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range, bool
// normal_only, float step)
// {
// if (!generatePath)
// {
// if (!generatePath) {
// return z;
// }
// float min_length = 100000.0f;
@@ -1695,12 +1693,10 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length) {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// {
// if (abs(current_z - z) < 0.5f) {
// return current_z;
// }
// }
@@ -1709,34 +1705,30 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length) {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// if (abs(current_z - z) < 0.5f) {
// return current_z;
// }
// }
// }
// for (delta = range / 2 + step; delta <= range; delta += 2) {
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length) {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// {
// if (abs(current_z - z) < 0.5f) {
// return current_z;
// }
// }
// }
// if (current_z == INVALID_HEIGHT && normal_only)
// {
// if (current_z == INVALID_HEIGHT && normal_only) {
// return INVALID_HEIGHT;
// }
// if (current_z == INVALID_HEIGHT && !normal_only)
// {
// if (current_z == INVALID_HEIGHT && !normal_only) {
// return z;
// }
// return current_z;
@@ -1811,59 +1803,6 @@ const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y,
return result;
}
void MovementAction::DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards)
{
if (!unit)
return;
MotionMaster* mm = unit->GetMotionMaster();
if (!mm)
return;
// enable flying
if (unit->HasUnitMovementFlag(MOVEMENTFLAG_FLYING))
{
unit->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY);
unit->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
}
else
{
unit->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY);
unit->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
}
// enable water walking
if (unit->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING))
{
float gLvlZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ());
unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gLvlZ, false);
// z = gLvlZ; do not overwrite Z axex, otherwise you wont be able to steer the bots into swimming when water
// walking.
}
mm->Clear();
if (backwards)
{
mm->MovePointBackwards(
/*id*/ 0,
/*coords*/ x, y, z,
/*generatePath*/ generatePath,
/*forceDestination*/ false);
return;
}
else
{
mm->MovePoint(
/*id*/ 0,
/*coords*/ x, y, z,
/*forcedMovement*/ FORCED_MOVEMENT_NONE,
/*speed*/ 0.f,
/*orientation*/ 0.f,
/*generatePath*/ generatePath, // true => terrain path (2d mmap); false => straight spline (3d vmap)
/*forceDestination*/ false);
}
}
bool FleeAction::Execute(Event event)
{
return MoveAway(AI_VALUE(Unit*, "current target"), sPlayerbotAIConfig->fleeDistance, true);
@@ -2124,8 +2063,8 @@ Position MovementAction::BestPositionForMeleeToFlee(Position pos, float radius)
if (currentTarget)
{
// Normally, move to left or right is the best position
bool isTanking = (!currentTarget->isFrozen()
&& !currentTarget->HasRootAura()) && (currentTarget->GetVictim() == bot);
bool isTanking =
(!currentTarget->isFrozen() && !currentTarget->HasRootAura()) && (currentTarget->GetVictim() == bot);
float angle = bot->GetAngle(currentTarget);
float angleLeft = angle + (float)M_PI / 2;
float angleRight = angle - (float)M_PI / 2;
@@ -2541,7 +2480,9 @@ bool RearFlankAction::isUseful()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
{
return false;
}
// Need to double the front angle check to account for mirrored angle.
bool inFront = target->HasInArc(2.f * minAngle, bot);
@@ -2556,7 +2497,9 @@ bool RearFlankAction::Execute(Event event)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
{
return false;
}
float angle = frand(minAngle, maxAngle);
float baseDistance = bot->GetMeleeRange(target) * 0.5f;
@@ -2850,7 +2793,7 @@ bool MoveAwayFromCreatureAction::Execute(Event event)
// Find all creatures with the specified Id
std::vector<Unit*> creatures;
for (auto const& guid : targets)
for (const auto& guid : targets)
{
Unit* unit = botAI->GetUnit(guid);
if (unit && (alive && unit->IsAlive()) && unit->GetEntry() == creatureId)

View File

@@ -6,16 +6,17 @@
#include "PassLeadershipToMasterAction.h"
#include "Event.h"
#include "PlayerbotOperations.h"
#include "Playerbots.h"
#include "PlayerbotWorldThreadProcessor.h"
bool PassLeadershipToMasterAction::Execute(Event event)
{
if (Player* master = GetMaster())
if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID()))
{
WorldPacket p(SMSG_GROUP_SET_LEADER, 8);
p << master->GetGUID();
bot->GetSession()->HandleGroupSetLeaderOpcode(p);
auto setLeaderOp = std::make_unique<GroupSetLeaderOperation>(bot->GetGUID(), master->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(setLeaderOp));
if (!message.empty())
botAI->TellMasterNoFacing(message);

View File

@@ -12,6 +12,7 @@
#include "RTSCValues.h"
#include "RtscAction.h"
#include "PositionValue.h"
#include "ByteBuffer.h"
Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp,
bool important)
@@ -31,27 +32,52 @@ Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z,
bool SeeSpellAction::Execute(Event event)
{
WorldPacket p(event.getPacket()); //
// RTSC packet data
WorldPacket p(event.getPacket());
uint8 castCount;
uint32 spellId;
uint8 castCount, castFlags;
uint8 castFlags;
// check RTSC header size = castCount (uint8) + spellId (uint32) + castFlags (uint8)
uint32 const rtscHeaderSize = sizeof(uint8) + sizeof(uint32) + sizeof(uint8);
if (p.size() < rtscHeaderSize)
{
LOG_WARN("playerbots", "SeeSpellAction: Corrupt RTSC packet size={}, expected>={}", p.size(), rtscHeaderSize);
return false;
}
Player* master = botAI->GetMaster();
p.rpos(0);
p >> castCount >> spellId >> castFlags;
if (!master)
return false;
// read RTSC packet data
p.rpos(0); // set read position to start
p >> castCount >> spellId >> castFlags;
// if (!botAI->HasStrategy("RTSC", botAI->GetState()))
// return false;
if (spellId != RTSC_MOVE_SPELL)
return false;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
// should not throw exception,just defensive measure to prevent any crashes when core function breaks.
SpellCastTargets targets;
targets.Read(p, botAI->GetMaster());
try
{
targets.Read(p, master);
if (!targets.GetDst())
{
// do not dereference a null destination; ignore malformed RTSC packets instead of crashing
LOG_WARN("playerbots", "SeeSpellAction: (malformed) RTSC payload does not contain full targets data");
return false;
}
}
catch (ByteBufferException const&)
{
// ignore malformed RTSC packets instead of crashing
LOG_WARN("playerbots", "SeeSpellAction: Failed deserialization (malformed) RTSC payload");
return false;
}
WorldPosition spellPosition(master->GetMapId(), targets.GetDst()->_position);
SET_AI_VALUE(WorldPosition, "see spell location", spellPosition);

View File

@@ -15,7 +15,7 @@ namespace GruulsLairHelpers
SPELL_SPELL_SHIELD = 33054,
// Hunter
SPELL_MISDIRECTION = 34477,
SPELL_MISDIRECTION = 35079,
// Warlock
SPELL_BANISH = 18647, // Rank 2

View File

@@ -24,7 +24,7 @@ namespace MagtheridonHelpers
SPELL_FEAR = 6215,
// Hunter
SPELL_MISDIRECTION = 34477,
SPELL_MISDIRECTION = 35079,
};
enum MagtheridonNPCs