mirror of
https://github.com/mod-playerbots/mod-playerbots.git
synced 2026-01-13 09:07:19 +00:00
Compare commits
11 Commits
05057ae9b5
...
master_v16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6620def962 | ||
|
|
cddd406b1c | ||
|
|
cf743a186a | ||
|
|
10213d8381 | ||
|
|
d97870facd | ||
|
|
0c1700c117 | ||
|
|
0b1b0eaecc | ||
|
|
8e03371147 | ||
|
|
27311b734d | ||
|
|
bb5ed37cd3 | ||
|
|
e88c1b779b |
5
.github/workflows/codestyle_cpp.yml
vendored
5
.github/workflows/codestyle_cpp.yml
vendored
@@ -5,11 +5,16 @@ on:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
paths:
|
||||
- src/**
|
||||
- "!README.md"
|
||||
- "!docs/**"
|
||||
|
||||
concurrency:
|
||||
group: "codestyle-cppcheck-${{ github.event.pull_request.number }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -34,7 +34,7 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can
|
||||
|
||||
Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support.
|
||||
|
||||
**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the playerbots module to build and function. Updates from the upstream are implemneted regularly to this branch. Instructions for installing this required branch and this module are provided below.
|
||||
**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below.
|
||||
|
||||
### Cloning the Repositories
|
||||
|
||||
@@ -50,7 +50,7 @@ For more information, refer to the [AzerothCore Installation Guide](https://www.
|
||||
|
||||
### Docker Installation
|
||||
|
||||
Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install the `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module:
|
||||
Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
|
||||
@@ -103,7 +103,7 @@ Please click on the "⭐" button to stay up to date and help us gain more visibi
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
`mod-playerbots` is is based off [ZhengPeiRu21/mod-playerbots](https://github.com/ZhengPeiRu21/mod-playerbots) and [celguar/mangosbot-bots](https://github.com/celguar/mangosbot-bots). We extend our gratitude to [@ZhengPeiRu21](https://github.com/ZhengPeiRu21) and [@celguar](https://github.com/celguar) for the continued efforts in maintaining the module.
|
||||
`mod-playerbots` is based on [ZhengPeiRu21/mod-playerbots](https://github.com/ZhengPeiRu21/mod-playerbots) and [celguar/mangosbot-bots](https://github.com/celguar/mangosbot-bots). We extend our gratitude to [@ZhengPeiRu21](https://github.com/ZhengPeiRu21) and [@celguar](https://github.com/celguar) for their continued efforts in maintaining the module.
|
||||
|
||||
Also, a thank you to the many contributors who've helped build this project:
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
93
src/PlayerbotOperation.h
Normal 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
500
src/PlayerbotOperations.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
217
src/PlayerbotWorldThreadProcessor.cpp
Normal file
217
src/PlayerbotWorldThreadProcessor.cpp
Normal 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
|
||||
}
|
||||
142
src/PlayerbotWorldThreadProcessor.h
Normal file
142
src/PlayerbotWorldThreadProcessor.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -122,21 +122,18 @@ bool CheckMountStateAction::Execute(Event /*event*/)
|
||||
bool shouldMount = false;
|
||||
|
||||
Unit* currentTarget = AI_VALUE(Unit*, "current target");
|
||||
bool masterInCombat = master && master->IsInCombat();
|
||||
|
||||
if (currentTarget && (bot->IsInCombat() || masterInCombat))
|
||||
if (currentTarget)
|
||||
{
|
||||
// Use target-based logic if bot is in combat OR master is in combat and needs assistance
|
||||
float dismountDistance = CalculateDismountDistance();
|
||||
float mountDistance = CalculateMountDistance();
|
||||
float combatReach = bot->GetCombatReach() + currentTarget->GetCombatReach();
|
||||
float distanceToTarget = bot->GetExactDist(currentTarget);
|
||||
|
||||
shouldDismount = (distanceToTarget <= dismountDistance);
|
||||
shouldMount = (distanceToTarget > mountDistance);
|
||||
shouldDismount = (distanceToTarget <= dismountDistance + combatReach);
|
||||
shouldMount = (distanceToTarget > mountDistance + combatReach);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If neither bot nor master is in combat, prioritize master-following
|
||||
shouldMount = true;
|
||||
}
|
||||
|
||||
@@ -163,19 +160,10 @@ bool CheckMountStateAction::Execute(Event /*event*/)
|
||||
|
||||
else if (ShouldDismountForMaster(master) && bot->IsMounted())
|
||||
{
|
||||
// If master dismounted, stay mounted until close enough to assist
|
||||
if (StayMountedToCloseDistance())
|
||||
return false;
|
||||
|
||||
Dismount();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mount up to close distance to master if beneficial - allow mounting even if master is in combat
|
||||
// as long as the bot itself is not in combat and has no attackers
|
||||
else if (!bot->IsMounted() && noAttackers && !bot->IsInCombat() && ShouldMountToCloseDistance())
|
||||
return Mount();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -409,50 +397,6 @@ bool CheckMountStateAction::TryRandomMountFiltered(const std::map<int32, std::ve
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CheckMountStateAction::StayMountedToCloseDistance() const
|
||||
{
|
||||
// Keep the bot mounted while closing distance to a recently dismounted master.
|
||||
// Rationale: if the master dismounts far away, immediately dismounting slows the bot down
|
||||
// and delays assistance. Instead, remain mounted until within reasonable proximity
|
||||
// of the master, then dismount to help.
|
||||
|
||||
if (!master)
|
||||
return false;
|
||||
|
||||
float distToMaster = sServerFacade->GetDistance2d(bot, master);
|
||||
|
||||
// If master is in combat, dismount at combat assist range to help immediately
|
||||
if (master->IsInCombat())
|
||||
{
|
||||
float assistRange = CalculateDismountDistance();
|
||||
return distToMaster > assistRange;
|
||||
}
|
||||
|
||||
// If master is not in combat, use smaller proximity range for general following
|
||||
float masterProximityRange = 10.0f; // Close enough to be near master but not attack range
|
||||
return distToMaster > masterProximityRange;
|
||||
}
|
||||
|
||||
bool CheckMountStateAction::ShouldMountToCloseDistance() const
|
||||
{
|
||||
// Mount up to close distance to master if beneficial
|
||||
// Uses the same logic as CalculateMountDistance() which already considers the 2-second mount cast time
|
||||
// This handles cases where master is in combat but bot isn't, and bot needs to mount to reach master
|
||||
|
||||
if (!master)
|
||||
return false;
|
||||
|
||||
// Only mount to close distance when actively following
|
||||
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
|
||||
return false;
|
||||
|
||||
float distToMaster = sServerFacade->GetDistance2d(bot, master);
|
||||
float mountDistance = CalculateMountDistance();
|
||||
|
||||
// Mount if distance is greater than the calculated mount distance threshold
|
||||
return distToMaster > mountDistance;
|
||||
}
|
||||
|
||||
float CheckMountStateAction::CalculateDismountDistance() const
|
||||
{
|
||||
// Warrior bots should dismount far enough to charge (because it's important for generating some initial rage),
|
||||
|
||||
@@ -60,8 +60,6 @@ private:
|
||||
bool TryPreferredMount(Player* master) const;
|
||||
uint32 GetMountType(Player* master) const;
|
||||
bool TryRandomMountFiltered(const std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const;
|
||||
bool StayMountedToCloseDistance() const;
|
||||
bool ShouldMountToCloseDistance() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
#include "Event.h"
|
||||
#include "Playerbots.h"
|
||||
#include "ServerFacade.h"
|
||||
#include "AoeValues.h"
|
||||
#include "TargetValue.h"
|
||||
|
||||
NextAction** CastAbolishPoisonAction::getAlternatives()
|
||||
{
|
||||
@@ -30,6 +33,32 @@ bool CastEntanglingRootsCcAction::Execute(Event event) { return botAI->CastSpell
|
||||
Value<Unit*>* CastHibernateCcAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", "hibernate"); }
|
||||
|
||||
bool CastHibernateCcAction::Execute(Event event) { return botAI->CastSpell("hibernate", GetTarget()); }
|
||||
bool CastStarfallAction::isUseful()
|
||||
{
|
||||
if (!CastSpellAction::isUseful())
|
||||
return false;
|
||||
|
||||
// Avoid breaking CC
|
||||
WorldLocation aoePos = *context->GetValue<WorldLocation>("aoe position");
|
||||
Unit* ccTarget = context->GetValue<Unit*>("current cc target")->Get();
|
||||
if (ccTarget && ccTarget->IsAlive())
|
||||
{
|
||||
float dist2d = sServerFacade->GetDistance2d(ccTarget, aoePos.GetPositionX(), aoePos.GetPositionY());
|
||||
if (sServerFacade->IsDistanceLessOrEqualThan(dist2d, sPlayerbotAIConfig->aoeRadius))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Avoid single-target usage on initial pull
|
||||
uint8 aoeCount = *context->GetValue<uint8>("aoe count");
|
||||
if (aoeCount < 2)
|
||||
{
|
||||
Unit* target = context->GetValue<Unit*>("current target")->Get();
|
||||
if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NextAction** CastReviveAction::getPrerequisites()
|
||||
{
|
||||
|
||||
@@ -144,6 +144,8 @@ class CastStarfallAction : public CastSpellAction
|
||||
{
|
||||
public:
|
||||
CastStarfallAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfall") {}
|
||||
|
||||
bool isUseful() override;
|
||||
};
|
||||
|
||||
class CastHurricaneAction : public CastSpellAction
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace GruulsLairHelpers
|
||||
SPELL_SPELL_SHIELD = 33054,
|
||||
|
||||
// Hunter
|
||||
SPELL_MISDIRECTION = 34477,
|
||||
SPELL_MISDIRECTION = 35079,
|
||||
|
||||
// Warlock
|
||||
SPELL_BANISH = 18647, // Rank 2
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace MagtheridonHelpers
|
||||
SPELL_FEAR = 6215,
|
||||
|
||||
// Hunter
|
||||
SPELL_MISDIRECTION = 34477,
|
||||
SPELL_MISDIRECTION = 35079,
|
||||
};
|
||||
|
||||
enum MagtheridonNPCs
|
||||
|
||||
Reference in New Issue
Block a user