Files
Viano-Core/tests/clientless/test_adventure_interactions.py
T
Nico aba5aeba2c Keep queued adventure bots active and preserve browser admission rules
Reuse formation performances while waiting for dungeon, raid or PvP admission, and accept compatible formation invitations without active recruitment or interrupting battleground offers.

Require the full low-population wait for every participating queue entry and acknowledge connected clientless members during native group teleportation.

Add regression coverage and document builds, live Yarumak/Seraellin checks and separately preserved shutdown crash findings.
2026-11-04 14:19:38 +01:00

158 lines
16 KiB
Python

"""Run production interaction manager against deterministic native-world adapters."""
from pathlib import Path
import subprocess
import tempfile
root=Path(__file__).resolve().parents[2]
header=(root/'src/server/game/World/AdventureInteractionMgr.h').read_text()
header='\n'.join(line for line in header.splitlines() if not line.startswith('#include')).replace('private:','public:')
source=(root/'src/server/game/World/AdventureInteractionMgr.cpp').read_text()
source='\n'.join(line for line in source.splitlines() if not line.startswith('#include'))
code=r'''
#include "AdventureInteractionPolicy.h"
#include "AdventureGroupPolicy.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <ctime>
#include <string>
#include <unordered_map>
#include <vector>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
struct ObjectGuid { unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id!=0;}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 Position {
float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;
void Relocate(Position const*p){*this=*p;}
float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}
float GetExactDist(Position const*p)const{return std::hypot(m_positionX-p->m_positionX,m_positionY-p->m_positionY);}
};
enum {TEXT_EMOTE_RUDE=1,TEXT_EMOTE_THREATEN,TEXT_EMOTE_ROAR,TEXT_EMOTE_GREET,TEXT_EMOTE_HELLO,TEXT_EMOTE_WAVE,TEXT_EMOTE_BOW,TEXT_EMOTE_SALUTE,TEXT_EMOTE_SMILE,TEXT_EMOTE_THANK,EMOTE_ONESHOT_BOW,EMOTE_ONESHOT_WAVE,EMOTE_ONESHOT_NO,UNIT_STAND_STATE_STAND,ALLIANCE,LANG_COMMON,LANG_ORCISH,CHAT_MSG_PARTY,GROUP_REMOVEMETHOD_LEAVE,CMSG_PARTY_INVITE_RESPONSE,UNIT_STATE_CASTING};
struct WorldPacket { explicit WorldPacket(int=0){} };
struct Player;struct Group;
namespace WorldPackets {namespace Party {struct PartyInviteResponse{explicit PartyInviteResponse(WorldPacket&&){}bool Accept=false;};}}
struct Session {Player*owner=nullptr;bool clientless=false;bool IsClientless(){return clientless;}void HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&);};
struct Map {bool dungeon=false;bool IsDungeon(){return dungeon;}};
struct Unit:Position {Player*victim=nullptr;std::vector<Unit*> attackers;virtual ~Unit()=default;virtual struct Creature*ToCreature(){return nullptr;}Unit*GetVictim(){return reinterpret_cast<Unit*>(victim);}auto&getAttackers(){return attackers;}};
struct Creature:Unit{};
struct Member {Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
struct Player:Unit {
ObjectGuid guid,selection;bool alive=true,inWorld=true,combat=false,teleport=false,transport=false,flight=false,charmed=false,casting=false,afk=false,visible=true,phase=true,los=true;
bool lfg=false,bgqueue=false,battleground=false;bool isUsingLfg(){return lfg;}bool InBattlegroundQueue(){return bgqueue;}bool InBattleground(){return battleground;}
unsigned mapid=0,team=ALLIANCE;Session session;Group*group=nullptr,*invite=nullptr;Map map;int gestures=0,says=0,posture=0;uint32 lastEmote=0;
explicit Player(unsigned id=26){guid.id=id;session.owner=this;}
ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsFlying(){return false;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;}
Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}unsigned level=60;unsigned getLevel(){return level;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}void UninviteFromGroup(){invite=nullptr;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;}
void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;}
std::vector<Player*> people;void GetPlayerListInGrid(std::vector<Player*>&out,float){out=people;}
};
struct Group {ObjectGuid leader;Member first;int removals=0,messages=0;ObjectGuid GetLeaderGUID(){return leader;}Member*GetFirstMember(){return first.player?&first:nullptr;}void BroadcastPacket(WorldPacket*,bool){++messages;}void RemoveMember(ObjectGuid,int){++removals;assert(first.player);first.player->group=nullptr;}};
int accepts=0,declines=0;
void Session::HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&p){assert(owner->invite);if(p.Accept){++accepts;owner->group=owner->invite;}else ++declines;owner->invite=nullptr;}
struct ChatHandler{void SendSysMessage(char const*){}template<class...Args>void PSendSysMessage(char const*,Args...){}static void BuildChatPacket(WorldPacket&,int,int,Player*,Player*,char const*){}};
namespace GameTime {time_t now=100;time_t GetGameTime(){return now;}}
unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,float){return roll;}
namespace ObjectAccessor {std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}}
struct Profile {std::string Locale="deDE";std::unordered_map<std::string,float> Personality;};
struct Lua {std::unordered_map<unsigned,Profile> profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua;
struct Behavior {int interrupts=0;bool protectGroupLeave=false,protectInstance=false;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}bool ProtectGroupLeave(Player*)const{return protectGroupLeave;}bool ProtectInstanceGroupLeave(Player*)const{return protectInstance;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core;
enum class ClientlessTravelState{Moving,Arrived};struct Nav {ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=10;};
struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*GetStatus(unsigned){return active?&nav:nullptr;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return valid;}void StartTravel(Player*,Position const&p){++starts;nav.Target=p;nav.State=ClientlessTravelState::Moving;active=true;}void CancelTravel(unsigned){++cancels;active=false;}}travel;auto sClientlessTravelMgr=&travel;
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;};
struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct BotGroups{unsigned browserCancels=0;void CancelBrowser(Player*){++browserCancels;}void Offline(Player*){}void Update(Player*){}bool Accept(Player*,Player*){return false;}bool botLed=false;bool BotLed(Player*){return botLed;}bool Forming(Player*){return false;}bool PlannedTransition(Player*){return false;}void Report(ChatHandler*,Player*){}}botGroups;auto sAdventureGroupMgr=&botGroups;
struct GroupTravel{bool protectedTravel=false,separated=false;int follows=0;bool Follow(Player*p){++follows;return p->GetGroup()!=nullptr;}bool Protected(Player*){return protectedTravel;}bool Separated(Player*){return separated;}void Clear(Player*){}void Offline(Player*){}void Report(ChatHandler*,Player*){}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
'''+header+'\n'+source+r'''
int main(){
using namespace AdventureInteractionPolicy;
assert(Willingness("adventure",.65f)>Willingness("profession",.65f));assert(Willingness("rest",0)==0);
assert(!LeaderExpired(699,100));assert(LeaderExpired(700,100));assert(!LeaderExpired(99,100));
assert(CanLeave(true,false,false,false));assert(!CanLeave(true,true,false,false));assert(!CanLeave(true,false,true,false));
AdventureInteractionMgr mgr;Player bot(26),leader(21),other(27);bot.session.clientless=true;other.session.clientless=true;
ObjectAccessor::players={{bot.guid,&bot},{leader.guid,&leader},{other.guid,&other}};
lua.profiles[26].Personality["sociability"]=.65f;lua.profiles[26].Personality["group_loyalty"]=0;lua.profiles[27]={};
Group group;group.leader=leader.guid;group.first.player=&bot;
auto tick=[&](time_t now){GameTime::now=now;return mgr.Update(&bot,1000);};
// Native invitation response creates actual membership, then suspends intent.
bot.invite=&group;tick(100);assert(!bot.group);tick(102);assert(bot.group && accepts==1);
assert(mgr._records[26].Grouped && behavior.interrupts==1);
// Follow uses verified local MMAP paths; invalid paths never move the player.
leader.m_positionX=20;auto follows=groupTravel.follows;tick(103);assert(groupTravel.follows==follows+1);leader.m_positionX=30;tick(104);assert(groupTravel.follows==follows+2);
// Changing leader resets both AFK and inactivity timers.
mgr._records[26].AfkSince=1;group.leader=other.guid;tick(105);assert(mgr._records[26].AfkSince==0 && mgr._records[26].LastActivity==105);
group.leader=leader.guid;tick(106);
// Chat/spells count as activity even while the leader stands still.
core.activity=650;tick(650);tick(1249);assert(bot.group);tick(1250);assert(!bot.group && group.removals==1);
// Hour intent change is deferred inside instances and during combat.
core.activity=0;bot.group=&group;behavior.schedule="adventure:road";tick(1300);behavior.schedule="economy:city";
bot.map.dungeon=true;tick(1301);assert(bot.group);bot.map.dungeon=false;bot.combat=true;tick(1302);assert(bot.group);bot.combat=false;tick(1303);assert(!bot.group);
// Native AFK needs a full ten minutes even if manually set; no early exit.
bot.group=&group;tick(1400);leader.afk=true;tick(1401);tick(2000);assert(bot.group);tick(2001);assert(!bot.group);leader.afk=false;
// Grouped bots also answer directed emotes; responses do not create events.
leader.m_positionX=0;bot.group=&group;tick(2100);mgr.Emote(&bot,&leader,TEXT_EMOTE_WAVE);tick(2101);assert(bot.gestures==1);tick(2102);assert(bot.gestures==1);tick(2103);assert(bot.group);
// Friendly greeting versus hostile contact; no combat call is made.
bot.group=nullptr;tick(2120);mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2121);assert(bot.lastEmote==EMOTE_ONESHOT_WAVE);tick(2123);
other.team=999;mgr._records[26].Encounters.clear();mgr._records[26].NextSocial=0;mgr.Emote(&bot,&other,TEXT_EMOTE_THREATEN);tick(2124);assert(bot.lastEmote==EMOTE_ONESHOT_NO && combat.assists==0);tick(2126);
// Invisible/distant targets and repeated emotes are ignored.
auto gestures=bot.gestures;other.visible=false;mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2130);assert(bot.gestures==gestures);other.visible=true;
// Bot encounters reserve both participants; cooldown prevents new loops.
other.team=ALLIANCE;bot.people={&other};mgr._records.clear();tick(2200);assert(mgr._records[26].Partner==other.guid && mgr._records[27].Partner==bot.guid);
tick(2201);GameTime::now=2203;mgr.Update(&other,1000);assert(other.gestures==1);tick(2205);GameTime::now=2205;mgr.Update(&other,1000);tick(2265);assert(!mgr._records[26].Partner);
// A native provisional leader also holds an invite; never answer his own invitation.
mgr._records.clear();group.leader=bot.guid;bot.invite=&group;tick(2270);tick(2275);
assert(bot.invite==&group);bot.invite=nullptr;
// Unreserved bot-origin invitations are declined.
mgr._records.clear();group.leader=other.guid;bot.invite=&group;tick(2300);tick(2302);assert(!bot.group && declines==1);
// Missing leader map uses the existing graph executor, without teleporting.
group.leader=leader.guid;bot.group=&group;leader.mapid=1;follows=groupTravel.follows;tick(2400);assert(groupTravel.follows>follows);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(bot.group);
// Pending help is acknowledged only outside combat, after caller's self-care.
mgr.CombatHelp(&bot,leader.guid);leader.mapid=0;executor.status.State=ClientlessActionExecutionState::Completed;bot.combat=true;auto says=bot.says;tick(2417);assert(bot.says==says);bot.combat=false;tick(2418);assert(bot.says==says+1);
// A newly joined bot follower cancels its previous scene without stopping the leader.
bot.group=&group;group.leader=other.guid;botGroups.botLed=true;mgr._records.clear();
GameTime::now=3000;int beforeInterrupts=behavior.interrupts;mgr.Update(&bot,1000);
assert(behavior.interrupts==beforeInterrupts+1);
// Bot-led groups must let the behavior clock advance every world tick.
bot.group=&group;group.leader=bot.guid;botGroups.botLed=true;
auto& rec=mgr._records[26];rec.Grouped=true;rec.SceneUntil=0;rec.Timer=1000;
assert(!mgr.Update(&bot,50)&&rec.Timer==950);botGroups.botLed=false;
// Native queueing is activity, not an AFK leader: no intent/AFK exit while waiting.
mgr._records.clear();group.leader=leader.guid;bot.group=&group;bot.lfg=true;tick(4000);
behavior.schedule="raid:meeting";tick(5000);assert(bot.group);
bot.lfg=false;bot.bgqueue=true;tick(6000);assert(bot.group);
bot.bgqueue=false;bot.battleground=true;tick(7000);assert(bot.group);
bot.battleground=false;tick(7001);assert(!bot.group);bot.group=&group;
// Formation, gathering and departure handover suppress both schedule and AFK exits.
mgr._records.clear();behavior.protectGroupLeave=true;behavior.schedule="groupformation:meeting";leader.afk=true;
tick(8000);behavior.schedule="instance:dungeon";tick(9000);assert(bot.group);
behavior.protectGroupLeave=false;leader.afk=false;behavior.schedule="adventure:road";tick(9001);assert(!bot.group);bot.group=&group;
// Instance travel/inside protects late arrivals from both AFK and distance.
mgr._records.clear();behavior.schedule="raid:instance";behavior.protectInstance=true;
leader.afk=true;groupTravel.separated=true;tick(10000);tick(12000);assert(bot.group);
// Explicit intent changes retain their existing behavior.
behavior.schedule="profession:city";tick(12001);assert(!bot.group);
bot.group=&group;behavior.schedule="raid:instance";tick(13000);
behavior.protectInstance=false;leader.afk=false;groupTravel.separated=false;
tick(13599);assert(bot.group);tick(13600);assert(!bot.group);
bot.group=&group;behavior.protectInstance=true;groupTravel.separated=true;tick(14000);
behavior.protectInstance=false;tick(14001);assert(!bot.group);
bot.group=&group;groupTravel.separated=false;
// Explicit diagnostic leave uses ordinary safe-state guards.
bot.combat=true;assert(!mgr.RequestLeave(&bot)&&bot.group);bot.combat=false;
assert(mgr.RequestLeave(&bot)&&!bot.group);
mgr.Offline(&bot);assert(!mgr._records.count(26));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed')