Files
Viano-Core/tests/clientless/test_adventure_groups.py
T
Nico 54d77d1e4d
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Fix AdventureBot group preparation, public portals and flight recovery
Add preparation outings and equipment checks with a five-minute return to the meeting point. Correct static portal visitor casting and level conditions, with native interaction diagnostics. Recover suspended flying mounts, walk to verified takeoff sites, and validate landing and return volumes. Include SQL migrations, targeted regression tests and live validation checkpoints.
2026-10-29 18:21:43 +01:00

157 lines
13 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;}};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&);};
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;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return &map;}
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 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{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];}}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;}
'''
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')
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::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=r'''
int main(){
using namespace AdventureGroupPolicy;
{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[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.')