Add deterministic AdventureBot group formation manager
This commit is contained in:
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")
|
||||
|
||||
@@ -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, "", {
|
||||
|
||||
Reference in new issue
Block a user