Add deterministic AdventureBot group formation manager
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-11-01 01:20:37 +01:00
1 parent 856832e7bd
commit be049baacc
11 files changed
+386

No files matched your search

@@ -1695,6 +1695,10 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
{
std::string args;std::getline(in,args);return EditPlan(handler,args);
}
if(op=="groupmanager")
{
std::string args;std::getline(in,args);return sAdventureGroupMgr->Command(handler,args.c_str());
}
if (op == "import") return sAdventureLocationMgr->Import(handler);
if (op == "repairimport") return sAdventureLocationMgr->Import(handler,true);
if (op == "reload")
+185
View File
@@ -31,6 +31,7 @@
#include "GroupMgr.h"
#include "LFGPackets.h"
#include <algorithm>
#include <sstream>
namespace
{
@@ -339,6 +340,189 @@ bool AdventureGroupMgr::Merge(Player* p,AdventureGroupPlan const& plan)
}
return true; // A mutation occurred; caller must not reuse its old Group pointer.
}
bool AdventureGroupMgr::ManagedTransfer(Player* leader,Player* member)
{
if(!leader || !member || leader==member || !Ready(leader) || !Ready(member) ||
leader->GetTeam()!=member->GetTeam() || !CanInvite(leader,member) || !NeedsRole(leader,member))return false;
auto target=leader->GetGroup(),source=member->GetGroup();
ObjectGuid originalLeader=source?source->GetLeaderGUID():ObjectGuid::Empty;
if(target && target->GetLeaderGUID()!=leader->GetGUID())return false;
if(source==target)return false;
if(source)
{
if(!BotLed(member) || source->isLFGGroup())return false;
if(source->GetLeaderGUID()==member->GetGUID() && source->GetMembersCount()>1)
{
Player* successor=nullptr;
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
if(auto candidate=ObjectAccessor::FindPlayer(slot.guid))
if(candidate->GetSession() && candidate->GetSession()->IsClientless()){successor=candidate;break;}
if(!successor)return false; // Never hand forced leadership to a real player.
source->ChangeLeader(successor->GetGUID());source->SendUpdate();
}
}
CancelBrowser(member);
ObjectGuid old=source?source->GetGUID():ObjectGuid::Empty;
ObjectGuid oldLeader=originalLeader;
bool oldRaid=source && source->isRaidGroup();
auto oldDungeonMode=source?source->GetDungeonDifficulty():DUNGEON_DIFFICULTY_NORMAL;
auto oldRaidMode=source?source->GetRaidDifficulty():RAID_DIFFICULTY_10MAN_NORMAL;
std::vector<ObjectGuid> oldMembers;
if(source)for(auto const& slot:source->GetMemberSlots())oldMembers.push_back(slot.guid);
if(source)source->RemoveMember(member->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
Invite(leader,member,Activity(sAdventureBehaviorMgr->GroupPlan(leader)),true);
if(member->GetGroupInvite())
{
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};
response.Accept=true;member->GetSession()->HandlePartyInviteResponseOpcode(response);
}
if(member->GetGroup() && member->GetGroup()==leader->GetGroup())
{
_records[member->GetGUID()].MergeAt=GameTime::GetGameTime()+120;
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_TRANSFER member=%u from=%s leader=%u target='%s'",
member->GetGUID().GetCounter(),old.ToString().c_str(),leader->GetGUID().GetCounter(),
sAdventureBehaviorMgr->GroupPlan(leader).NextLocation.c_str());
return true;
}
// Native invitation rejection must not strand a member removed from a
// still-existing source group. Reuse the same bounded rollback as organic
// merging, including recreation of a disbanded two-bot source.
if(old)
{
if(auto previous=sGroupMgr->GetGroupByGUID(old.GetCounter()))
{
previous->AddMember(member);
if(oldLeader==member->GetGUID())previous->ChangeLeader(member->GetGUID());
previous->SendUpdate();
}
else if(oldMembers.size()==2)
{
auto peer=ObjectAccessor::FindPlayer(oldMembers[0]==member->GetGUID()?oldMembers[1]:oldMembers[0]);
if(peer && !peer->GetGroup() && !peer->GetGroupInvite())
{
auto restored=new Group();auto owner=oldLeader==member->GetGUID()?member:peer;
if(restored->Create(owner))
{
sGroupMgr->AddGroup(restored);if(oldRaid)restored->ConvertToRaid();
restored->SetDungeonDifficulty(oldDungeonMode);restored->SetRaidDifficulty(oldRaidMode);
restored->AddMember(owner==member?peer:member);restored->SendUpdate();
}
else delete restored;
}
}
}
return false;
}
unsigned AdventureGroupMgr::RunFormationManager(ChatHandler* handler,bool apply,bool forced)
{
auto now=GameTime::GetGameTime();unsigned candidates=0,moves=0;
auto forming=[&](Player* p)
{
if(!Ready(p) || !p->GetSession()->IsClientless() || p->isUsingLfg() || p->InBattlegroundQueue() || p->GetGroupInvite())return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
if(!plan.Enabled || !plan.Forming || !AdventureGroupPolicy::Eligible(plan.NextIntent) || (!forced && !AdventureGroupPolicy::ManagerWindow(plan.Deadline,now)))return false;
if(auto group=p->GetGroup())
{
if(!BotLed(p) || group->isLFGGroup())return false;
if(Complete(p))return false;
}
return true;
};
std::vector<Player*> pool;
for(auto const& entry:_records)if(auto p=ObjectAccessor::FindPlayer(entry.first))if(forming(p))pool.push_back(p);
candidates=pool.size();
auto groupSize=[](Player* p){return p->GetGroup()?p->GetGroup()->GetMembersCount():1u;};
auto leaderOf=[](Player* p)->Player*
{
if(auto group=p->GetGroup())return ObjectAccessor::FindPlayer(group->GetLeaderGUID());
return p;
};
for(unsigned pass=0;pass<pool.size();++pass)
{
std::vector<Player*> leaders;
for(auto p:pool)if(forming(p))
{
auto leader=leaderOf(p);if(!leader || !forming(leader))continue;
if(std::find(leaders.begin(),leaders.end(),leader)==leaders.end())leaders.push_back(leader);
}
std::sort(leaders.begin(),leaders.end(),[&](Player* a,Player* b)
{auto sa=groupSize(a),sb=groupSize(b);return sa!=sb?sa>sb:a->GetGUID()<b->GetGUID();});
bool changed=false;
for(auto leader:leaders)
{
if(!forming(leader))continue;
auto targetPlan=sAdventureBehaviorMgr->GroupPlan(leader);auto targetGroup=leader->GetGroup();
if((targetGroup?targetGroup->GetMembersCount():1)>=Capacity(targetPlan))continue;
Player* best=nullptr;unsigned bestSource=999;
for(auto member:pool)
{
if(member==leader || !forming(member) || member->GetGroup()==targetGroup)continue;
auto memberPlan=sAdventureBehaviorMgr->GroupPlan(member);
if(Activity(memberPlan)!=Activity(targetPlan) || member->GetTeam()!=leader->GetTeam() ||
!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),leader->getLevel()))continue;
bool levels=true;if(targetGroup)for(auto const& slot:targetGroup->GetMemberSlots())
if(auto present=ObjectAccessor::FindPlayer(slot.guid))
if(!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),present->getLevel())){levels=false;break;}
if(!levels || !ProfessionCompatible(leader,member,Activity(targetPlan)) || !CanInvite(leader,member) || !NeedsRole(leader,member))continue;
auto source=member->GetGroup();unsigned sourceSize=source?source->GetMembersCount():1;
if(source && source->GetLeaderGUID()==member->GetGUID() && sourceSize>1)
{
bool botSuccessor=false;
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
if(auto successor=ObjectAccessor::FindPlayer(slot.guid))
if(successor->GetSession() && successor->GetSession()->IsClientless()){botSuccessor=true;break;}
if(!botSuccessor)continue;
}
if(targetGroup && sourceSize>=targetGroup->GetMembersCount())continue;
if(sourceSize<bestSource || (sourceSize==bestSource && (!best || member->GetGUID()<best->GetGUID())))
{best=member;bestSource=sourceSize;}
}
if(!best)continue;
if(handler)handler->PSendSysMessage("Group manager %s: member=%u '%s' -> leader=%u '%s' target='%s' role=%u",
apply?"move":"proposal",best->GetGUID().GetCounter(),best->GetName().c_str(),leader->GetGUID().GetCounter(),leader->GetName().c_str(),
targetPlan.NextLocation.c_str(),unsigned(BrowserRole(best)));
if(!apply){++moves;pool.erase(std::remove(pool.begin(),pool.end(),best),pool.end());changed=true;break;}
if(ManagedTransfer(leader,best)){++moves;changed=true;break;}
}
if(!changed)break;
}
_formationManagerLast=now;_formationManagerMoves=moves;_formationManagerCandidates=candidates;
_formationManagerDetail=apply?"applied":"preview";
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_RUN apply=%u forced=%u candidates=%u moves=%u",apply?1:0,forced?1:0,candidates,moves);
if(handler)handler->PSendSysMessage("Group manager %s: candidates=%u moves=%u.",apply?"applied":"preview",candidates,moves);
return moves;
}
void AdventureGroupMgr::MaybeRunFormationManager(time_t now)
{
if(now<_formationManagerAt)return;
_formationManagerAt=now+30;
bool due=false;
for(auto const& entry:_records)if(auto player=ObjectAccessor::FindPlayer(entry.first))
{
auto plan=sAdventureBehaviorMgr->GroupPlan(player);
if(plan.Enabled && plan.Forming && AdventureGroupPolicy::ManagerWindow(plan.Deadline,now))
{due=true;break;}
}
if(!due)return;
RunFormationManager(nullptr,true,false);
}
bool AdventureGroupMgr::Command(ChatHandler* handler,char const* args)
{
std::istringstream in(args?args:"");std::string op;in>>op;
if(op=="preview"){RunFormationManager(handler,false,true);return true;}
if(op=="run"){RunFormationManager(handler,true,true);return true;}
if(op=="status")
{
handler->PSendSysMessage("Group manager last=%llu candidates=%u moves=%u mode='%s' nextAutomatic=%llu",
static_cast<unsigned long long>(_formationManagerLast),_formationManagerCandidates,_formationManagerMoves,_formationManagerDetail.c_str(),
static_cast<unsigned long long>(_formationManagerAt));return true;
}
handler->SendSysMessage("Usage: clientless groupmanager preview|run|status");return false;
}
void AdventureGroupMgr::CheckFullGroup(Player* p)
{
auto group=p->GetGroup();if(!group)return;
@@ -361,6 +545,7 @@ void AdventureGroupMgr::CheckFullGroup(Player* p)
void AdventureGroupMgr::Update(Player* p)
{
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
MaybeRunFormationManager(now);
CheckFullGroup(p);
if(PlayerLeadership(p))return;
if(!Ready(p) || p->isUsingLfg() || p->InBattlegroundQueue())return;
@@ -36,6 +36,7 @@ public:
bool Forming(Player* player) const;
bool PlannedTransition(Player* player);
void Report(ChatHandler* handler,Player* player) const;
bool Command(ChatHandler* handler,char const* args);
private:
struct Record
{
@@ -50,6 +51,9 @@ private:
unsigned PendingInvites(Player* leader,Player* except=nullptr) const;
bool SurplusRole(Player* member);
bool Merge(Player* player,AdventureGroupPlan const& plan);
bool ManagedTransfer(Player* leader,Player* member);
unsigned RunFormationManager(ChatHandler* handler,bool apply,bool forced);
void MaybeRunFormationManager(time_t now);
void CheckFullGroup(Player* player);
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
void Speak(Player* player,std::string const& message);
@@ -57,6 +61,9 @@ private:
bool RecruitPlayer(Player* player,std::string const& intent);
std::unordered_map<ObjectGuid,time_t> _playerInvites;
time_t _nextChannelMessage=0;
time_t _formationManagerAt=0,_formationManagerLast=0;
unsigned _formationManagerMoves=0,_formationManagerCandidates=0;
std::string _formationManagerDetail="not run";
std::unordered_map<ObjectGuid,Record> _records;
};
#define sAdventureGroupMgr AdventureGroupMgr::instance()
@@ -40,6 +40,13 @@ inline bool AcceptPlayerLevel(unsigned bot,unsigned player)
{return bot<=player || bot-player<=5;}
inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other)
{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);}
inline bool ManagerLevelCompatible(unsigned level,unsigned other)
{
if(level==85 || other==85)return level==85 && other==85;
return level>other?level-other<=5:other-level<=5;
}
inline bool ManagerWindow(long long deadline,long long now)
{return deadline>now && deadline-now<=600;}
template<class Day,class Meeting>
void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting)
{
@@ -19,6 +19,7 @@
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessWorldPointMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGeneratorMgr.h"
@@ -2617,6 +2618,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
} },
{ "adventure", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureBehaviorMgr->Command(handler,args); }, "" },
{ "groupmanager", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureGroupMgr->Command(handler,args); }, "" },
{ "flight", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessFlight, "" },
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {