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/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; 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..c43864b345 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; @@ -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; @@ -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 m_groupsScheduledToLeave; + 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