From 9be29af049155f13c17f214fddb906f496207708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A3=99=E4=B8=8B=E5=AD=A4=E9=AD=82?= <46797568+zhangbo8418@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:49:15 +0800 Subject: [PATCH 1/3] Fix random bots stuck in group on player logout; add leader transfer and delayed leave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Prevents random bots from staying in a group when their master logs out, which made them uninvitable on re-login. - When a real player leaves a party that still has other real players, the party is kept: leader is transferred to the next real player and bots get a new master. The party only breaks when the last real player leaves. - When no real player remains in the party, random bots leave after a configurable delay (default 180s) so brief disconnects don’t disband the group. - After a server crash/restart, groups are restored from DB; if a random bot logs in and its group has no connected real player, that group is scheduled for delayed leave so bots don’t stay stuck. ## Changes - **OnPlayerLogout**: For each random bot that had the logging-out player as master, either assign a new master (first connected real player in the group) and transfer party leader to that player, or schedule delayed leave and clear master. If the core did not remove the player on logout (`LeaveGroupOnLogout=0`), transfer leader when possible, then remove the player from the group. - **Delayed leave**: New config `AiPlayerbot.BotLeaveGroupDelayWhenNoRealPlayer` (default 180 seconds). Groups with no real player are scheduled; each tick `ProcessScheduledGroupLeaves` re-checks for a connected real player and, if still none, only **random bots** in that group are forced to leave (alt/addclass bots are not kicked). Delay 0 means leave on the next tick. - **OnBotLoginInternal**: When a random bot logs in and is in a group with no connected real player, schedule that group for delayed leave (avoids stuck/uninvitable bots after crash). - **Scope**: Only 5-man parties are handled; raid, LFG, and battleground are skipped. Logic applies only to random bots managed by RandomPlayerbotMgr. ## Config - `AiPlayerbot.BotLeaveGroupDelayWhenNoRealPlayer` (default: 180) — seconds before random bots leave the group when no real player remains. ## Notes - If `LeaveGroupOnLogout` is enabled in the core, the core removes the player and broadcasts first; bots may leave immediately via `PartyCommandAction(PARTY_OP_LEAVE)`, so the delay does not apply in that case. When `LeaveGroupOnLogout` is disabled, we transfer leader and remove the player ourselves, and the delay applies. - Defensive checks added: `FindFirstRealConnectedPlayerInGroup` validates `GetSession()` and skips logout/invalid members to avoid crashes. --- conf/playerbots.conf.dist | 4 + src/Bot/RandomPlayerbotMgr.cpp | 166 +++++++++++++++++++++++++++++++++ src/Bot/RandomPlayerbotMgr.h | 7 ++ src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 2 + 5 files changed, 180 insertions(+) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 9073773167..226b01dd9b 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -110,6 +110,10 @@ AiPlayerbot.DisabledWithoutRealPlayer = 0 AiPlayerbot.DisabledWithoutRealPlayerLoginDelay = 30 AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300 +# When the last real player leaves a group, bots leave after this delay (seconds) to avoid disband on accidental disconnect +# Default: 180 +AiPlayerbot.BotLeaveGroupDelayWhenNoRealPlayer = 180 + #################################################################################################### ################################### diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 892368c66f..1cdc62b580 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -45,6 +45,9 @@ #include "TravelMgr.h" #include "Unit.h" #include "World.h" +#include "WorldConfig.h" +#include "Group.h" +#include "GroupMgr.h" #include "Cell.h" #include "GridNotifiers.h" // Required for Cell because of poor AC implementation @@ -333,6 +336,8 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) if (!sPlayerbotAIConfig.randomBotAutologin || !sPlayerbotAIConfig.enabled) return; + ProcessScheduledGroupLeaves(); + /*if (sPlayerbotAIConfig.enablePrototypePerformanceDiff) { LOG_INFO("playerbots", "---------------------------------------"); @@ -2962,10 +2967,112 @@ void RandomPlayerbotMgr::HandleCommand(uint32 type, std::string const text, Play } } +// ============================================================================ +// Group and leader logic (applies only to random bots, not alt/addclass etc.) +// ============================================================================ +// +// 1) Player logout (OnPlayerLogout) +// - Only 5-man parties are handled; raid/LFG/battleground are skipped. +// - Step1: For each random bot in RandomPlayerbotMgr that had this player as master: +// · If there is another connected real player in the group -> transfer leader to that player and set bot's master to them; +// · Otherwise -> schedule delayed leave for the group and set bot's master to nullptr. +// - Step2: If the core did not remove the player on logout (LeaveGroupOnLogout=0) and the player is still in the group: +// · If there is another connected real player -> transfer leader to them; +// · Otherwise -> schedule delayed leave for the group; +// · Finally remove the logging-out player from the group (RemoveMember). +// - Order vs core: When LeaveGroupOnLogout=1, the core removes the player and broadcasts first; bots may leave +// immediately in PartyCommandAction(PARTY_OP_LEAVE), so the delay does not apply. When LeaveGroupOnLogout=0, +// we set master and then RemoveMember, so bots do not leave in the packet handler and the delay applies. +// +// 2) Delayed leave (ScheduleGroupDelayedLeave + ProcessScheduledGroupLeaves) +// - Schedule: group GUID -> leaveAt = now + BotLeaveGroupDelayWhenNoRealPlayer (0 = next tick). +// - Each tick in UpdateAIInternal, ProcessScheduledGroupLeaves runs: +// · If now >= leaveAt: re-check if the group has any connected real player; +// · If still none: only random bots in that group leave (IsRandomBot); alt/addclass are not kicked. +// +// 3) Crash/restart (OnBotLoginInternal) +// - When a random bot logs in, if it is in a group and the group has no connected real player -> schedule +// delayed leave for that group so bots do not stay stuck and uninvitable. +// +// Find first real (non-bot) connected player in group, excluding excludePlayer +static Player* FindFirstRealConnectedPlayerInGroup(Group* group, Player* excludePlayer) +{ + if (!group) + return nullptr; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == excludePlayer) + continue; + if (!member->GetSession()) + continue; + if (!member->IsInWorld() || member->GetSession()->PlayerLogout()) + continue; + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (memberAI && !memberAI->IsRealPlayer()) + continue; + return member; + } + return nullptr; +} + void RandomPlayerbotMgr::OnPlayerLogout(Player* player) { + // When CONFIG_LEAVE_GROUP_ON_LOGOUT is true, the core removes the player from the group before this hook + // and sends group updates; bots may then leave immediately in LeaveGroupAction::PartyCommandAction (PARTY_OP_LEAVE). + // When it is false, we transfer leader / remove player ourselves below and set bot master to nullptr before + // RemoveMember, so bots do not leave in PartyCommandAction and the scheduled delay applies. + + // 1. For each bot that had this player as master: assign new master or schedule group leave + for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it) + { + Player* const bot = it->second; + PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (!botAI || botAI->GetMaster() != player) + continue; + + Group* group = bot->GetGroup(); + if (!group || group->isRaidGroup() || group->isLFGGroup() || bot->InBattleground()) + { + botAI->SetMaster(nullptr); + if (!bot->InBattleground()) + botAI->ResetStrategies(); + continue; + } + + Player* newMaster = FindFirstRealConnectedPlayerInGroup(group, player); + if (newMaster) + { + // Always set leader to real player (core may have picked a bot when LeaveGroupOnLogout removed player first) + if (group->GetLeaderGUID() != newMaster->GetGUID()) + group->ChangeLeader(newMaster->GetGUID()); + botAI->SetMaster(newMaster); + } + else + { + ScheduleGroupDelayedLeave(group); + botAI->SetMaster(nullptr); + if (!bot->InBattleground()) + botAI->ResetStrategies(); + } + } + + // 2. If player is still in group (core did not remove on logout), transfer leader then remove + Group* group = player->GetGroup(); + if (group && !group->isRaidGroup() && !group->isLFGGroup() && !player->InBattleground()) + { + Player* newLeader = FindFirstRealConnectedPlayerInGroup(group, player); + if (newLeader && group->GetLeaderGUID() != newLeader->GetGUID()) + group->ChangeLeader(newLeader->GetGUID()); + else if (!newLeader) + ScheduleGroupDelayedLeave(group); + if (!sWorld->getBoolConfig(CONFIG_LEAVE_GROUP_ON_LOGOUT)) + group->RemoveMember(player->GetGUID()); + } + DisablePlayerBot(player->GetGUID()); + // 3. Fallback: clear master and reset strategies for bots not handled in Step1 (no group / raid/BG/LFG) for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it) { Player* const bot = it->second; @@ -2985,6 +3092,56 @@ void RandomPlayerbotMgr::OnPlayerLogout(Player* player) players.erase(i); } +void RandomPlayerbotMgr::ScheduleGroupDelayedLeave(Group* group) +{ + if (!group) + return; + // Delay 0 = leave on next ProcessScheduledGroupLeaves tick (avoids bots stuck in group when config is 0) + time_t leaveAt = time(nullptr) + sPlayerbotAIConfig.botLeaveGroupDelayWhenNoRealPlayer; + m_groupsScheduledToLeave[group->GetGUID().GetCounter()] = leaveAt; +} + +void RandomPlayerbotMgr::ProcessScheduledGroupLeaves() +{ + time_t now = time(nullptr); + for (auto it = m_groupsScheduledToLeave.begin(); it != m_groupsScheduledToLeave.end();) + { + if (now < it->second) + { + ++it; + continue; + } + ObjectGuid::LowType groupGuidLow = it->first; + it = m_groupsScheduledToLeave.erase(it); + + Group* group = sGroupMgr->GetGroupByGUID(groupGuidLow); + if (!group || group->isLFGGroup()) + continue; + if (FindFirstRealConnectedPlayerInGroup(group, nullptr)) + continue; // Cancel leave if a real player joined during the delay + + // Only force random bots to leave; do not kick alt/addclass + std::vector botsToLeave; + for (auto const& slot : group->GetMemberSlots()) + { + Player* member = ObjectAccessor::FindPlayer(slot.guid); + if (!member || member->InBattleground()) + continue; + if (!IsRandomBot(member)) + continue; + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (memberAI && !memberAI->IsRealPlayer()) + botsToLeave.push_back(member); + } + for (Player* bot : botsToLeave) + { + PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (botAI) + botAI->LeaveOrDisbandGroup(); + } + } +} + void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot) { if (_isBotLogging) @@ -3006,6 +3163,15 @@ void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot) { bot->RemovePlayerFlag(PLAYER_FLAGS_NO_XP_GAIN); } + + // After server crash/restart, group is restored from DB but leader may be offline or group may have no real player + // → schedule delayed leave so bots don't stay stuck (and become uninvitable) if no real player logs in + Group* group = bot->GetGroup(); + if (group && !bot->InBattleground() && !group->isLFGGroup()) + { + if (!FindFirstRealConnectedPlayerInGroup(group, nullptr)) + ScheduleGroupDelayedLeave(group); + } } void RandomPlayerbotMgr::OnPlayerLogin(Player* player) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index 94c0a01513..a8c5272021 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -12,6 +12,8 @@ #include "GameTime.h" #include "PlayerbotCommandServer.h" +class Group; + struct BattlegroundInfo { std::vector bgInstances; @@ -266,6 +268,11 @@ class RandomPlayerbotMgr : public PlayerbotHolder uint32 bgBotsCount; uint32 playersLevel; + // Groups scheduled for bot leave when no real player remains (groupGuidLow -> leave at time) + std::map m_groupsScheduledToLeave; + void ScheduleGroupDelayedLeave(Group* group); + void ProcessScheduledGroupLeaves(); + // Account lists std::vector rndBotTypeAccounts; // Accounts marked as RNDbot (type 1) std::vector addClassTypeAccounts; // Accounts marked as AddClass (type 2) diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index b511369484..c1ce3adc89 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -86,6 +86,7 @@ bool PlayerbotAIConfig::Initialize() rpWarningCooldown = sConfigMgr->GetOption("AiPlayerbot.RPWarningCooldown", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); + botLeaveGroupDelayWhenNoRealPlayer = sConfigMgr->GetOption("AiPlayerbot.BotLeaveGroupDelayWhenNoRealPlayer", 180); farDistance = sConfigMgr->GetOption("AiPlayerbot.FarDistance", 20.0f); sightDistance = sConfigMgr->GetOption("AiPlayerbot.SightDistance", 100.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 729fc5be16..836b49c8b5 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -140,6 +140,8 @@ class PlayerbotAIConfig uint32 randomBotsPerInterval; uint32 minRandomBotsPriceChangeInterval, maxRandomBotsPriceChangeInterval; uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay; + // Delay (seconds) before bots leave group when no real player remains (avoids disband on accidental disconnect) + uint32 botLeaveGroupDelayWhenNoRealPlayer; bool randomBotJoinLfg; // Buff system From 4b7b01aeaff6ba4d65ca2e72de5dbd4b8ac92c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A3=99=E4=B8=8B=E5=AD=A4=E9=AD=82?= <46797568+zhangbo8418@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:46:21 +0800 Subject: [PATCH 2/3] fix(playerbots): Respect BotLeaveGroupDelayWhenNoRealPlayer in LeaveGroupAction --- src/Ai/Base/Actions/LeaveGroupAction.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Ai/Base/Actions/LeaveGroupAction.cpp b/src/Ai/Base/Actions/LeaveGroupAction.cpp index 03a24bd199..2b64fe7693 100644 --- a/src/Ai/Base/Actions/LeaveGroupAction.cpp +++ b/src/Ai/Base/Actions/LeaveGroupAction.cpp @@ -8,6 +8,7 @@ #include "Event.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "RandomPlayerbotMgr.h" bool LeaveGroupAction::Execute(Event event) { @@ -42,6 +43,13 @@ bool PartyCommandAction::Execute(Event event) return false; } } + + if (sPlayerbotAIConfig.botLeaveGroupDelayWhenNoRealPlayer > 0) + { + sRandomPlayerbotMgr.ScheduleGroupDelayedLeave(bot->GetGroup()); + return false; + } + return Leave(); } return false; From bb3a246c341f997a52fba57b9157dbed612c5b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A3=99=E4=B8=8B=E5=AD=A4=E9=AD=82?= <46797568+zhangbo8418@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:23:40 +0800 Subject: [PATCH 3/3] Add ScheduleGroupDelayedLeave method declaration --- src/Bot/RandomPlayerbotMgr.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index a8c5272021..c43864b345 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -190,6 +190,8 @@ class RandomPlayerbotMgr : public PlayerbotHolder void AssignAccountTypes(); bool IsAccountType(uint32 accountId, uint8 accountType); + void ScheduleGroupDelayedLeave(Group* group); + protected: void OnBotLoginInternal(Player* const bot) override; @@ -270,7 +272,6 @@ class RandomPlayerbotMgr : public PlayerbotHolder // Groups scheduled for bot leave when no real player remains (groupGuidLow -> leave at time) std::map m_groupsScheduledToLeave; - void ScheduleGroupDelayedLeave(Group* group); void ProcessScheduledGroupLeaves(); // Account lists