Playerbots integration

pull/522/merge
Flekz 3 months ago committed by David Parra
parent d7234090dc
commit 62cb97ac64

@ -27,13 +27,13 @@ jobs:
- OPTIONAL_DEFINES: ""
TYPE: "default"
- OPTIONAL_DEFINES: "-DBUILD_EXTRACTORS=ON -DBUILD_PLAYERBOT=ON -DBUILD_AHBOT=ON -DBUILD_RECASTDEMOMOD=ON -DBUILD_GIT_ID=ON"
- OPTIONAL_DEFINES: "-DBUILD_EXTRACTORS=ON -DBUILD_PLAYERBOTS=ON -DBUILD_AHBOT=ON -DBUILD_RECASTDEMOMOD=ON -DBUILD_GIT_ID=ON"
TYPE: "with-all"
- OPTIONAL_DEFINES: "-DBUILD_PLAYERBOT=ON -DBUILD_AHBOT=ON"
- OPTIONAL_DEFINES: "-DBUILD_PLAYERBOTS=ON -DBUILD_AHBOT=ON"
TYPE: "with-playerbot-ahbot"
- OPTIONAL_DEFINES: "-DBUILD_PLAYERBOT=ON"
- OPTIONAL_DEFINES: "-DBUILD_PLAYERBOTS=ON"
TYPE: "with-playerbot"
- OPTIONAL_DEFINES: "-DBUILD_AHBOT=ON"
@ -170,8 +170,8 @@ jobs:
- [Default Download](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.DEFAULT_ARCH_NAME}})
- [All Options Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.ALL_ARCH_NAME}})
- [AHBot Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.AB_ARCH_NAME}})
- [PlayerBot Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.PB_ARCH_NAME}})
- [AHBot & PlayerBot Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.PB_AB_ARCH_NAME}})
- [PlayerBots Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.PB_ARCH_NAME}})
- [AHBot & PlayerBots Enabled](${{github.server_url}}/${{ github.repository }}/releases/download/latest/${{env.PB_AB_ARCH_NAME}})
If you find any bugs or issues please report them [here](https://github.com/cmangos/issues/issues/new/choose).
footer: Created by the CMaNGOS Team!

5
.gitignore vendored

@ -91,3 +91,8 @@ cmake_install.cmake
# recastnavigation directory needs exception
!dep/recastnavigation/RecastDemo/Build/
/_build/
#
# Module files
#
src/modules/

@ -329,6 +329,18 @@ if(NOT BUILD_GAME_SERVER AND BUILD_DEPRECATED_PLAYERBOT)
message(STATUS "BUILD_DEPRECATED_PLAYERBOT forced to OFF due to BUILD_GAME_SERVER is not set")
endif()
if(BUILD_PLAYERBOTS)
if(BUILD_DEPRECATED_PLAYERBOT)
set(BUILD_DEPRECATED_PLAYERBOT OFF)
message(STATUS "BUILD_DEPRECATED_PLAYERBOT forced to OFF because BUILD_PLAYERBOTS is set")
endif()
if(NOT BUILD_GAME_SERVER)
set(BUILD_PLAYERBOTS OFF)
message(STATUS "BUILD_PLAYERBOTS forced to OFF due to BUILD_GAME_SERVER is not set")
endif()
endif()
if(PCH)
if(${CMAKE_VERSION} VERSION_LESS "3.16")
message("PCH is not supported by your CMake version")

@ -6,6 +6,7 @@ option(BUILD_GAME_SERVER "Build game server"
option(BUILD_LOGIN_SERVER "Build login server" ON)
option(BUILD_EXTRACTORS "Build map/dbc/vmap/mmap extractors" OFF)
option(BUILD_SCRIPTDEV "Build ScriptDev. (OFF Speedup build)" ON)
option(BUILD_PLAYERBOTS "Build Playerbots mod" OFF)
option(BUILD_AHBOT "Build Auction House Bot mod" OFF)
option(BUILD_METRICS "Build Metrics, generate data for Grafana" OFF)
option(BUILD_RECASTDEMOMOD "Build map/vmap/mmap viewer" OFF)
@ -33,6 +34,7 @@ message(STATUS
BUILD_GAME_SERVER Build game server (core server)
BUILD_LOGIN_SERVER Build login server (auth server)
BUILD_EXTRACTORS Build map/dbc/vmap/mmap extractor
BUILD_PLAYERBOTS Build Playerbots mod
BUILD_AHBOT Build Auction House Bot mod
BUILD_METRICS Build Metrics, generate data for Grafana
BUILD_RECASTDEMOMOD Build map/vmap/mmap viewer

@ -67,6 +67,12 @@ else()
message(STATUS "Build OLD Playerbot : No (default)")
endif()
if(BUILD_PLAYERBOTS)
message(STATUS "Build Playerbots : Yes")
else()
message(STATUS "Build Playerbots : No (default)")
endif()
if(BUILD_EXTRACTORS)
message(STATUS "Build extractors : Yes")
else()

@ -31,6 +31,36 @@
using namespace VMAP;
void rcModAlmostUnwalkableTriangles(rcContext* ctx, const float walkableSlopeAngle,
const float* verts, int /*nv*/,
const int* tris, int nt,
unsigned char* areas)
{
rcIgnoreUnused(ctx);
const float walkableThr = cosf(walkableSlopeAngle / 180.0f * RC_PI);
float norm[3];
for (int i = 0; i < nt; ++i)
{
if (areas[i] & RC_WALKABLE_AREA)
{
const int* tri = &tris[i * 3];
float e0[3], e1[3];
rcVsub(e0, &verts[tri[1] * 3], &verts[tri[0] * 3]);
rcVsub(e1, &verts[tri[2] * 3], &verts[tri[0] * 3]);
rcVcross(norm, e0, e1);
rcVnormalize(norm);
// Check if the face is walkable.
if (norm[1] <= walkableThr)
areas[i] = NAV_AREA_GROUND_STEEP; //Slopes between 50 and 60. Walkable for mobs, unwalkable for players.
}
}
}
void from_json(const json& j, rcConfig& config)
{
config.tileSize = MMAP::VERTEX_PER_TILE;
@ -1004,6 +1034,10 @@ namespace MMAP
unsigned char* triFlags = new unsigned char[tTriCount];
memset(triFlags, NAV_AREA_GROUND, tTriCount * sizeof(unsigned char));
rcClearUnwalkableTriangles(m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags);
// mark almost unwalkable triangles with steep flag
rcModAlmostUnwalkableTriangles(m_rcContext, 50.0f, tVerts, tVertCount, tTris, tTriCount, triFlags);
rcRasterizeTriangles(m_rcContext, tVerts, tVertCount, tTris, triFlags, tTriCount, *tile.solid, tileCfg.walkableClimb);
delete[] triFlags;

@ -8,10 +8,10 @@ CREATE DATABASE `tbcrealmd` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER IF NOT EXISTS 'mangos'@'localhost' IDENTIFIED BY 'mangos';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbcmangos`.* TO 'mangos'@'localhost';
GRANT INDEX, SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbcmangos`.* TO 'mangos'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbclogs`.* TO 'mangos'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbccharacters`.* TO 'mangos'@'localhost';
GRANT INDEX SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbccharacters`.* TO 'mangos'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES ON `tbcrealmd`.* TO 'mangos'@'localhost';

@ -21,6 +21,35 @@ if(BUILD_GAME_SERVER OR BUILD_LOGIN_SERVER OR BUILD_EXTRACTORS)
add_subdirectory(shared)
endif()
# Playerbots module
if(BUILD_PLAYERBOTS)
include(FetchContent)
FetchContent_Declare(
PlayerBots
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/PlayerBots"
GIT_REPOSITORY "https://github.com/cmangos/playerbots.git"
GIT_TAG "master"
)
FetchContent_GetProperties(PlayerBots)
if (NOT playerbots_POPULATED)
FetchContent_Populate(PlayerBots)
message(STATUS "Playerbots module source dir: ${playerbots_SOURCE_DIR}")
else()
message(STATUS "Playerbots module already populated: ${playerbots_POPULATED}")
endif()
add_subdirectory(${playerbots_SOURCE_DIR})
else()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/modules/PlayerBots)
message(STATUS "PlayerBots module exists, but not building. You can remove it manually if you don't need it. (rm -rf ${CMAKE_CURRENT_SOURCE_DIR}/modules/PlayerBots)")
#file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/modules/PlayerBots)
endif()
endif()
if(BUILD_GAME_SERVER)
add_subdirectory(game)
add_subdirectory(mangosd)
@ -28,4 +57,4 @@ endif()
if(BUILD_LOGIN_SERVER)
add_subdirectory(realmd)
endif()
endif()

@ -1683,6 +1683,22 @@ uint32 BattleGround::GetSingleCreatureGuid(uint8 event1, uint8 event2)
return ObjectGuid();
}
/**
Function returns a gameobject guid from event map
@param event1
@param event2
*/
uint32 BattleGround::GetSingleGameObjectGuid(uint8 event1, uint8 event2)
{
auto itr = m_eventObjects[MAKE_PAIR32(event1, event2)].gameobjects.begin();
if (itr != m_eventObjects[MAKE_PAIR32(event1, event2)].gameobjects.end())
{
return *itr;
}
return ObjectGuid();
}
/**
Method that handles gameobject load from DB event map

@ -561,6 +561,9 @@ class BattleGround
// Get creature guid from event
uint32 GetSingleCreatureGuid(uint8 /*event1*/, uint8 /*event2*/);
// Get gameobject guid from event
uint32 GetSingleGameObjectGuid(uint8 /*event1*/, uint8 /*event2*/);
// Handle door events
void OpenDoorEvent(uint8 /*event1*/, uint8 event2 = 0);
bool IsDoorEvent(uint8 /*event1*/, uint8 /*event2*/) const;

@ -51,6 +51,17 @@ if(NOT BUILD_DEPRECATED_PLAYERBOT)
endforeach()
endif()
if(NOT BUILD_PLAYERBOTS)
# exclude Playerbots folder
set (EXCLUDE_DIR "PlayerBots/")
foreach (TMP_PATH ${LIBRARY_SRCS})
string (FIND ${TMP_PATH} ${EXCLUDE_DIR} EXCLUDE_DIR_FOUND)
if (NOT ${EXCLUDE_DIR_FOUND} EQUAL -1)
list(REMOVE_ITEM LIBRARY_SRCS ${TMP_PATH})
endif ()
endforeach()
endif()
set(PCH_BASE_FILENAME "pchdef")
# exclude pchdef files
set (EXCLUDE_FILE "${PCH_BASE_FILENAME}")
@ -83,16 +94,27 @@ target_link_libraries(${LIBRARY_NAME}
PRIVATE zlib
)
# include additionals headers
set(ADDITIONAL_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/vmap
${CMAKE_CURRENT_SOURCE_DIR}/AuctionHouseBot
${CMAKE_CURRENT_SOURCE_DIR}/BattleGround
${CMAKE_CURRENT_SOURCE_DIR}/OutdoorPvP
${CMAKE_CURRENT_SOURCE_DIR}/PlayerBot
${CMAKE_BINARY_DIR}
)
# TO DO: Remove this if when old playerbots get removed
if(NOT BUILD_PLAYERBOTS)
set(ADDITIONAL_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/vmap
${CMAKE_CURRENT_SOURCE_DIR}/AuctionHouseBot
${CMAKE_CURRENT_SOURCE_DIR}/BattleGround
${CMAKE_CURRENT_SOURCE_DIR}/OutdoorPvP
${CMAKE_CURRENT_SOURCE_DIR}/PlayerBot
${CMAKE_BINARY_DIR}
)
else()
set(ADDITIONAL_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/vmap
${CMAKE_CURRENT_SOURCE_DIR}/AuctionHouseBot
${CMAKE_CURRENT_SOURCE_DIR}/BattleGround
${CMAKE_CURRENT_SOURCE_DIR}/OutdoorPvP
${CMAKE_BINARY_DIR}
)
endif()
target_include_directories(${LIBRARY_NAME}
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
@ -100,6 +122,12 @@ target_include_directories(${LIBRARY_NAME}
PRIVATE ${Boost_INCLUDE_DIRS}
)
if(BUILD_PLAYERBOTS)
target_link_libraries(${LIBRARY_NAME} PUBLIC playerbots)
target_include_directories(${LIBRARY_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/src/modules/PlayerBots)
add_dependencies(${LIBRARY_NAME} playerbots)
endif()
if(UNIX)
# Both systems don't have libdl and don't need them
if (NOT (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "NetBSD"))
@ -127,6 +155,11 @@ if (BUILD_DEPRECATED_PLAYERBOT)
add_definitions(-DBUILD_DEPRECATED_PLAYERBOT)
endif()
# Define ENABLE_PLAYERBOTS if need
if (BUILD_PLAYERBOTS)
add_definitions(-DENABLE_PLAYERBOTS)
endif()
if (MSVC)
set_target_properties(${LIBRARY_NAME} PROPERTIES PROJECT_LABEL "Game")
endif()

@ -34,6 +34,12 @@
#include "Pools/PoolManager.h"
#include "GameEvents/GameEventMgr.h"
#ifdef ENABLE_PLAYERBOTS
#include "ahbot/AhBot.h"
#include "playerbot/playerbot.h"
#include "playerbot/PlayerbotAIConfig.h"
#endif
#include <cstdarg>
// Supported shift-links (client generated and server side)
@ -942,6 +948,14 @@ ChatCommand* ChatHandler::getCommandTable()
{ "auction", SEC_ADMINISTRATOR, false, nullptr, "", auctionCommandTable },
#ifdef BUILD_AHBOT
{ "ahbot", SEC_ADMINISTRATOR, true, nullptr, "", ahbotCommandTable },
#endif
#ifdef ENABLE_PLAYERBOTS
#ifndef BUILD_AHBOT
{ "ahbot", SEC_GAMEMASTER, true, &ChatHandler::HandleAhBotCommand, "", nullptr },
#endif
{ "rndbot", SEC_GAMEMASTER, true, &ChatHandler::HandleRandomPlayerbotCommand, "", nullptr },
{ "bot", SEC_PLAYER, false, &ChatHandler::HandlePlayerbotCommand, "", nullptr },
{ "pmon", SEC_GAMEMASTER, true, &ChatHandler::HandlePerfMonCommand, "", nullptr },
#endif
{ "cast", SEC_ADMINISTRATOR, false, nullptr, "", castCommandTable },
{ "character", SEC_GAMEMASTER, true, nullptr, "", characterCommandTable},

@ -99,7 +99,9 @@ class ChatHandler
static bool HasEscapeSequences(const char* message);
static bool CheckEscapeSequences(const char* message);
bool HasSentErrorMessage() const { return sentErrorMessage;}
bool HasSentErrorMessage() const { return sentErrorMessage; }
WorldSession* GetSession() { return m_session; }
/**
* \brief Prepare SMSG_GM_MESSAGECHAT/SMSG_MESSAGECHAT
@ -768,6 +770,13 @@ class ChatHandler
bool HandlePlayerbotCommand(char* args);
#endif
#ifdef ENABLE_PLAYERBOTS
bool HandlePlayerbotCommand(char* args);
bool HandleRandomPlayerbotCommand(char* args);
bool HandleAhBotCommand(char* args);
bool HandlePerfMonCommand(char* args);
#endif
bool HandleArenaFlushPointsCommand(char* args);
bool HandleArenaSeasonRewardsCommand(char* args);
bool HandleArenaDataReset(char* args);

@ -37,6 +37,11 @@
#include "GMTickets/GMTicketMgr.h"
#include "Anticheat/Anticheat.hpp"
#ifdef ENABLE_PLAYERBOTS
#include "playerbot/playerbot.h"
#include "playerbot/RandomPlayerbotMgr.h"
#endif
bool WorldSession::CheckChatMessage(std::string& msg, bool addon/* = false*/)
{
#ifdef BUILD_DEPRECATED_PLAYERBOT
@ -192,6 +197,23 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
if (!CheckChatMessage(msg))
return;
#ifdef ENABLE_PLAYERBOTS
if (GetSecurity() > SEC_PLAYER && GetPlayer()->IsGameMaster())
{
sRandomPlayerbotMgr.HandleCommand(type, msg, *_player, "", TEAM_BOTH_ALLOWED, lang);
}
else
{
sRandomPlayerbotMgr.HandleCommand(type, msg, *_player, "", GetPlayer()->GetTeam(), lang);
}
// apply to own bots
if (_player->GetPlayerbotMgr())
{
_player->GetPlayerbotMgr()->HandleCommand(type, msg, lang);
}
#endif
if (type == CHAT_MSG_SAY)
{
GetPlayer()->Say(msg, lang);
@ -278,6 +300,17 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
}
}
#ifdef ENABLE_PLAYERBOTS
if (player->GetPlayerbotAI())
{
player->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
GetPlayer()->m_speakTime = 0;
GetPlayer()->m_speakCount = 0;
}
if (msg.find("BOT\t") != 0) //These are spoofed SendAddonMessage with channel "WHISPER".
#endif
GetPlayer()->Whisper(msg, lang, player->GetObjectGuid());
if (lang != LANG_ADDON && !m_anticheat->IsSilenced())
@ -316,6 +349,19 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
ChatHandler::BuildChatPacket(data, ChatMsg(type), msg.c_str(), Language(lang), _player->GetChatTag(), _player->GetObjectGuid(), _player->GetName());
group->BroadcastPacket(data, false, group->GetMemberGroup(GetPlayer()->GetObjectGuid()));
#ifdef ENABLE_PLAYERBOTS
for (GroupReference* itr = group->GetFirstMember(); itr != NULL; itr = itr->next())
{
Player* player = itr->getSource();
if (player && player->GetPlayerbotAI())
{
player->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
GetPlayer()->m_speakTime = 0;
GetPlayer()->m_speakCount = 0;
}
}
#endif
break;
}
case CHAT_MSG_GUILD:
@ -340,6 +386,23 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
if (Guild* guild = sGuildMgr.GetGuildById(GetPlayer()->GetGuildId()))
guild->BroadcastToGuild(this, msg, lang == LANG_ADDON ? LANG_ADDON : LANG_UNIVERSAL);
#ifdef ENABLE_PLAYERBOTS
PlayerbotMgr* mgr = GetPlayer()->GetPlayerbotMgr();
if (mgr && GetPlayer()->GetGuildId())
{
for (PlayerBotMap::const_iterator it = mgr->GetPlayerBotsBegin(); it != mgr->GetPlayerBotsEnd(); ++it)
{
Player* const bot = it->second;
if (bot->GetGuildId() == GetPlayer()->GetGuildId())
{
bot->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
}
}
}
sRandomPlayerbotMgr.HandleCommand(type, msg, *_player, "", GetPlayer()->GetTeam(), lang);
#endif
break;
}
case CHAT_MSG_OFFICER:
@ -400,6 +463,20 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID, msg.c_str(), Language(lang), _player->GetChatTag(), _player->GetObjectGuid(), _player->GetName());
group->BroadcastPacket(data, false);
#ifdef ENABLE_PLAYERBOTS
for (GroupReference* itr = group->GetFirstMember(); itr != NULL; itr = itr->next())
{
Player* player = itr->getSource();
if (player && player->GetPlayerbotAI())
{
player->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
GetPlayer()->m_speakTime = 0;
GetPlayer()->m_speakCount = 0;
}
}
#endif
} break;
case CHAT_MSG_RAID_LEADER:
{
@ -431,6 +508,20 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID_LEADER, msg.c_str(), Language(lang), _player->GetChatTag(), _player->GetObjectGuid(), _player->GetName());
group->BroadcastPacket(data, false);
#ifdef ENABLE_PLAYERBOTS
for (GroupReference* itr = group->GetFirstMember(); itr != NULL; itr = itr->next())
{
Player* player = itr->getSource();
if (player && player->GetPlayerbotAI())
{
player->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
GetPlayer()->m_speakTime = 0;
GetPlayer()->m_speakCount = 0;
}
}
#endif
} break;
case CHAT_MSG_RAID_WARNING:
@ -461,6 +552,20 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID_WARNING, msg.c_str(), Language(lang), _player->GetChatTag(), _player->GetObjectGuid(), _player->GetName());
group->BroadcastPacket(data, true);
#ifdef ENABLE_PLAYERBOTS
for (GroupReference* itr = group->GetFirstMember(); itr != NULL; itr = itr->next())
{
Player* player = itr->getSource();
if (player && player->GetPlayerbotAI())
{
player->GetPlayerbotAI()->HandleCommand(type, msg, *GetPlayer(), lang);
GetPlayer()->m_speakTime = 0;
GetPlayer()->m_speakCount = 0;
}
}
#endif
} break;
case CHAT_MSG_BATTLEGROUND:
@ -533,6 +638,24 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data)
if (lang != LANG_ADDON && (chn->HasFlag(Channel::ChannelFlags::CHANNEL_FLAG_GENERAL) || chn->IsStatic()))
m_anticheat->Channel(msg);
#ifdef ENABLE_PLAYERBOTS
// if GM apply to all random bots
if (GetSecurity() > SEC_PLAYER && GetPlayer()->IsGameMaster())
{
sRandomPlayerbotMgr.HandleCommand(type, msg, *_player, "", TEAM_BOTH_ALLOWED, lang);
}
else
{
sRandomPlayerbotMgr.HandleCommand(type, msg, *_player, "", GetPlayer()->GetTeam(), lang);
}
// apply to own bots
if (_player->GetPlayerbotMgr() && chn->GetFlags() & 0x18)
{
_player->GetPlayerbotMgr()->HandleCommand(type, msg, lang);
}
#endif
}
}
} break;

@ -145,6 +145,11 @@ template void Camera::UpdateVisibilityOf(DynamicObject*, UpdateData&, WorldObjec
void Camera::UpdateVisibilityForOwner(bool addToWorld)
{
#ifdef ENABLE_PLAYERBOTS
if (!m_owner.isRealPlayer())
return;
#endif
MaNGOS::VisibleNotifier notifier(*this);
Cell::VisitAllObjects(m_source, notifier, addToWorld ? MAX_VISIBILITY_DISTANCE : m_source->GetVisibilityData().GetVisibilityDistance(), false);
notifier.Notify();

@ -44,6 +44,11 @@
#include "PlayerBot/Base/PlayerbotMgr.h"
#endif
#ifdef ENABLE_PLAYERBOTS
#include "playerbot/playerbot.h"
#include "playerbot/PlayerbotAIConfig.h"
#endif
// config option SkipCinematics supported values
enum CinematicsSkipMode
{
@ -65,6 +70,108 @@ class LoginQueryHolder : public SqlQueryHolder
bool Initialize();
};
#ifdef ENABLE_PLAYERBOTS
class PlayerbotLoginQueryHolder : public LoginQueryHolder
{
private:
uint32 masterAccountId;
PlayerbotHolder* playerbotHolder;
public:
PlayerbotLoginQueryHolder(PlayerbotHolder* playerbotHolder, uint32 masterAccount, uint32 accountId, uint32 guid)
: LoginQueryHolder(accountId, ObjectGuid(HIGHGUID_PLAYER, guid)), masterAccountId(masterAccount), playerbotHolder(playerbotHolder) { }
public:
uint32 GetMasterAccountId() const { return masterAccountId; }
PlayerbotHolder* GetPlayerbotHolder() { return playerbotHolder; }
};
void PlayerbotHolder::AddPlayerBot(uint32 playerGuid, uint32 masterAccount)
{
// has bot already been added?
ObjectGuid guid = ObjectGuid(HIGHGUID_PLAYER, playerGuid);
Player* bot = sObjectMgr.GetPlayer(guid);
if (bot && bot->IsInWorld())
return;
uint32 accountId = sObjectMgr.GetPlayerAccountIdByGUID(guid);
if (accountId == 0)
return;
PlayerbotLoginQueryHolder* holder = new PlayerbotLoginQueryHolder(this, masterAccount, accountId, playerGuid);
if (!holder->Initialize())
{
delete holder; // delete all unprocessed queries
return;
}
CharacterDatabase.DelayQueryHolder(this, &PlayerbotHolder::HandlePlayerBotLoginCallback, holder);
}
void PlayerbotHolder::HandlePlayerBotLoginCallback(QueryResult* dummy, SqlQueryHolder* holder)
{
if (!holder)
return;
PlayerbotLoginQueryHolder* lqh = (PlayerbotLoginQueryHolder*)holder;
uint32 masterAccount = lqh->GetMasterAccountId();
WorldSession* masterSession = masterAccount ? sWorld.FindSession(masterAccount) : NULL;
uint32 botAccountId = lqh->GetAccountId();
WorldSession* botSession = new WorldSession(botAccountId, NULL, SEC_PLAYER, 1, 0, LOCALE_enUS, "", 0, 0, false);
botSession->SetNoAnticheat();
// has bot already been added?
if (sObjectMgr.GetPlayer(lqh->GetGuid()))
return;
uint32 guid = lqh->GetGuid().GetRawValue();
botSession->HandlePlayerLogin(lqh); // will delete lqh
Player* bot = botSession->GetPlayer();
if (!bot)
{
sLog.outError("Error logging in bot %d, please try to reset all random bots", guid);
return;
}
bot->RemovePlayerbotMgr();
sRandomPlayerbotMgr.OnPlayerLogin(bot);
bool allowed = false;
if (botAccountId == masterAccount)
{
allowed = true;
}
else if (masterSession && sPlayerbotAIConfig.allowGuildBots && bot->GetGuildId() == masterSession->GetPlayer()->GetGuildId())
{
allowed = true;
}
else if (sPlayerbotAIConfig.IsInRandomAccountList(botAccountId))
{
allowed = true;
}
if (allowed)
{
OnBotLogin(bot);
return;
}
if (masterSession)
{
ChatHandler ch(masterSession);
ch.PSendSysMessage("You are not allowed to control bot %s", bot->GetName());
}
LogoutPlayerBot(bot->GetObjectGuid());
sLog.outError("Attempt to add not allowed bot %s, please try to reset all random bots", bot->GetName());
}
#endif
bool LoginQueryHolder::Initialize()
{
SetSize(MAX_PLAYER_LOGIN_QUERY);
@ -128,9 +235,30 @@ class CharacterHandler
{
if (!holder) return;
#ifdef ENABLE_PLAYERBOTS
WorldSession* session = sWorld.FindSession(((LoginQueryHolder*)holder)->GetAccountId());
if (!session)
{
delete holder;
return;
}
ObjectGuid guid = ((LoginQueryHolder*)holder)->GetGuid();
session->HandlePlayerLogin((LoginQueryHolder*)holder);
Player* player = session->GetPlayer();
if (player)
{
player->CreatePlayerbotMgr();
player->GetPlayerbotMgr()->OnPlayerLogin(player);
sRandomPlayerbotMgr.OnPlayerLogin(player);
}
#else
if (WorldSession* session = sWorld.FindSession(((LoginQueryHolder*)holder)->GetAccountId()))
session->HandlePlayerLogin((LoginQueryHolder*)holder);
#endif
}
#ifdef BUILD_DEPRECATED_PLAYERBOT
// This callback is different from the normal HandlePlayerLoginCallback in that it
// sets up the bot's world session and also stores the pointer to the bot player in the master's
@ -501,6 +629,36 @@ void WorldSession::HandlePlayerLoginOpcode(WorldPacket& recv_data)
return;
}
#ifdef ENABLE_PLAYERBOTS
if (pCurrChar && pCurrChar->GetPlayerbotAI())
{
WorldSession* botSession = pCurrChar->GetSession();
SetPlayer(pCurrChar, playerGuid);
_player->SetSession(this);
_logoutTime = time(0);
m_sessionDbcLocale = botSession->m_sessionDbcLocale;
m_sessionDbLocaleIndex = botSession->m_sessionDbLocaleIndex;
PlayerbotMgr* mgr = _player->GetPlayerbotMgr();
if (!mgr || mgr->GetMaster() != _player)
{
_player->RemovePlayerbotMgr();
_player->CreatePlayerbotMgr();
_player->GetPlayerbotMgr()->OnPlayerLogin(_player);
if (sRandomPlayerbotMgr.GetPlayerBot(playerGuid))
{
sRandomPlayerbotMgr.MovePlayerBot(playerGuid, _player->GetPlayerbotMgr());
}
else
{
_player->GetPlayerbotMgr()->OnBotLogin(_player);
}
}
}
#endif
if (_player)
{
// player is reconnecting
@ -1241,4 +1399,4 @@ void WorldSession::HandleSetPlayerDeclinedNamesOpcode(WorldPacket& recv_data)
SendPacket(data, true);
sWorld.InvalidatePlayerDataToAllClient(guid);
}
}

@ -45,6 +45,7 @@ typedef std::unordered_set<WorldObject*> WorldObjectUnSet;
typedef std::list<Unit*> UnitList;
typedef std::list<Creature*> CreatureList;
typedef std::list<GameObject*> GameObjectList;
typedef std::list<DynamicObject*> DynamicObjectList;
typedef std::list<Corpse*> CorpseList;
typedef std::list<Player*> PlayerList;
typedef std::unordered_set<Player*> PlayerSet;

@ -2555,8 +2555,14 @@ struct WorldObjectChangeAccumulator
{
// send self fields changes in another way, otherwise
// with new camera system when player's camera too far from player, camera wouldn't receive packets and changes from player
if (i_object.isType(TYPEMASK_PLAYER))
i_object.BuildUpdateDataForPlayer((Player*)&i_object, i_updateDatas);
if (i_object.IsPlayer())
{
Player* plr = static_cast<Player*>(&i_object);
#ifdef ENABLE_PLAYERBOTS
if (plr->isRealPlayer())
#endif
i_object.BuildUpdateDataForPlayer(plr, i_updateDatas);
}
}
void Visit(CameraMapType& m)
@ -2564,8 +2570,15 @@ struct WorldObjectChangeAccumulator
for (auto& iter : m)
{
Player* owner = iter.getSource()->GetOwner();
#ifdef ENABLE_PLAYERBOTS
if (owner->isRealPlayer())
{
#endif
if (owner != &i_object && owner->HasAtClient(&i_object))
i_object.BuildUpdateDataForPlayer(owner, i_updateDatas);
#ifdef ENABLE_PLAYERBOTS
}
#endif
}
}

@ -517,6 +517,10 @@ void WorldSession::HandlePetitionSignOpcode(WorldPacket& recv_data)
// client doesn't allow to sign petition two times by one character, but not check sign by another character from same account
// not allow sign another player from already sign player account
#ifdef ENABLE_PLAYERBOTS
if (!_player->GetPlayerbotAI())
{
#endif
queryResult = CharacterDatabase.PQuery("SELECT playerguid FROM petition_sign WHERE player_account = '%u' AND petitionguid = '%u'", GetAccountId(), petitionLowGuid);
if (queryResult)
@ -534,6 +538,9 @@ void WorldSession::HandlePetitionSignOpcode(WorldPacket& recv_data)
owner->GetSession()->SendPacket(data);
return;
}
#ifdef ENABLE_PLAYERBOTS
}
#endif
CharacterDatabase.PExecute("INSERT INTO petition_sign (ownerguid,petitionguid, playerguid, player_account) VALUES ('%u', '%u', '%u','%u')",
ownerLowGuid, petitionLowGuid, _player->GetGUIDLow(), GetAccountId());

@ -71,6 +71,11 @@
#include "Config/Config.h"
#endif
#ifdef ENABLE_PLAYERBOTS
#include "playerbot/playerbot.h"
#include "playerbot/PlayerbotAIConfig.h"
#endif
#include <cmath>
#define ZONE_UPDATE_INTERVAL (1*IN_MILLISECONDS)
@ -477,9 +482,9 @@ void TradeData::SetAccepted(bool state, bool crosssend /*= false*/)
Player::Player(WorldSession* session): Unit(), m_taxiTracker(*this), m_mover(this), m_camera(this), m_reputationMgr(this), m_launched(false)
{
#ifdef BUILD_DEPRECATED_PLAYERBOT
m_playerbotAI = 0;
m_playerbotMgr = 0;
#if defined(BUILD_DEPRECATED_PLAYERBOT) || defined(ENABLE_PLAYERBOTS)
m_playerbotAI = nullptr;
m_playerbotMgr = nullptr;
#endif
m_speakTime = 0;
m_speakCount = 0;
@ -693,14 +698,20 @@ Player::~Player()
if (m_playerbotAI)
{
delete m_playerbotAI;
m_playerbotAI = 0;
m_playerbotAI = nullptr;
}
if (m_playerbotMgr)
{
delete m_playerbotMgr;
m_playerbotMgr = 0;
m_playerbotMgr = nullptr;
}
#endif
#ifdef ENABLE_PLAYERBOTS
RemovePlayerbotAI();
RemovePlayerbotMgr();
#endif
delete m_declinedname;
}
@ -1565,6 +1576,9 @@ void Player::Update(const uint32 diff)
{
if (diff >= m_DetectInvTimer)
{
#ifdef ENABLE_PLAYERBOTS
if (isRealPlayer())
#endif
HandleStealthedUnitsDetection();
m_DetectInvTimer = GetMap()->IsBattleGroundOrArena() ? 500 : 2000;
}
@ -1634,6 +1648,43 @@ void Player::Heartbeat()
SendUpdateToOutOfRangeGroupMembers();
}
#ifdef ENABLE_PLAYERBOTS
void Player::CreatePlayerbotAI()
{
assert(!m_playerbotAI);
m_playerbotAI = std::make_unique<PlayerbotAI>(this);
}
void Player::RemovePlayerbotAI()
{
m_playerbotAI = nullptr;
}
void Player::CreatePlayerbotMgr()
{
assert(!m_playerbotMgr);
m_playerbotMgr = std::make_unique<PlayerbotMgr>(this);
}
void Player::RemovePlayerbotMgr()
{
m_playerbotMgr = nullptr;
}
void Player::UpdateAI(const uint32 diff, bool minimal)
{
if (m_playerbotAI)
{
m_playerbotAI->UpdateAI(diff);
}
if (m_playerbotMgr)
{
m_playerbotMgr->UpdateAI(diff);
}
}
#endif
void Player::SetDeathState(DeathState s)
{
uint32 ressSpellId = 0;
@ -8655,6 +8706,33 @@ Item* Player::GetItemByPos(uint8 bag, uint8 slot) const
return nullptr;
}
Item* Player::GetItemByEntry(uint32 item) const
{
for (int i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i)
{
if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i))
{
if (pItem->GetEntry() == item)
{
return pItem;
}
}
}
for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i)
{
if (Bag* pBag = (Bag*)GetItemByPos(INVENTORY_SLOT_BAG_0, i))
{
if (Item* itemPtr = pBag->GetItemByEntry(item))
{
return itemPtr;
}
}
}
return nullptr;
}
uint32 Player::GetItemDisplayIdInSlot(uint8 bag, uint8 slot) const
{
const Item* pItem = GetItemByPos(bag, slot);
@ -13163,7 +13241,11 @@ void Player::AddQuest(Quest const* pQuest, Object* questGiver)
questStatusData.uState = QUEST_CHANGED;
// quest accept scripts
#ifdef ENABLE_PLAYERBOTS
if (questGiver && this != questGiver)
#else
if (questGiver)
#endif
{
switch (questGiver->GetTypeId())
{
@ -13370,6 +13452,10 @@ void Player::RewardQuest(Quest const* pQuest, uint32 reward, Object* questGiver,
bool handled = false;
#ifdef ENABLE_PLAYERBOTS
if (this != questGiver)
{
#endif
switch (questGiver->GetTypeId())
{
case TYPEID_UNIT:
@ -13379,9 +13465,17 @@ void Player::RewardQuest(Quest const* pQuest, uint32 reward, Object* questGiver,
handled = sScriptDevAIMgr.OnQuestRewarded(this, (GameObject*)questGiver, pQuest);
break;
}
#ifdef ENABLE_PLAYERBOTS
}
if (this != questGiver)
{
#endif
if (!handled && pQuest->GetQuestCompleteScript() != 0)
GetMap()->ScriptsStart(SCRIPT_TYPE_QUEST_END, pQuest->GetQuestCompleteScript(), questGiver, this, Map::SCRIPT_EXEC_PARAM_UNIQUE_BY_SOURCE);
#ifdef ENABLE_PLAYERBOTS
}
#endif
// Find spell cast on spell reward if any, then find the appropriate caster and cast it
uint32 spellId = pQuest->GetRewSpellCast();
@ -16949,10 +17043,17 @@ void Player::_SaveInventory()
}
// update enchantment durations
#ifdef ENABLE_PLAYERBOTS
if (!GetPlayerbotAI())
{
#endif
for (EnchantDurationList::const_iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end(); ++itr)
{
itr->item->SetEnchantmentDuration(itr->slot, itr->leftduration);
}
#ifdef ENABLE_PLAYERBOTS
}
#endif
// if no changes
if (m_itemUpdateQueue.empty()) return;
@ -20984,6 +21085,164 @@ void Player::learnSpellHighRank(uint32 spellid)
sSpellMgr.doForHighRanks(spellid, worker);
}
#ifdef ENABLE_PLAYERBOTS
void Player::learnClassLevelSpells(bool includeHighLevelQuestRewards)
{
ChrClassesEntry const* clsEntry = sChrClassesStore.LookupEntry(getClass());
if (!clsEntry)
return;
uint32 family = clsEntry->spellfamily;
// special cases which aren't sourced from trainers and normally require quests to obtain - added here for convenience
ObjectMgr::QuestMap const& qTemplates = sObjectMgr.GetQuestTemplates();
for (const auto& qTemplate : qTemplates)
{
Quest const* quest = qTemplate.second;
if (!quest)
continue;
// only class quests player could do
if (quest->GetRequiredClasses() == 0 || !SatisfyQuestClass(quest, false) || !SatisfyQuestRace(quest, false) || !SatisfyQuestLevel(quest, false))
continue;
// custom filter for scripting purposes
if (!includeHighLevelQuestRewards && quest->GetMinLevel() >= 60)
continue;
learnQuestRewardedSpells(quest);
}
// learn trainer spells
for (uint32 id = 0; id < sCreatureStorage.GetMaxEntry(); ++id)
{
CreatureInfo const* co = sCreatureStorage.LookupEntry<CreatureInfo>(id);
if (!co)
continue;
if (co->TrainerType != TRAINER_TYPE_CLASS)
continue;
if (co->TrainerType == TRAINER_TYPE_CLASS && co->TrainerClass != getClass())
continue;
uint32 trainerId = co->TrainerTemplateId;
if (!trainerId)
trainerId = co->Entry;
TrainerSpellData const* trainer_spells = sObjectMgr.GetNpcTrainerTemplateSpells(trainerId);
if (!trainer_spells)
trainer_spells = sObjectMgr.GetNpcTrainerSpells(trainerId);
if (!trainer_spells)
continue;
for (TrainerSpellMap::const_iterator itr = trainer_spells->spellList.begin(); itr != trainer_spells->spellList.end(); ++itr)
{
TrainerSpell const* tSpell = &itr->second;
if (!tSpell)
continue;
uint32 reqLevel = 0;
// skip wrong class/race skills
if (!IsSpellFitByClassAndRace(tSpell->learnedSpell, &reqLevel))
continue;
if (tSpell->conditionId && !sObjectMgr.IsConditionSatisfied(tSpell->conditionId, this, GetMap(), this, CONDITION_FROM_TRAINER))
continue;
// skip spells with first rank learned as talent (and all talents then also)
uint32 first_rank = sSpellMgr.GetFirstSpellInChain(tSpell->learnedSpell);
reqLevel = tSpell->isProvidedReqLevel ? tSpell->reqLevel : std::max(reqLevel, tSpell->reqLevel);
bool isValidTalent = GetTalentSpellCost(first_rank) && HasSpell(first_rank) && reqLevel <= GetLevel();
TrainerSpellState state = GetTrainerSpellState(tSpell, reqLevel);
if (state != TRAINER_SPELL_GREEN && !isValidTalent)
continue;
SpellEntry const* proto = sSpellTemplate.LookupEntry<SpellEntry>(tSpell->learnedSpell);
if (!proto)
continue;
// fix activate state for non-stackable low rank (and find next spell for !active case)
if (uint32 nextId = sSpellMgr.GetSpellBookSuccessorSpellId(proto->Id))
{
if (HasSpell(nextId))
{
// high rank already known so this must !active
continue;
}
}
// skip other spell families (minus a few exceptions)
if (proto->SpellFamilyName != family)
{
SkillLineAbilityMapBounds bounds = sSpellMgr.GetSkillLineAbilityMapBoundsBySpellId(tSpell->learnedSpell);
if (bounds.first == bounds.second)
continue;
SkillLineAbilityEntry const* skillInfo = bounds.first->second;
if (!skillInfo)
continue;
switch (skillInfo->skillId)
{
case SKILL_SUBTLETY:
case SKILL_BEAST_MASTERY:
case SKILL_SURVIVAL:
case SKILL_DEFENSE:
case SKILL_DUAL_WIELD:
case SKILL_FERAL_COMBAT:
case SKILL_PROTECTION:
case SKILL_PLATE_MAIL:
case SKILL_DEMONOLOGY:
case SKILL_ENHANCEMENT:
case SKILL_MAIL:
case SKILL_HOLY2:
case SKILL_LOCKPICKING:
break;
default: continue;
}
}
// skip wrong class/race skills
if (!IsSpellFitByClassAndRace(tSpell->learnedSpell))
continue;
// skip broken spells
if (!SpellMgr::IsSpellValid(proto, this, false))
continue;
if (tSpell->learnedSpell)
{
bool learned = false;
for (int j = 0; j < 3; ++j)
{
if (proto->Effect[j] == SPELL_EFFECT_LEARN_SPELL)
{
uint32 learnedSpell = proto->EffectTriggerSpell[j];
learnSpell(learnedSpell, false);
learned = true;
}
}
if (!learned)
{
learnSpell(tSpell->learnedSpell, false);
}
}
else
{
CastSpell(this, tSpell->spell, TRIGGERED_OLD_TRIGGERED);
}
}
}
}
#endif
void Player::_LoadSkills(std::unique_ptr<QueryResult> queryResult)
{
// 0 1 2
@ -21571,6 +21830,11 @@ AreaLockStatus Player::GetAreaTriggerLockStatus(AreaTrigger const* at, uint32& m
if (!GetGroup() || !GetGroup()->IsRaidGroup())
return AREA_LOCKSTATUS_RAID_LOCKED;
#ifdef ENABLE_PLAYERBOTS
if (!isRealPlayer() && GetPlayerbotAI() && GetPlayerbotAI()->CanEnterArea(at))
return AREA_LOCKSTATUS_OK;
#endif
// Item Requirements: must have requiredItem OR requiredItem2, report the first one that's missing
if (at->requiredItem)
{

@ -64,6 +64,11 @@ struct FactionTemplateEntry;
#include "PlayerBot/Base/PlayerbotAI.h"
#endif
#ifdef ENABLE_PLAYERBOTS
class PlayerbotAI;
class PlayerbotMgr;
#endif
struct AreaTrigger;
typedef std::deque<Mail*> PlayerMails;
@ -1136,6 +1141,7 @@ class Player : public Unit
Item* GetItemByGuid(ObjectGuid guid) const;
Item* GetItemByPos(uint16 pos) const;
Item* GetItemByPos(uint8 bag, uint8 slot) const;
Item* GetItemByEntry(uint32 item) const;
uint32 GetItemDisplayIdInSlot(uint8 bag, uint8 slot) const;
Item* GetWeaponForAttack(WeaponAttackType attackType) const { return GetWeaponForAttack(attackType, false, false); }
Item* GetWeaponForAttack(WeaponAttackType attackType, bool nonbroken, bool useable) const;
@ -1558,6 +1564,10 @@ class Player : public Unit
void learnQuestRewardedSpells(Quest const* quest);
void learnSpellHighRank(uint32 spellid);
#ifdef ENABLE_PLAYERBOTS
void learnClassLevelSpells(bool includeHighLevelQuestRewards = false);
#endif
uint32 GetFreeTalentPoints() const { return GetUInt32Value(PLAYER_CHARACTER_POINTS1); }
void SetFreeTalentPoints(uint32 points) { SetUInt32Value(PLAYER_CHARACTER_POINTS1, points); }
void UpdateFreeTalentPoints(bool resetIfNeed = true);
@ -2264,6 +2274,20 @@ class Player : public Unit
bool IsInDuel() const { return duel && duel->startTime != 0; }
#endif
#ifdef ENABLE_PLAYERBOTS
// A Player can either have a playerbotMgr (to manage its bots), or have playerbotAI (if it is a bot), or
// neither. Code that enables bots must create the playerbotMgr and set it using SetPlayerbotMgr.
void UpdateAI(const uint32 diff, bool minimal = false);
void CreatePlayerbotAI();
void RemovePlayerbotAI();
PlayerbotAI* GetPlayerbotAI() { return m_playerbotAI.get(); }
void CreatePlayerbotMgr();
void RemovePlayerbotMgr();
PlayerbotMgr* GetPlayerbotMgr() { return m_playerbotMgr.get(); }
void SetBotDeathTimer() { m_deathTimer = 0; }
bool isRealPlayer() { return m_session && (m_session->GetRemoteAddress() != "disconnected/bot"); }
#endif
virtual UnitAI* AI() override { if (m_charmInfo) return m_charmInfo->GetAI(); return nullptr; }
virtual CombatData* GetCombatData() override { if (m_charmInfo && m_charmInfo->GetCombatData()) return m_charmInfo->GetCombatData(); return m_combatData; }
virtual CombatData const* GetCombatData() const override { if (m_charmInfo && m_charmInfo->GetCombatData()) return m_charmInfo->GetCombatData(); return m_combatData; }