189 lines
16 KiB
Python
189 lines
16 KiB
Python
"""Execute production bot recruitment with native-party/channel adapters."""
|
|
from pathlib import Path
|
|
import subprocess,tempfile
|
|
root=Path(__file__).resolve().parents[2]
|
|
s=(root/'src/server/game/World/AdventureGroupMgr.cpp').read_text()
|
|
def fun(sig):
|
|
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
|
|
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
|
|
return s[a:e]
|
|
header=(root/'src/server/game/World/AdventureGroupMgr.h').read_text()
|
|
header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:')
|
|
code=r'''
|
|
#include "AdventureGroupPolicy.h"
|
|
#include "AdventureBehaviorPolicy.h"
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
using uint32=unsigned;
|
|
#define TC_GAME_API
|
|
#define TC_LOG_INFO(...) ((void)0)
|
|
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}bool operator<(ObjectGuid b)const{return id<b.id;}};
|
|
namespace std{template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)const{return g.id;}};}
|
|
struct Player;struct ChatHandler;
|
|
struct Map{bool IsDungeon(){return false;}}map;
|
|
struct Member{Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
|
|
constexpr int GROUP_REMOVEMETHOD_LEAVE=1;
|
|
struct Group{struct Slot{ObjectGuid guid;};std::vector<Slot>slots;auto const&GetMemberSlots(){return slots;}void RemoveMember(ObjectGuid,int);Member*members=nullptr;Member*GetFirstMember(){return members;}void ChangeLeader(ObjectGuid g){leader=g;}void SendUpdate(){}ObjectGuid leader;unsigned count=2;bool raid=false;ObjectGuid GetLeaderGUID(){return leader;}unsigned GetMembersCount(){return count;}bool isRaidGroup(){return raid;}void ConvertToRaid(){raid=true;}void ConvertToGroup(){raid=false;}};
|
|
struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};using Difficulty=unsigned;constexpr int MSG_SET_DUNGEON_DIFFICULTY=3;constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int ALLIANCE=0,LANG_COMMON=7,LANG_ORCISH=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
|
|
namespace WorldPackets{namespace Party{struct PartyInviteClient{PartyInviteClient(WorldPacket&&){}std::string TargetName;ObjectGuid TargetGUID;};}}
|
|
struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);void HandleSetDungeonDifficultyOpcode(WorldPacket&p){HandleSetRaidDifficultyOpcode(p);}};
|
|
struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session session{this};bool combat=false;unsigned zone=1519,level=60,team=0;float distance=10;
|
|
unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}unsigned GetDifficulty(bool){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
|
|
bool isUsingLfg(){return false;}bool InBattlegroundQueue(){return false;}
|
|
bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return ↦}
|
|
unsigned GetZoneId(){return zone;}unsigned getLevel(){return level;}unsigned GetTeam(){return team;}
|
|
Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}Session*GetSession(){return &session;}std::string GetName(){return std::to_string(guid.id);}
|
|
bool IsInMap(Player*o){return zone==o->zone;}bool IsInPhase(Player*){return true;}bool CanSeeOrDetect(Player*){return true;}
|
|
float GetExactDist(Player*){return distance;}bool IsWithinLOSInMap(Player*){return true;}
|
|
std::vector<Player*> people;bool afk=false;std::string whisper;bool isAFK(){return afk;}void GetPlayerListInGrid(std::vector<Player*>& out,int){out=people;}void Whisper(std::string const& s,int,Player*){whisper=s;}
|
|
void SetFacingToObject(Player*){}void HandleEmoteCommand(int){}
|
|
};
|
|
namespace ClientlessBrowserPolicy{bool Human(Player*p){return p&&!p->GetSession()->IsClientless();}}
|
|
namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid g){auto it=players.find(g);return it==players.end()?nullptr:it->second;}}
|
|
void Session::HandleSetRaidDifficultyOpcode(WorldPacket&p){owner->difficulty=p.value;}
|
|
void Group::RemoveMember(ObjectGuid g,int){--count;ObjectAccessor::FindPlayer(g)->group=nullptr;}
|
|
Group pending;int invites=0;
|
|
void Session::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&p){++invites;pending.leader=owner->guid;ObjectAccessor::FindPlayer(p.TargetGUID)->invite=owner->group?owner->group:&pending;}
|
|
struct Profile{std::unordered_map<std::string,float> Personality{{"sociability",1}};std::unordered_set<std::string>PrimaryProfessions{"skinning"};}profile;
|
|
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
|
|
struct Combat{bool Busy(Player*){return false;}}combat;auto sAdventureCombatMgr=&combat;
|
|
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
|
float frand(float,float){return 0;}unsigned urand(unsigned a,unsigned){return a;}
|
|
'''+header+r'''
|
|
struct Position{void Relocate(float,float,float){}};
|
|
struct Location{unsigned ZoneId=1519,AreaId=1519;std::string Name="Stormwind";struct{unsigned MapId=0;float X=0,Y=0,Z=0;}Point;}location;
|
|
struct Locations{bool levelEligible=true;Location*Find(std::string const&){return &location;}bool LevelEligible(Player*,Location const&,std::string const&,bool){return levelEligible;}}locations;auto sAdventureLocationMgr=&locations;
|
|
struct Areas{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
|
|
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}unsigned arms=0;void ArmGroupDeparture(Player*,AdventureGroupPlan const&){++arms;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
|
struct GroupTravel {unsigned follows=0;bool Follow(Player*){++follows;return true;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
|
|
int messages=0;
|
|
void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;}
|
|
bool BattlegroundCompatible(Player*,Player*){return true;}
|
|
'''
|
|
code+=r'''
|
|
void AdventureGroupMgr::UpdateBrowser(Player*){}
|
|
bool AdventureGroupMgr::Merge(Player*,AdventureGroupPlan const&){return false;}
|
|
bool AdventureGroupMgr::NeedsRole(Player*,Player*){return true;}
|
|
void AdventureGroupMgr::MaybeRunFormationManager(time_t){}
|
|
bool AdventureGroupMgr::Complete(Player*p){auto group=p->GetGroup();if(!group)return false;auto plan=sAdventureBehaviorMgr->GroupPlan(p);return group->GetMembersCount()==(plan.Capacity?plan.Capacity:AdventureGroupPolicy::Capacity(plan.Forming?plan.NextIntent:plan.Intent));}
|
|
'''
|
|
code+=fun('float Sociability(')+'\n'+fun('bool ProfessionCompatible(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')+'\n'+fun('unsigned Capacity(')
|
|
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::CheckFullGroup(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
|
|
code+=r'''
|
|
int main(){
|
|
using namespace AdventureGroupPolicy;
|
|
// Joined membership triggers gathering; pending invitations alone do not.
|
|
{AdventureGroupMgr check;Player one,two,human;one.guid={801};two.guid={802};human.guid={803};human.session.clientless=false;
|
|
Group g;g.leader=one.guid;g.count=4;one.group=two.group=&g;
|
|
Member second{&two},first{&one,&second};g.members=&first;
|
|
ObjectAccessor::players={{one.guid,&one},{two.guid,&two},{human.guid,&human}};
|
|
behavior.plans[801]={true,true,"groupformation","city","instance","destination",3600};behavior.plans[802]=behavior.plans[801];
|
|
unsigned before=behavior.arms;check._records[two.guid].Inviter=one.guid;check._records[two.guid].InviteUntil=200;
|
|
check.CheckFullGroup(&one);assert(behavior.arms==before);
|
|
g.count=5;check.CheckFullGroup(&one);assert(behavior.arms==before+2);
|
|
// A real player may lead the full group; forming and one-time departure exemption still work.
|
|
g.leader=human.guid;human.group=&g;before=behavior.arms;
|
|
assert(check.Forming(&one));check.CheckFullGroup(&one);assert(behavior.arms==before+2);
|
|
behavior.plans[801].Intent="instance";behavior.plans[801].Forming=false;behavior.plans[801].EarlyDeparture=true;
|
|
assert(check.PlannedTransition(&one)&&!check.PlannedTransition(&one));
|
|
// Ten-player native raids fill at ten; the twenty-bot cap still applies to larger raids.
|
|
behavior.plans[801]={true,true,"groupformation","city","raid","destination",3600};behavior.plans[801].Capacity=10;
|
|
g.count=9;before=behavior.arms;check.CheckFullGroup(&one);assert(behavior.arms==before);
|
|
g.count=10;check.CheckFullGroup(&one);assert(behavior.arms==before+2);
|
|
g.leader=one.guid;behavior.plans[801].Mode=2;check._records[one.guid].SearchAt=10000;
|
|
check.Update(&one);assert(one.difficulty==2&&g.raid); // native heroic10, no hardcoded25-normal reset
|
|
ObjectAccessor::players.clear();behavior.plans.clear();}
|
|
|
|
{AdventureGroupMgr check;Player leader,member;leader.guid={901};member.guid={902};Group party;party.leader=leader.guid;leader.group=member.group=&party;
|
|
ObjectAccessor::players[leader.guid]=&leader;ObjectAccessor::players[member.guid]=&member;
|
|
behavior.plans[leader.guid.id]={true,true,"groupformation","capital","instance","target"};
|
|
assert(!check.FollowMember(&member)&&groupTravel.follows==0);
|
|
behavior.plans[leader.guid.id]={true,false,"instance","target"};
|
|
assert(check.FollowMember(&member)&&groupTravel.follows==1);
|
|
ObjectAccessor::players.clear();behavior.plans.clear();}
|
|
|
|
AdventureBehaviorPolicy::Day day;std::mt19937 random(42);
|
|
for(unsigned i=0;i<24;++i)day[i]={i%2?"instance":"social",std::to_string(i)};
|
|
Prepare(day,random,1,[]{return std::string("capital");});
|
|
for(unsigned h=0;h<24;++h)if(day[h].Intent=="groupformation")assert(h<23&&Eligible(day[h+1].Intent)&&day[h].Location=="capital");
|
|
|
|
assert(Capacity("instance")==5&&Capacity("raid")==20&&Capacity("social")>5);
|
|
assert(Compatible("instance","instance",60,65)&&!Compatible("instance","raid",60,60));
|
|
assert(!Compatible("raid","raid",60,66));
|
|
AdventureGroupMgr mgr;Player a,b;a.guid={29};b.guid={31};b.zone=1537;b.distance=10000;
|
|
ObjectAccessor::players={{a.guid,&a},{b.guid,&b}};
|
|
behavior.plans[29]={true,true,"groupformation","stormwind","instance","deadmines",3600};
|
|
behavior.plans[31]={true,true,"groupformation","ironforge","instance","shadowfang",3600};
|
|
mgr._records[a.guid]={};mgr._records[b.guid]={};
|
|
// Remote capital recruitment accepts different original targets/meeting points.
|
|
mgr.Update(&a);assert(invites==1&&b.invite&&b.invite->leader==a.guid&&messages>=1);
|
|
assert(mgr.Accept(&b,&a));
|
|
behavior.plans[31].NextIntent="raid";assert(!mgr.Accept(&b,&a));behavior.plans[31].NextIntent="instance";
|
|
b.team=1;assert(!mgr.Accept(&b,&a));b.team=0;
|
|
b.combat=true;assert(!mgr.Accept(&b,&a));b.combat=false;
|
|
// Native party cap and pending reservations prevent sixth members.
|
|
Group party;party.leader=a.guid;party.count=5;a.group=&party;
|
|
assert(!mgr.Accept(&b,&a));b.invite=nullptr;mgr._records[b.guid].InviteUntil=0;
|
|
mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==1);
|
|
// Existing group recruits and lone bot requests membership without replacing leader.
|
|
party.count=3;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();
|
|
mgr._records[b.guid].SearchAt=0;mgr.Update(&b);assert(invites==2&&b.invite==&party&&party.leader==a.guid);
|
|
assert(mgr.Accept(&b,&a));
|
|
// Intent-hour transition is exempt exactly once, no permanent exit exemption.
|
|
b.group=&party;behavior.plans[31].Intent="instance";behavior.plans[31].Forming=false;
|
|
assert(mgr.PlannedTransition(&b));assert(!mgr.PlannedTransition(&b));
|
|
// Raid native conversion and twentieth-member boundary.
|
|
b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=19;behavior.plans[29].NextIntent="raid";behavior.plans[29].Mode=1;
|
|
behavior.plans[31]={true,true,"groupformation","ironforge","raid","otherraid",3600};
|
|
mgr._records[a.guid].SearchAt=0;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();mgr._records[b.guid].InviteUntil=0;
|
|
mgr.Update(&a);assert(a.difficulty==RAID_DIFFICULTY_25MAN_NORMAL&&party.raid&&invites==3&&mgr.Accept(&b,&a));
|
|
party.count=20;assert(!mgr.Accept(&b,&a));
|
|
// Solo recruitment outside formation requires actual local contact.
|
|
a.group=nullptr;b.invite=nullptr;behavior.plans[29]={true,false,"social","city",{}, {},3600};behavior.plans[31]=behavior.plans[29];
|
|
mgr._records.clear();mgr._records[a.guid]={};mgr._records[b.guid]={};mgr.Update(&a);assert(invites==3);
|
|
b.zone=a.zone;b.distance=10;mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==4);
|
|
// When a large raid changes to a smaller activity, excess bots leave safely.
|
|
party.count=5;party.slots={{{29}},{{40}},{{41}},{{31}},{{42}}};party.leader=a.guid;b.group=&party;
|
|
behavior.plans[29].Intent="adventure";assert(mgr.EnforceCapacity(&b)&&!b.group&&party.count==4);
|
|
behavior.plans[29].Intent="social";
|
|
// Explicit human invite to a grouped bot offers admission, then native leadership.
|
|
Player human;human.guid={21};human.session.clientless=false;ObjectAccessor::players[human.guid]=&human;
|
|
a.group=&party;party.leader=a.guid;party.count=2;
|
|
mgr.RequestPlayerAdmission(&a,&human);assert(invites==5&&human.invite==&party);
|
|
human.group=&party;Member humanSlot;humanSlot.player=&human;party.members=&humanSlot;
|
|
mgr.Update(&a);assert(party.leader==human.guid&&!mgr.BotLed(&a));
|
|
// Content checks and level ranges apply before any native invitation.
|
|
human.group=nullptr;human.invite=nullptr;a.level=60;human.level=54;
|
|
assert(!mgr.CanInvite(&a,&human));human.level=55;locations.levelEligible=false;
|
|
assert(!mgr.CanInvite(&a,&human));locations.levelEligible=true;areas.allowed=false;
|
|
assert(!mgr.CanInvite(&a,&human));areas.allowed=true;human.level=60;
|
|
// A player must not bypass the level limit by targeting another bot in a group.
|
|
party.leader=a.guid;a.group=&party;a.level=80;b.level=85;b.group=&party;human.level=75;
|
|
party.slots={{{29}},{{31}}};
|
|
assert(!mgr.CanInvite(&a,&human));int beforeAdmission=invites;
|
|
mgr.RequestPlayerAdmission(&b,&human);assert(invites==beforeAdmission);
|
|
a.level=b.level=human.level=60;party.slots.clear();
|
|
// Inform nearby humans of the actual NEXT destination before invitation.
|
|
{mgr={};Player human;human.guid={999};human.session.clientless=false;
|
|
ObjectAccessor::players[human.guid]=&human;a.group=nullptr;a.people={&human};
|
|
behavior.plans[a.guid.id]={true,true,"groupformation","capital","raid","target"};
|
|
int before=invites;assert(mgr.RecruitPlayer(&a,"raid"));
|
|
assert(invites==before+1&&human.invite&&a.whisper.find("Schlachtzug")!=std::string::npos&&a.whisper.find("Stormwind")!=std::string::npos);
|
|
human.invite=nullptr;assert(!mgr.RecruitPlayer(&a,"raid")); // shared cooldown even after decline
|
|
GameTime::now+=601;human.afk=true;assert(!mgr.RecruitPlayer(&a,"raid"));
|
|
human.afk=false;human.team=1;assert(!mgr.RecruitPlayer(&a,"raid"));
|
|
human.team=0;human.group=&pending;assert(!mgr.RecruitPlayer(&a,"raid"));}
|
|
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p=Path(d);(p/'groups.cpp').write_text(code)
|
|
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'groups.cpp'),'-o',str(p/'groups')],check=True)
|
|
subprocess.run([str(p/'groups')],check=True)
|
|
print('Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.')
|