diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 1b83542c..aeb6de17 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -41,7 +41,7 @@ # MAGE # WARLOCK # DRUID -# RANDOM BOT DEFAULT TALENT SPEC +# RANDOM BOT DEFAULT TALENT SPEC # WARRIOR # PALADIN # HUNTER @@ -54,7 +54,7 @@ # DRUID # PLAYERBOT SYSTEM SETTINGS # DATABASE & CONNECTIONS -# DEBUG +# DEBUG # CHAT SETTINGS # LOGS # DEPRECIATED (TEMPORARY) @@ -132,6 +132,9 @@ AiPlayerbot.AllowAccountBots = 1 # Allow/deny bots in the player's guild AiPlayerbot.AllowGuildBots = 1 +# Allow linking accounts for shared alt-bot control +AiPlayerbot.AllowTrustedAccountBots = 1 + # Random bot guild count AiPlayerbot.RandomBotGuildCount = 20 @@ -790,7 +793,7 @@ AiPlayerbot.RandomBotAutoJoinBG = 0 # Known issue: When enabling a lot of brackats in combination with multiple instances, # can lead to more instances created by bots than intended (over-queuing). # -# This section controls the level brackets and +# This section controls the level brackets and # automatic bot participation in battlegrounds and arenas. # # Brackets: @@ -1179,7 +1182,7 @@ AiPlayerbot.PremadeSpecLink.11.3.80 = -553202032322010053100030310511-205503012 ################################### #################################################################################################### -# +# # # @@ -1231,7 +1234,7 @@ AiPlayerbot.WorldBuff.0.11.3.80.80 = 53760,57358 #DRUID FERAL ################################### #################################################################################################### -# +# # # diff --git a/data/sql/playerbots/base/playerbotds_account_keys.sql b/data/sql/playerbots/base/playerbotds_account_keys.sql new file mode 100644 index 00000000..97c239ed --- /dev/null +++ b/data/sql/playerbots/base/playerbotds_account_keys.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS `playerbot_account_keys`; + +CREATE TABLE `playerbot_account_keys` ( + `account_id` INT PRIMARY KEY, + `security_key` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=INNODB DEFAULT CHARSET=latin1; diff --git a/data/sql/playerbots/base/playerbots_account_links.sql b/data/sql/playerbots/base/playerbots_account_links.sql new file mode 100644 index 00000000..590e4825 --- /dev/null +++ b/data/sql/playerbots/base/playerbots_account_links.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `playerbot_account_links`; + +CREATE TABLE `playerbot_account_links` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `account_id` INT NOT NULL, + `linked_account_id` INT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `account_link` (`account_id`, `linked_account_id`) +) ENGINE=INNODB DEFAULT CHARSET=latin1; diff --git a/data/sql/playerbots/updates/db_playerbots/2025_05_05_00_accountlinking.sql b/data/sql/playerbots/updates/db_playerbots/2025_05_05_00_accountlinking.sql new file mode 100644 index 00000000..53e74acc --- /dev/null +++ b/data/sql/playerbots/updates/db_playerbots/2025_05_05_00_accountlinking.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS `playerbot_account_links`; + +CREATE TABLE `playerbot_account_links` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `account_id` INT NOT NULL, + `linked_account_id` INT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `account_link` (`account_id`, `linked_account_id`) +) ENGINE=INNODB DEFAULT CHARSET=latin1; + +DROP TABLE IF EXISTS `playerbot_account_keys`; + +CREATE TABLE `playerbot_account_keys` ( + `account_id` INT PRIMARY KEY, + `security_key` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=INNODB DEFAULT CHARSET=latin1; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index ab62ba09..e665ce06 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -128,6 +128,7 @@ bool PlayerbotAIConfig::Initialize() allowAccountBots = sConfigMgr->GetOption("AiPlayerbot.AllowAccountBots", true); allowGuildBots = sConfigMgr->GetOption("AiPlayerbot.AllowGuildBots", true); + allowTrustedAccountBots = sConfigMgr->GetOption("AiPlayerbot.AllowTrustedAccountBots", true); randomBotGuildNearby = sConfigMgr->GetOption("AiPlayerbot.RandomBotGuildNearby", false); randomBotInvitePlayer = sConfigMgr->GetOption("AiPlayerbot.RandomBotInvitePlayer", false); inviteChat = sConfigMgr->GetOption("AiPlayerbot.InviteChat", false); @@ -554,7 +555,7 @@ bool PlayerbotAIConfig::Initialize() { sRandomPlayerbotMgr->Init(); } - + sRandomItemMgr->Init(); sRandomItemMgr->InitAfterAhBot(); sPlayerbotTextMgr->LoadBotTexts(); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 325c174a..59799699 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -55,7 +55,7 @@ public: bool IsInPvpProhibitedArea(uint32 id); bool enabled; - bool allowAccountBots, allowGuildBots; + bool allowAccountBots, allowGuildBots, allowTrustedAccountBots; bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat; uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime, dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay; diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 13863ecb..21e5aa66 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include "ChannelMgr.h" #include "CharacterCache.h" @@ -93,21 +95,22 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(playerGuid); if (!accountId) return; - + WorldSession* masterSession = masterAccountId ? sWorldSessionMgr->FindSession(masterAccountId) : nullptr; Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr; bool isRndbot = !masterAccountId; bool sameAccount = sPlayerbotAIConfig->allowAccountBots && accountId == masterAccountId; Guild* guild = masterPlayer ? sGuildMgr->GetGuildById(masterPlayer->GetGuildId()) : nullptr; - bool sameGuild = sPlayerbotAIConfig->allowGuildBots && guild && guild->GetMember(playerGuid); + bool sameGuild = sPlayerbotAIConfig->allowGuildBots && guild && guild->GetMember(playerGuid); bool addClassBot = sRandomPlayerbotMgr->IsAddclassBot(playerGuid.GetCounter()); + bool linkedAccount = sPlayerbotAIConfig->allowTrustedAccountBots && IsAccountLinked(accountId, masterAccountId); bool allowed = true; std::ostringstream out; std::string botName; sCharacterCache->GetCharacterNameByGuid(playerGuid, botName); - if (!isRndbot && !sameAccount && !sameGuild && !addClassBot) + if (!isRndbot && !sameAccount && !sameGuild && !addClassBot && !linkedAccount) { allowed = false; out << "Failure: You are not allowed to control bot " << botName.c_str(); @@ -144,13 +147,20 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId } botLoading.insert(playerGuid); - + // Always login in with world session to avoid race condition sWorld->AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder)) .AfterComplete([this](SQLQueryHolderBase const& holder) { HandlePlayerBotLoginCallback(static_cast(holder)); }); } +bool PlayerbotHolder::IsAccountLinked(uint32 accountId, uint32 linkedAccountId) +{ + QueryResult result = PlayerbotsDatabase.Query( + "SELECT 1 FROM playerbot_account_links WHERE account_id = {} AND linked_account_id = {}", accountId, linkedAccountId); + return result != nullptr; +} + void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder const& holder) { uint32 botAccountId = holder.GetAccountId(); @@ -181,7 +191,7 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con { LOG_DEBUG("mod-playerbots", "Master session found but no player is associated for master account ID: {}", masterAccount); } - + sRandomPlayerbotMgr->OnPlayerLogin(bot); OnBotLogin(bot); @@ -425,7 +435,7 @@ void PlayerbotHolder::DisablePlayerBot(ObjectGuid guid) void PlayerbotHolder::RemoveFromPlayerbotsMap(ObjectGuid guid) { - playerBots.erase(guid); + playerBots.erase(guid); } Player* PlayerbotHolder::GetPlayerBot(ObjectGuid playerGuid) const @@ -451,7 +461,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) sPlayerbotsMgr->AddPlayerbotData(bot, true); playerBots[bot->GetGUID()] = bot; - + OnBotLoginInternal(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); @@ -674,8 +684,11 @@ std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, Obje if (!accountId) return "character not found"; - if (!sPlayerbotAIConfig->allowAccountBots && accountId != masterAccountId) - return "you can only add bots from your own account"; + if (!sPlayerbotAIConfig->allowAccountBots && accountId != masterAccountId && + !(sPlayerbotAIConfig->allowTrustedAccountBots && IsAccountLinked(accountId, masterAccountId))) + { + return "you can only add bots from your own account or linked accounts"; + } } AddPlayerBot(guid, masterAccountId); @@ -701,12 +714,12 @@ std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, Obje if (!bot) return "bot not found"; - + bool addClassBot = sRandomPlayerbotMgr->IsAddclassBot(guid.GetCounter()); if (!addClassBot) return "ERROR: You can not use this command on non-addclass bot."; - + if (!admin) { Player* master = ObjectAccessor::FindConnectedPlayer(masterguid); @@ -732,7 +745,7 @@ std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, Obje { return "Initialization already in progress, please wait."; } - + int gs; if (cmd == "init=white" || cmd == "init=common") { @@ -1705,3 +1718,121 @@ PlayerbotMgr* PlayerbotsMgr::GetPlayerbotMgr(Player* player) return nullptr; } + +void PlayerbotMgr::HandleSetSecurityKeyCommand(Player* player, const std::string& key) +{ + uint32 accountId = player->GetSession()->GetAccountId(); + + // Hash the security key using SHA-256 + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256((unsigned char*)key.c_str(), key.size(), hash); + + // Convert the hash to a hexadecimal string + std::ostringstream hashedKey; + for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) + hashedKey << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i]; + + // Store the hashed key in the database + PlayerbotsDatabase.Execute( + "REPLACE INTO playerbot_account_keys (account_id, security_key) VALUES ({}, '{}')", + accountId, hashedKey.str()); + + ChatHandler(player->GetSession()).PSendSysMessage("Security key set successfully."); +} + +void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& accountName, const std::string& key) +{ + QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName); + if (!result) + { + ChatHandler(player->GetSession()).PSendSysMessage("Account not found."); + return; + } + + Field* fields = result->Fetch(); + uint32 linkedAccountId = fields[0].Get(); + + result = PlayerbotsDatabase.Query("SELECT security_key FROM playerbot_account_keys WHERE account_id = {}", linkedAccountId); + if (!result) + { + ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key."); + return; + } + + // Hash the provided key + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256((unsigned char*)key.c_str(), key.size(), hash); + + // Convert the hash to a hexadecimal string + std::ostringstream hashedKey; + for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) + hashedKey << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i]; + + // Compare the hashed key with the stored hashed key + std::string storedKey = result->Fetch()->Get(); + if (hashedKey.str() != storedKey) + { + ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key."); + return; + } + + uint32 accountId = player->GetSession()->GetAccountId(); + PlayerbotsDatabase.Execute( + "INSERT IGNORE INTO playerbot_account_links (account_id, linked_account_id) VALUES ({}, {})", + accountId, linkedAccountId); + PlayerbotsDatabase.Execute( + "INSERT IGNORE INTO playerbot_account_links (account_id, linked_account_id) VALUES ({}, {})", + linkedAccountId, accountId); + + ChatHandler(player->GetSession()).PSendSysMessage("Account linked successfully."); +} + +void PlayerbotMgr::HandleViewLinkedAccountsCommand(Player* player) +{ + uint32 accountId = player->GetSession()->GetAccountId(); + QueryResult result = PlayerbotsDatabase.Query("SELECT linked_account_id FROM playerbot_account_links WHERE account_id = {}", accountId); + + if (!result) + { + ChatHandler(player->GetSession()).PSendSysMessage("No linked accounts."); + return; + } + + ChatHandler(player->GetSession()).PSendSysMessage("Linked accounts:"); + do + { + Field* fields = result->Fetch(); + uint32 linkedAccountId = fields[0].Get(); + + QueryResult accountResult = LoginDatabase.Query("SELECT username FROM account WHERE id = {}", linkedAccountId); + if (accountResult) + { + Field* accountFields = accountResult->Fetch(); + std::string username = accountFields[0].Get(); + ChatHandler(player->GetSession()).PSendSysMessage("- {}", username.c_str()); + } + else + { + ChatHandler(player->GetSession()).PSendSysMessage("- Unknown account"); + } + } while (result->NextRow()); +} + +void PlayerbotMgr::HandleUnlinkAccountCommand(Player* player, const std::string& accountName) +{ + QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName); + if (!result) + { + ChatHandler(player->GetSession()).PSendSysMessage("Account not found."); + return; + } + + Field* fields = result->Fetch(); + uint32 linkedAccountId = fields[0].Get(); + uint32 accountId = player->GetSession()->GetAccountId(); + + PlayerbotsDatabase.Execute("DELETE FROM playerbot_account_links WHERE (account_id = {} AND linked_account_id = {}) OR (account_id = {} AND linked_account_id = {})", + accountId, linkedAccountId, linkedAccountId, accountId); + + ChatHandler(player->GetSession()).PSendSysMessage("Account unlinked successfully."); +} diff --git a/src/PlayerbotMgr.h b/src/PlayerbotMgr.h index 5c62c999..5ef31776 100644 --- a/src/PlayerbotMgr.h +++ b/src/PlayerbotMgr.h @@ -28,6 +28,7 @@ public: virtual ~PlayerbotHolder(){}; void AddPlayerBot(ObjectGuid guid, uint32 masterAccountId); + bool IsAccountLinked(uint32 accountId, uint32 masterAccountId); void HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder const& holder); void LogoutPlayerBot(ObjectGuid guid); @@ -82,6 +83,11 @@ public: void SaveToDB(); + void HandleSetSecurityKeyCommand(Player* player, const std::string& key); + void HandleLinkAccountCommand(Player* player, const std::string& accountName, const std::string& key); + void HandleViewLinkedAccountsCommand(Player* player); + void HandleUnlinkAccountCommand(Player* player, const std::string& accountName); + protected: void OnBotLoginInternal(Player* const bot) override; void CheckTellErrors(uint32 elapsed); diff --git a/src/cs_playerbots.cpp b/src/cs_playerbots.cpp index e39b7f33..b0b7aa9a 100644 --- a/src/cs_playerbots.cpp +++ b/src/cs_playerbots.cpp @@ -33,12 +33,21 @@ public: static ChatCommandTable playerbotsDebugCommandTable = { {"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes}, }; + + static ChatCommandTable playerbotsAccountCommandTable = { + {"setKey", HandleSetSecurityKeyCommand, SEC_PLAYER, Console::No}, + {"link", HandleLinkAccountCommand, SEC_PLAYER, Console::No}, + {"linkedAccounts", HandleViewLinkedAccountsCommand, SEC_PLAYER, Console::No}, + {"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No}, + }; + static ChatCommandTable playerbotsCommandTable = { {"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No}, {"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes}, {"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes}, {"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes}, {"debug", playerbotsDebugCommandTable}, + {"account", playerbotsAccountCommandTable}, }; static ChatCommandTable commandTable = { @@ -101,6 +110,103 @@ public: { return BGTactics::HandleConsoleCommand(handler, args); } + + static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args) + { + if (!args || !*args) + { + handler->PSendSysMessage("Usage: .playerbots account setKey "); + return false; + } + + Player* player = handler->GetSession()->GetPlayer(); + std::string key = args; + + PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player); + if (mgr) + { + mgr->HandleSetSecurityKeyCommand(player, key); + return true; + } + else + { + handler->PSendSysMessage("PlayerbotMgr instance not found."); + return false; + } + } + + static bool HandleLinkAccountCommand(ChatHandler* handler, char const* args) + { + if (!args || !*args) + return false; + + char* accountName = strtok((char*)args, " "); + char* key = strtok(nullptr, " "); + + if (!accountName || !key) + { + handler->PSendSysMessage("Usage: .playerbots account link "); + return false; + } + + Player* player = handler->GetSession()->GetPlayer(); + + PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player); + if (mgr) + { + mgr->HandleLinkAccountCommand(player, accountName, key); + return true; + } + else + { + handler->PSendSysMessage("PlayerbotMgr instance not found."); + return false; + } + } + + static bool HandleViewLinkedAccountsCommand(ChatHandler* handler, char const* /*args*/) + { + Player* player = handler->GetSession()->GetPlayer(); + + PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player); + if (mgr) + { + mgr->HandleViewLinkedAccountsCommand(player); + return true; + } + else + { + handler->PSendSysMessage("PlayerbotMgr instance not found."); + return false; + } + } + + static bool HandleUnlinkAccountCommand(ChatHandler* handler, char const* args) + { + if (!args || !*args) + return false; + + char* accountName = strtok((char*)args, " "); + if (!accountName) + { + handler->PSendSysMessage("Usage: .playerbots account unlink "); + return false; + } + + Player* player = handler->GetSession()->GetPlayer(); + + PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player); + if (mgr) + { + mgr->HandleUnlinkAccountCommand(player, accountName); + return true; + } + else + { + handler->PSendSysMessage("PlayerbotMgr instance not found."); + return false; + } + } }; void AddSC_playerbots_commandscript() { new playerbots_commandscript(); }