Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions conf/playerbots.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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

####################################################################################################

###################################
Expand Down
8 changes: 8 additions & 0 deletions src/Ai/Base/Actions/LeaveGroupAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "Event.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "RandomPlayerbotMgr.h"

bool LeaveGroupAction::Execute(Event event)
{
Expand Down Expand Up @@ -42,6 +43,13 @@ bool PartyCommandAction::Execute(Event event)
return false;
}
}

if (sPlayerbotAIConfig.botLeaveGroupDelayWhenNoRealPlayer > 0)
{
sRandomPlayerbotMgr.ScheduleGroupDelayedLeave(bot->GetGroup());
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delay blocks non-random bots from ever leaving group

Medium Severity

The delayed-leave check in PartyCommandAction::Execute is placed outside the IsRandomBot guard, so when botLeaveGroupDelayWhenNoRealPlayer > 0 (default 180), all bot types — including alt/addclass bots — hit return false instead of reaching return Leave(). However, ProcessScheduledGroupLeaves explicitly skips non-random bots (!IsRandomBot(member)), meaning those bots are never actually removed and stay stuck in the group indefinitely. This happens when a real player manually leaves a group containing their alt/addclass bots.

Additional Locations (1)

Fix in Cursor Fix in Web


return Leave();
}
return false;
Expand Down
166 changes: 166 additions & 0 deletions src/Bot/RandomPlayerbotMgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -333,6 +336,8 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/)
if (!sPlayerbotAIConfig.randomBotAutologin || !sPlayerbotAIConfig.enabled)
return;

ProcessScheduledGroupLeaves();

/*if (sPlayerbotAIConfig.enablePrototypePerformanceDiff)
{
LOG_INFO("playerbots", "---------------------------------------");
Expand Down Expand Up @@ -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;
Expand All @@ -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<Player*> 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)
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions src/Bot/RandomPlayerbotMgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "GameTime.h"
#include "PlayerbotCommandServer.h"

class Group;

struct BattlegroundInfo
{
std::vector<uint32> bgInstances;
Expand Down Expand Up @@ -188,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;

Expand Down Expand Up @@ -266,6 +270,10 @@ class RandomPlayerbotMgr : public PlayerbotHolder
uint32 bgBotsCount;
uint32 playersLevel;

// Groups scheduled for bot leave when no real player remains (groupGuidLow -> leave at time)
std::map<ObjectGuid::LowType, time_t> m_groupsScheduledToLeave;
void ProcessScheduledGroupLeaves();

// Account lists
std::vector<uint32> rndBotTypeAccounts; // Accounts marked as RNDbot (type 1)
std::vector<uint32> addClassTypeAccounts; // Accounts marked as AddClass (type 2)
Expand Down
1 change: 1 addition & 0 deletions src/PlayerbotAIConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ bool PlayerbotAIConfig::Initialize()
rpWarningCooldown = sConfigMgr->GetOption<int32>("AiPlayerbot.RPWarningCooldown", 30);
disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30);
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
botLeaveGroupDelayWhenNoRealPlayer = sConfigMgr->GetOption<uint32>("AiPlayerbot.BotLeaveGroupDelayWhenNoRealPlayer", 180);

farDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FarDistance", 20.0f);
sightDistance = sConfigMgr->GetOption<float>("AiPlayerbot.SightDistance", 100.0f);
Expand Down
2 changes: 2 additions & 0 deletions src/PlayerbotAIConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down