"""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 #include #include #include #include #include #include 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 idstruct hash{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::vectorslots;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 people;bool afk=false;std::string whisper;bool isAFK(){return afk;}void GetPlayerListInGrid(std::vector& 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 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 Personality{{"sociability",1}};std::unordered_setPrimaryProfessions{"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 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.')