Allow destination micro activities and coordinate group departures while prioritizing combat assistance and shared leader routes. Remove artificial world-group capacity limits, improve PvP coordination and resurrection, fix parent-zone graveyard lookup and cross-map corpse cleanup, and add hourly death recovery. Include regression tests, documented live validation and repeatable test scenarios.
233 lines
20 KiB
Python
233 lines
20 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 unsigned PLAYER_MAX_BATTLEGROUND_QUEUES=3;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 queued=false;bool isUsingLfg(){return queued;}bool bgqueued=false,bg=false,bgoffer=false;
|
|
bool InBattlegroundQueue(){return bgqueued;}bool InBattleground(){return bg;}
|
|
unsigned GetBattlegroundQueueTypeId(unsigned){return 1;}bool IsInvitedForBattlegroundQueueType(unsigned){return bgoffer;}
|
|
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{bool GroupDeparting(Player*){return false;}bool AllowGroupActivities(Player*){return false;}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+=r'namespace lfg{constexpr int LFG_STATE_QUEUED=1;} struct LfgMgr{int GetState(ObjectGuid guid){auto p=ObjectAccessor::FindPlayer(guid);return p&&p->queued?1:0;}}lfgMgr;auto sLFGMgr=&lfgMgr;'
|
|
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::WaitingForBrowserGroup(', '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");
|
|
|
|
for(auto intent:{"adventure","pvp","social","profession","leisure","rest","economy"})assert(Capacity(intent)==40);
|
|
assert(Capacity("instance")==5 && Capacity("raid")==40);
|
|
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 respects the actual 25-player destination capacity.
|
|
b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=24;behavior.plans[29].NextIntent="raid";behavior.plans[29].Mode=1;behavior.plans[29].Capacity=25;
|
|
behavior.plans[31]={true,true,"groupformation","ironforge","raid","otherraid",3600};behavior.plans[31].Capacity=25;
|
|
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=25;assert(!mgr.Accept(&b,&a));
|
|
// Queued bots remain passive but accept compatible formation invitations.
|
|
{AdventureGroupMgr waiting;Player leader,member;leader.guid={950};member.guid={951};
|
|
ObjectAccessor::players[leader.guid]=&leader;ObjectAccessor::players[member.guid]=&member;
|
|
behavior.plans[950]={true,true,"groupformation","capital","instance","target",3600};
|
|
behavior.plans[951]={true,false,"instance","target",{}, {},3600};member.queued=true;
|
|
waiting._records[leader.guid]={};waiting._records[member.guid]={};int before=invites;
|
|
waiting.Update(&member);assert(invites==before);
|
|
waiting.Update(&leader);assert(invites==before+1&&member.GetGroupInvite());
|
|
profile.Personality["sociability"]=0;assert(waiting.Accept(&member,&leader));
|
|
locations.levelEligible=false;assert(!waiting.Accept(&member,&leader));locations.levelEligible=true;
|
|
member.level=30;assert(!waiting.Accept(&member,&leader));member.level=60;
|
|
behavior.plans[950].NextIntent="raid";assert(!waiting.Accept(&member,&leader));
|
|
// The same passive admission applies to PvP queues, but never steals an active BG offer.
|
|
behavior.plans[950].NextIntent="pvp";behavior.plans[951].Intent="pvp";
|
|
member.queued=false;member.bgqueued=true;assert(waiting.WaitingForBrowserGroup(&member));
|
|
member.invite=nullptr;waiting._records.clear();waiting._records[leader.guid]={};waiting._records[member.guid]={};
|
|
profile.Personality["sociability"]=1;int bgBefore=invites;
|
|
waiting.Update(&member);assert(invites==bgBefore);
|
|
waiting.Update(&leader);assert(invites==bgBefore+1&&member.GetGroupInvite());
|
|
profile.Personality["sociability"]=0;assert(waiting.Accept(&member,&leader));
|
|
member.bgoffer=true;assert(!waiting.WaitingForBrowserGroup(&member)&&!waiting.Accept(&member,&leader));
|
|
member.bgoffer=false;member.bg=true;assert(!waiting.WaitingForBrowserGroup(&member));member.bg=false;
|
|
member.bgqueued=false;assert(!waiting.WaitingForBrowserGroup(&member));
|
|
profile.Personality["sociability"]=1;ObjectAccessor::players.erase(leader.guid);ObjectAccessor::players.erase(member.guid);
|
|
invites=before;}
|
|
// 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);
|
|
// Open-world activities never trim an existing group, even with stale low metadata.
|
|
party.count=10;party.slots={{{29}},{{40}},{{41}},{{31}},{{42}}};party.leader=a.guid;b.group=&party;
|
|
for(auto intent:{"adventure","pvp","social","profession","leisure","rest","economy"})
|
|
{
|
|
behavior.plans[29].Intent=intent;behavior.plans[29].Capacity=3;
|
|
assert(!mgr.EnforceCapacity(&b) && b.group==&party && party.count==10);
|
|
}
|
|
// Only enabled, known instance/raid capacity may trim; unknown targets keep members.
|
|
behavior.plans[29].Intent="instance";behavior.plans[29].Capacity=0;
|
|
assert(!mgr.EnforceCapacity(&b));
|
|
behavior.plans[29].Capacity=5;behavior.plans[29].Enabled=false;
|
|
assert(!mgr.EnforceCapacity(&b));behavior.plans[29].Enabled=true;
|
|
party.count=5;behavior.plans[29].Capacity=3;
|
|
assert(mgr.EnforceCapacity(&b)&&!b.group&&party.count==4);
|
|
b.group=&party;party.count=10;behavior.plans[29].Intent="raid";behavior.plans[29].Capacity=10;
|
|
assert(!mgr.EnforceCapacity(&b));party.count=11;
|
|
party.slots={{{29}},{{40}},{{41}},{{42}},{{43}},{{44}},{{45}},{{46}},{{47}},{{48}},{{31}}};
|
|
assert(mgr.EnforceCapacity(&b)&&!b.group&&party.count==10);
|
|
behavior.plans[29].Intent="social";behavior.plans[29].Capacity=0;
|
|
// 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.')
|