/* * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by the * Free Software Foundation; either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "Playerbots.h" #include "Channel.h" #include "Config.h" #include "DatabaseEnv.h" #include "DatabaseLoader.h" #include "GuildTaskMgr.h" #include "Metric.h" #include "PlayerScript.h" #include "PlayerbotAIConfig.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" #include "cs_playerbots.h" #include "cmath" #include "BattleGroundTactics.h" #include "ObjectAccessor.h" class PlayerbotsDatabaseScript : public DatabaseScript { public: PlayerbotsDatabaseScript() : DatabaseScript("PlayerbotsDatabaseScript") {} bool OnDatabasesLoading() override { DatabaseLoader playerbotLoader("server.playerbots"); playerbotLoader.SetUpdateFlags(sConfigMgr->GetOption("Playerbots.Updates.EnableDatabases", true) ? DatabaseLoader::DATABASE_PLAYERBOTS : 0); playerbotLoader.AddDatabase(PlayerbotsDatabase, "Playerbots"); return playerbotLoader.Load(); } void OnDatabasesKeepAlive() override { PlayerbotsDatabase.KeepAlive(); } void OnDatabasesClosing() override { PlayerbotsDatabase.Close(); } void OnDatabaseWarnAboutSyncQueries(bool apply) override { PlayerbotsDatabase.WarnAboutSyncQueries(apply); } void OnDatabaseSelectIndexLogout(Player* player, uint32& statementIndex, uint32& statementParam) override { statementIndex = CHAR_UPD_CHAR_OFFLINE; statementParam = player->GetGUID().GetCounter(); } void OnDatabaseGetDBRevision(std::string& revision) override { if (QueryResult resultPlayerbot = PlayerbotsDatabase.Query("SELECT date FROM version_db_playerbots ORDER BY date DESC LIMIT 1")) { Field* fields = resultPlayerbot->Fetch(); revision = fields[0].Get(); } if (revision.empty()) { revision = "Unknown Playerbots Database Revision"; } } }; class PlayerbotsPlayerScript : public PlayerScript { public: PlayerbotsPlayerScript() : PlayerScript("PlayerbotsPlayerScript", { PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_AFTER_UPDATE, PLAYERHOOK_ON_CHAT, PLAYERHOOK_ON_CHAT_WITH_CHANNEL, PLAYERHOOK_ON_CHAT_WITH_GROUP, PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS, PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE, PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT, PLAYERHOOK_ON_GIVE_EXP }) {} void OnPlayerLogin(Player* player) override { if (!player->GetSession()->IsBot()) { sPlayerbotsMgr->AddPlayerbotData(player, false); sRandomPlayerbotMgr->OnPlayerLogin(player); // Before modifying the following messages, please make sure it does not violate the AGPLv3.0 license // especially if you are distributing a repack or hosting a public server // e.g. you can replace the URL with your own repository, // but it should be publicly accessible and include all modifications you've made if (sPlayerbotAIConfig->enabled) { ChatHandler(player->GetSession()).SendSysMessage( "|cff00ff00This server runs with |cff00ccffmod-playerbots|r " "|cffcccccchttps://github.com/liyunfan1223/mod-playerbots|r"); } /*if (sPlayerbotAIConfig->enabled || sPlayerbotAIConfig->randomBotAutologin) { std::string roundedTime = std::to_string(std::ceil((sPlayerbotAIConfig->maxRandomBots * 0.11 / 60) * 10) / 10.0); roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2); ChatHandler(player->GetSession()).SendSysMessage( "|cff00ff00Playerbots:|r bot initialization at server startup takes about '" + roundedTime + "' minutes."); }*/ } } void OnPlayerAfterUpdate(Player* player, uint32 diff) override { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player)) { botAI->UpdateAI(diff); } if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { playerbotMgr->UpdateAI(diff); } } bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Player* receiver) override { /*if (type == CHAT_MSG_WHISPER) { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(receiver)) { botAI->HandleCommand(type, msg, player); return false; } }*/ if (type == CHAT_MSG_WHISPER && receiver) // [Crash Fix] Add non-null receiver check to avoid calling on a null pointer in edge cases. { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(receiver)) { botAI->HandleCommand(type, msg, player); return false; } } return true; } void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override { /*for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { if (Player* member = itr->GetSource()) { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(member)) { botAI->HandleCommand(type, msg, player); } } }*/ if (!group) return; // [Crash Fix] 'group' should not be null in this hook, but this safeguard prevents a crash if the caller changes or in case of an unexpected call. for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (!member) continue; if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(member)) { botAI->HandleCommand(type, msg, player); } } } void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg) override { if (type == CHAT_MSG_GUILD) { if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { for (PlayerBotMap::const_iterator it = playerbotMgr->GetPlayerBotsBegin(); it != playerbotMgr->GetPlayerBotsEnd(); ++it) { if (Player* const bot = it->second) { if (bot->GetGuildId() == player->GetGuildId()) { // GET_PLAYERBOT_AI(bot)->HandleCommand(type, msg, player); if (PlayerbotAI* ai = GET_PLAYERBOT_AI(bot)) // [Crash Fix] Possible crash source because we don't check if the returned pointer is not null ai->HandleCommand(type, msg, player); } } } } } } void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override { if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { if (channel->GetFlags() & 0x18) { playerbotMgr->HandleCommand(type, msg); } } sRandomPlayerbotMgr->HandleCommand(type, msg, player); } bool OnPlayerBeforeAchievementComplete(Player* player, AchievementEntry const* achievement) override { if (sRandomPlayerbotMgr->IsRandomBot(player) && (achievement->flags == 256 || achievement->flags == 768)) { return false; } return true; } void OnPlayerGiveXP(Player* player, uint32& amount, Unit* /*victim*/, uint8 /*xpSource*/) override { // early return if (sPlayerbotAIConfig->randomBotXPRate == 1.0 || !player) return; // no XP multiplier, when player is no bot. if (!player->GetSession()->IsBot() || !sRandomPlayerbotMgr->IsRandomBot(player)) return; // no XP multiplier, when bot has group where leader is a real player. if (Group* group = player->GetGroup()) { Player* leader = group->GetLeader(); if (leader && leader != player) { if (PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(leader)) { if (leaderBotAI->HasRealPlayerMaster()) return; } } } // otherwise apply bot XP multiplier. amount = static_cast(std::round(static_cast(amount) * sPlayerbotAIConfig->randomBotXPRate)); } }; class PlayerbotsMiscScript : public MiscScript { public: PlayerbotsMiscScript() : MiscScript("PlayerbotsMiscScript", {MISCHOOK_ON_DESTRUCT_PLAYER}) {} void OnDestructPlayer(Player* player) override { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player)) { delete botAI; } if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { delete playerbotMgr; } } }; class PlayerbotsServerScript : public ServerScript { public: PlayerbotsServerScript() : ServerScript("PlayerbotsServerScript", { SERVERHOOK_CAN_PACKET_RECEIVE }) {} void OnPacketReceived(WorldSession* session, WorldPacket const& packet) override { if (Player* player = session->GetPlayer()) if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) playerbotMgr->HandleMasterIncomingPacket(packet); } }; class PlayerbotsWorldScript : public WorldScript { public: PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", { WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED }) {} void OnBeforeWorldInitialized() override { // Before modifying the following messages, please make sure it does not violate the AGPLv3.0 license // especially if you are distributing a repack or hosting a public server // e.g. you can replace the URL with your own repository, // but it should be publicly accessible and include all modifications you've made LOG_INFO("server.loading", "╔══════════════════════════════════════════════════════════╗"); LOG_INFO("server.loading", "║ ║"); LOG_INFO("server.loading", "║ AzerothCore Playerbots Module ║"); LOG_INFO("server.loading", "║ ║"); LOG_INFO("server.loading", "╟──────────────────────────────────────────────────────────╢"); LOG_INFO("server.loading", "║ mod-playerbots is a community-driven open-source ║"); LOG_INFO("server.loading", "║ project based on AzerothCore, licensed under AGPLv3.0 ║"); LOG_INFO("server.loading", "╟──────────────────────────────────────────────────────────╢"); LOG_INFO("server.loading", "║ https://github.com/liyunfan1223/mod-playerbots ║"); LOG_INFO("server.loading", "╚══════════════════════════════════════════════════════════╝"); uint32 oldMSTime = getMSTime(); LOG_INFO("server.loading", " "); LOG_INFO("server.loading", "Load Playerbots Config..."); sPlayerbotAIConfig->Initialize(); LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", " "); } }; class PlayerbotsScript : public PlayerbotScript { public: PlayerbotsScript() : PlayerbotScript("PlayerbotsScript") {} /*bool OnPlayerbotCheckLFGQueue(lfg::Lfg5Guids const& guidsList) override { bool nonBotFound = false; for (ObjectGuid const& guid : guidsList.guids) { Player* player = ObjectAccessor::FindPlayer(guid); if (guid.IsGroup() || (player && !GET_PLAYERBOT_AI(player))) { nonBotFound = true; break; } } return nonBotFound; }*/ // New LFG Function bool OnPlayerbotCheckLFGQueue(lfg::Lfg5Guids const& guidsList) { const size_t totalSlots = guidsList.guids.size(); size_t ignoredEmpty = 0, ignoredNonPlayer = 0; size_t offlinePlayers = 0, botPlayers = 0, realPlayers = 0; bool groupGuidSeen = false; LOG_DEBUG("playerbots", "[LFG] check start: slots={}", totalSlots); for (size_t i = 0; i < totalSlots; ++i) { ObjectGuid const& guid = guidsList.guids[i]; // 1) Placeholders to ignore if (guid.IsEmpty()) { ++ignoredEmpty; LOG_DEBUG("playerbots", "[LFG] slot {}: -> ignored", i); continue; } // Group GUID: in the original implementation this counted as "non-bot found" if (guid.IsGroup()) { groupGuidSeen = true; LOG_DEBUG("playerbots", "[LFG] slot {}: -> counts as having a real player (compat)", i); continue; } // Other non-Player GUIDs: various placeholders, ignore them if (!guid.IsPlayer()) { ++ignoredNonPlayer; LOG_DEBUG("playerbots", "[LFG] slot {}: guid={} (non-player/high={}) -> ignored", i, static_cast(guid.GetRawValue()), (unsigned)guid.GetHigh()); continue; } // 2) Player present? Player* player = ObjectAccessor::FindPlayer(guid); if (!player) { ++offlinePlayers; LOG_DEBUG("playerbots", "[LFG] slot {}: player guid={} is offline/not in world", i, static_cast(guid.GetRawValue())); continue; } // 3) Bot or real player? if (GET_PLAYERBOT_AI(player) != nullptr) { ++botPlayers; LOG_DEBUG("playerbots", "[LFG] slot {}: BOT {} (lvl {}, class {})", i, player->GetName().c_str(), player->GetLevel(), player->getClass()); } else { ++realPlayers; LOG_DEBUG("playerbots", "[LFG] slot {}: REAL {} (lvl {}, class {})", i, player->GetName().c_str(), player->GetLevel(), player->getClass()); } } // "Ultra-early phase" detection: only placeholders => DO NOT VETO const bool onlyPlaceholders = (realPlayers + botPlayers + (groupGuidSeen ? 1 : 0)) == 0 && (ignoredEmpty + ignoredNonPlayer) == totalSlots; // "Soft" LFG preflight if we actually see players AND at least one offline if (!onlyPlaceholders && offlinePlayers > 0) { // Find a plausible leader: prefer a real online player, otherwise any online player Player* leader = nullptr; for (ObjectGuid const& guid : guidsList.guids) if (guid.IsPlayer()) if (Player* p = ObjectAccessor::FindPlayer(guid)) if (GET_PLAYERBOT_AI(p) == nullptr) { leader = p; break; } if (!leader) for (ObjectGuid const& guid : guidsList.guids) if (guid.IsPlayer()) if (Player* p = ObjectAccessor::FindPlayer(guid)) { leader = p; break; } if (leader) { Group* g = leader->GetGroup(); if (g) { LOG_DEBUG("playerbots", "[LFG-RESET] group members={}, isRaid={}, isLFGGroup={}", (int)g->GetMembersCount(), g->isRaidGroup() ? 1 : 0, g->isLFGGroup() ? 1 : 0); // "Soft" reset of LFG states on the bots' AI side (proposal/role-check, etc.) for (GroupReference* ref = g->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) continue; if (PlayerbotAI* ai = GET_PLAYERBOT_AI(member)) ai->Reset(true); } } } LOG_DEBUG("playerbots", "[LFG] preflight soft-reset triggered (offline detected) -> allowQueue=no (retry)"); return false; // ask the client to retry right after the reset } // "Hybrid" policy: permissive if only placeholders; otherwise original logic bool allowQueue = onlyPlaceholders ? true : ((offlinePlayers == 0) && (realPlayers >= 1 || groupGuidSeen)); LOG_DEBUG("playerbots", "[LFG] summary: slots={}, real={}, bots={}, offline={}, ignored(empty+nonPlayer)={}, " "groupGuidSeen={} -> allowQueue={}", totalSlots, realPlayers, botPlayers, offlinePlayers, (ignoredEmpty + ignoredNonPlayer), (groupGuidSeen ? "yes" : "no"), (allowQueue ? "yes" : "no")); return allowQueue; } // End LFG void OnPlayerbotCheckKillTask(Player* player, Unit* victim) override { if (player) sGuildTaskMgr->CheckKillTask(player, victim); } void OnPlayerbotCheckPetitionAccount(Player* player, bool& found) override { if (found && GET_PLAYERBOT_AI(player)) found = false; } bool OnPlayerbotCheckUpdatesToSend(Player* player) override { if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player)) return botAI->IsRealPlayer(); return true; } void OnPlayerbotPacketSent(Player* player, WorldPacket const* packet) override { if (!player) return; if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player)) { botAI->HandleBotOutgoingPacket(*packet); } if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { playerbotMgr->HandleMasterOutgoingPacket(*packet); } } void OnPlayerbotUpdate(uint32 diff) override { sRandomPlayerbotMgr->UpdateAI(diff); sRandomPlayerbotMgr->UpdateSessions(); } void OnPlayerbotUpdateSessions(Player* player) override { if (player) if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) playerbotMgr->UpdateSessions(); } void OnPlayerbotLogout(Player* player) override { // immediate purge of the bot's AI upon disconnection if (player && player->GetSession()->IsBot()) sPlayerbotsMgr->RemovePlayerbotAI(player->GetGUID()); // removes a long-standing crash (0xC0000005 ACCESS_VIOLATION) if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); if (!botAI || botAI->IsRealPlayer()) { playerbotMgr->LogoutAllBots(); } } sRandomPlayerbotMgr->OnPlayerLogout(player); } void OnPlayerbotLogoutBots() override { LOG_INFO("playerbots", "Logging out all bots..."); sRandomPlayerbotMgr->LogoutAllBots(); } }; class PlayerBotsBGScript : public BGScript { public: PlayerBotsBGScript() : BGScript("PlayerBotsBGScript") {} void OnBattlegroundStart(Battleground* bg) override { BGStrategyData data; switch (bg->GetBgTypeID()) { case BATTLEGROUND_WS: data.allianceStrategy = urand(0, WS_STRATEGY_MAX - 1); data.hordeStrategy = urand(0, WS_STRATEGY_MAX - 1); break; case BATTLEGROUND_AB: data.allianceStrategy = urand(0, AB_STRATEGY_MAX - 1); data.hordeStrategy = urand(0, AB_STRATEGY_MAX - 1); break; case BATTLEGROUND_AV: data.allianceStrategy = urand(0, AV_STRATEGY_MAX - 1); data.hordeStrategy = urand(0, AV_STRATEGY_MAX - 1); break; case BATTLEGROUND_EY: data.allianceStrategy = urand(0, EY_STRATEGY_MAX - 1); data.hordeStrategy = urand(0, EY_STRATEGY_MAX - 1); break; default: break; } bgStrategies[bg->GetInstanceID()] = data; } void OnBattlegroundEnd(Battleground* bg, TeamId /*winnerTeam*/) override { bgStrategies.erase(bg->GetInstanceID()); } }; void AddPlayerbotsScripts() { new PlayerbotsDatabaseScript(); new PlayerbotsPlayerScript(); new PlayerbotsMiscScript(); new PlayerbotsServerScript(); new PlayerbotsWorldScript(); new PlayerbotsScript(); new PlayerBotsBGScript(); AddSC_playerbots_commandscript(); }