"""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 #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;}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{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 attackers;virtual ~Unit()=default;virtual struct Creature*ToCreature(){return nullptr;}Unit*GetVictim(){return reinterpret_cast(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 ↦}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 people;void GetPlayerListInGrid(std::vector&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*){}templatevoid 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 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 Personality;}; struct Lua {std::unordered_map 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;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;}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 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{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=⊥ 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; // 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')