Improve AdventureBot recruitment, world presence and performance

Manage Arthoran as a persistent level-60 tank profile, add Worgen forms and mounts, enable PvP and recruit through native LFG with preparation actions and player invitations.

Reduce repeated route and flight-planning work, bound failed-travel retries and add runtime timing diagnostics. Include 86 passing tests and measurements with 110 online bots.
This commit is contained in:
Nico committed 2026-10-29 01:36:35 +01:00
1 parent cac4142ec1
commit 0173a434f4
39 files changed
+1958 -74

No files matched your search

@@ -94,8 +94,8 @@ struct Travel{
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
'''
code+=r'''
struct Profile{unsigned Class=0,Specialization=0,SoloSpecialization=0;std::string SourcePath;};
struct Lua{Profile*GetAdventureProfile(unsigned){return nullptr;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
struct Profile{bool AllowRetreat=true;unsigned Class=0,Specialization=0,SoloSpecialization=0;std::string SourcePath;};
struct Lua{Profile* profile=nullptr;Profile*GetAdventureProfile(unsigned){return profile;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
'''
# production Update uses direct Position members and creature near point.
code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1)
@@ -197,6 +197,11 @@ int main(){
// Group join aborts immediately, even while critically injured.
p.grouped=true;p.health=1;assert(!mgr.Flee(&p,r)&&!r.Fleeing&&!travel.present);
assert(!mgr.Flee(&p,r));p.grouped=false;p.health=100;
// Explicit profile refusal cancels retreat without suppressing combat.
Profile steadfast;steadfast.AllowRetreat=false;lua.profile=&steadfast;
p.health=1;r.Fleeing=true;travel.present=true;
assert(!mgr.Flee(&p,r)&&!r.Fleeing&&!travel.present&&r.AI);
lua.profile=nullptr;p.health=100;
// Blocked paths preserve native self-defence; no teleport/resurrection.
travel={};travel.path=false;GameTime::now=1004;
int ticks=r.AI->ticks;assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==0&&r.AI->ticks>ticks);
+14 -2
View File
@@ -29,7 +29,7 @@ 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 CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
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;
@@ -39,6 +39,7 @@ struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session sessio
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;}}
@@ -58,7 +59,7 @@ 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 ['bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', '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;
@@ -112,6 +113,17 @@ int main(){
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));
// 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:
@@ -30,6 +30,7 @@ struct Player {
Map* GetMap(){return &map;}
} player;
namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}}
struct Lua{void UpdateWorldPresence(Player*,std::string const&,bool){}}lua;auto sClientlessLuaEngine=&lua;
struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups;
struct Encounters{bool active=false;bool Active(Player*){return active;}}encounters;auto sAdventureEncounterMgr=&encounters;
bool unsafe=false;bool Unsafe(Player*){return unsafe;}
+9 -3
View File
@@ -53,11 +53,11 @@ bool Unsafe(Player* p){return p->combat||p->flying;}
constexpr int PATHFIND_NORMAL=1;
struct PathGenerator{
struct Point{float x,y,z;};std::vector<Point> path;
static inline bool reachable=true,broken=false;static inline int type=1;
static inline bool reachable=true,broken=false;static inline int type=1;static inline float bend=20;
PathGenerator(Player*){}void SetPathLengthLimit(float){}
bool CalculatePath(Position const&a,Position const&b){
path.clear();int n=int(a.GetExactDist(b)/4)+1;
for(int i=0;i<=n;++i){float t=float(i)/n;path.push_back({a.m_positionX+(b.m_positionX-a.m_positionX)*t,a.m_positionY+(b.m_positionY-a.m_positionY)*t+20*std::sin(3.14159265f*t),a.m_positionZ+(b.m_positionZ-a.m_positionZ)*t});}
for(int i=0;i<=n;++i){float t=float(i)/n;path.push_back({a.m_positionX+(b.m_positionX-a.m_positionX)*t,a.m_positionY+(b.m_positionY-a.m_positionY)*t+bend*std::sin(3.14159265f*t),a.m_positionZ+(b.m_positionZ-a.m_positionZ)*t});}
if(broken && path.size()>5) path[5].z+=10;
return reachable;
}
@@ -89,7 +89,7 @@ class AdventureBehaviorMgr{public:
enum class Phase{Idle,OnSite,Switching};
struct Record{
AdventureInstancePolicy::Visit Instance;
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000;std::mt19937 Random{26};
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000,RetryTimer=0;std::mt19937 Random{26};
time_t Deadline=0;bool Dirty=false;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
};
@@ -219,6 +219,12 @@ int main(){
combatMgr.available=true;assert(mgr.LeisureAvailable(&p,r,{"training"}));
assert(mgr.StartLeisure(&p,r,{"training"}) && combatMgr.starts==1);
r.Intent="adventure";assert(!mgr.LeisureAvailable(&p,r,{"training"}));
// Recruitment walks stay near the meeting point, also inside a tavern.
mgr.ResetLeisure(&p,r);r.Intent="groupformation";r.MicroTimer=45000;terrain.outdoors=false;p.outdoors=false;PathGenerator::bend=2;
assert(mgr.StartLeisure(&p,r,{"walk"}));
for(auto const& point:r.Leisure.Route)assert(point.GetExactDist(r.Leisure.Origin)<=18);
assert(!mgr.LeisureAvailable(&p,r,{"ride"}));mgr.ResetLeisure(&p,r);terrain.outdoors=true;PathGenerator::bend=20;
r.RetryTimer=30000;r.Tried.insert("old");mgr.SetWanted(&p,r,"rest","new",1234);assert(r.RetryTimer==0&&r.Tried.empty()&&r.Deadline==1234);
}
'''
@@ -0,0 +1,33 @@
"""Run real bot Speak against native channel/zone adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/World/AdventureGroupMgr.cpp').read_text()
a=s.index('void AdventureGroupMgr::Speak(');b=s.index('std::string AdventureGroupMgr::RecruitmentText(',a)
code=r'''
#include <vector>
#include <string>
#include <algorithm>
#include <cassert>
using uint32=unsigned;using time_t=long;
#define TC_LOG_INFO(...) ((void)0)
constexpr int ALLIANCE=0,LANG_COMMON=7,LANG_ORCISH=1;
namespace AdventureGroupPolicy{bool Capital(unsigned z){return z==1519;}}
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
struct Entry{unsigned ID=26;}entry;
struct Store{unsigned expected;Entry*LookupEntry(unsigned id){assert(id==expected);return &entry;}}sChatChannelsStore{26},sAreaTableStore{1519};
struct Channel;
struct Player{bool allowed=true;std::vector<Channel*> channels;unsigned GetZoneId(){return 1519;}unsigned GetTeam(){return 0;}unsigned GetGUID(){return 1;}bool CanJoinConstantChannelInZone(Entry*,Entry*){return allowed;}auto&GetJoinedChannels(){return channels;}};
struct Channel{int messages=0;bool deny=false;void JoinChannel(Player*p,char const*){if(!deny)p->channels.push_back(this);}void Say(unsigned,std::string const&,int){++messages;}}channel;
struct Manager{Channel*GetJoinChannel(unsigned id,char const* name,Entry*){assert(id==26 && std::string(name).empty());return &channel;}}manager;
namespace ChannelMgr{Manager*forTeam(unsigned){return &manager;}}
struct AdventureGroupMgr{time_t _nextChannelMessage=0;void Speak(Player*,std::string const&);};
'''+s[a:b]+r'''
int main(){AdventureGroupMgr m;Player p;m.Speak(&p,"test");assert(channel.messages==1&&p.channels.size()==1);m.Speak(&p,"test");assert(channel.messages==1);
GameTime::now+=16;p.allowed=false;m.Speak(&p,"test");assert(channel.messages==1);
GameTime::now+=16;p.allowed=true;p.channels.clear();channel.deny=true;m.Speak(&p,"test");assert(channel.messages==1);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True);subprocess.run([str(p/'test')],check=True)
print('Native LFG: ID26, localized standard channel, zone eligibility, join verification and population throttle passed')
+13 -2
View File
@@ -17,7 +17,7 @@ code=r'''
using uint32=unsigned;using uint64=unsigned long long;
using namespace AdventurePopulationPolicy;
namespace GameTime{time_t now=100000;time_t GetGameTime(){return now;}}
struct ObjectGuid{enum{Player};unsigned id;ObjectGuid(int,unsigned i):id(i){}};
struct ObjectGuid{enum{Player};unsigned id;ObjectGuid(int,unsigned i):id(i){} unsigned GetCounter(){return id;}};
namespace HighGuid{constexpr int Player=0;}
namespace ObjectAccessor{std::unordered_set<unsigned>connected;bool FindConnectedPlayer(ObjectGuid g){return connected.count(g.id);}}
enum class ClientlessPlayerState{Offline,Loading,Online,Removing};
@@ -26,14 +26,18 @@ struct Core{bool succeed=true;std::vector<unsigned>started;bool StartPlayer(unsi
int CreateClientlessAdventureLuaController(){return 0;}
float sample=.5;float frand(int,int){return sample;}
#define TC_LOG_INFO(...) ((void)0)
struct Profile{unsigned ProfileVersion=0,Class=0,Level=0;std::string SourcePath;};
struct Player{ObjectGuid GetGUID(){return ObjectGuid{ObjectGuid::Player,26};}};
struct Lua{Profile* profile=nullptr;Profile* GetAdventureProfile(unsigned){return profile;}}lua;auto sClientlessLuaEngine=&lua;
struct AdventurePopulationMgr{
struct Bot{uint32 Guid,Account,Pool;uint64 Created;uint32 Failures=0;time_t Retry=0;};
unsigned _cursor=0,_rotationAge=86400;double _rotationChance=.15;
std::array<unsigned,4>_targets{};std::vector<Bot>_bots;std::unordered_set<unsigned>_considered;
std::vector<std::pair<unsigned,unsigned>>generated;
std::unordered_map<unsigned,unsigned> _checks;void Online(Player*);
void Step();void Generate(unsigned pool,unsigned replace=0){generated.push_back({pool,replace});}
};
'''+function('void AdventurePopulationMgr::Step()')+r'''
'''+function('void AdventurePopulationMgr::Step()')+function('void AdventurePopulationMgr::Online(')+r'''
int main(){
assert(Pool(1,true)==0 && Pool(1,false)==1 && Pool(2,true)==2 && Pool(2,false)==3);
assert(!Replace(true,true,false,100,0,0,1));assert(!Replace(false,false,false,100,0,0,1));
@@ -41,6 +45,13 @@ int main(){
assert(Replace(false,true,false,100,100,.1,.2));assert(!Replace(false,true,false,100,0,.3,.2));
assert(!Deficit(5,3,2) && Deficit(5,2,1)==2 && !Deficit(0,2,0));
AdventurePopulationMgr mgr;
Player named;Profile profile;lua.profile=&profile;
mgr.Online(&named);assert(mgr._checks.empty());
profile.ProfileVersion=2;profile.Class=1;profile.Level=60;profile.SourcePath="profiles/arthoran.lua";
mgr.Online(&named);assert(mgr._checks.count(26));
mgr._checks.clear();profile.Level=0;mgr.Online(&named);assert(mgr._checks.empty());
profile.Level=60;profile.ProfileVersion=0;mgr.Online(&named);assert(mgr._checks.empty());
lua.profile=nullptr;mgr.Online(&named);assert(mgr._checks.empty());
mgr.Step();assert(mgr.generated.empty() && core.started.empty());
mgr._targets={1,2,3,4};
mgr.Step();assert(mgr.generated.back().first==0);mgr.Step();assert(mgr.generated.back().first==1);
@@ -0,0 +1,24 @@
"""Population retry spacing is bounded and increases only after failures."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "AdventureBehaviorPolicy.h"
#include <cassert>
#include <limits>
int main(){using AdventureBehaviorPolicy::TravelRetryMs;
assert(TravelRetryMs(1,0)==2000&&TravelRetryMs(2,0)==4000);
assert(TravelRetryMs(3,0)==8000&&TravelRetryMs(4,0)==16000);
assert(TravelRetryMs(5,0)==30000&&TravelRetryMs(1000,1000)==30000);
assert(TravelRetryMs(std::numeric_limits<unsigned>::max(),1000)==30000);
assert(TravelRetryMs(1,1000)==3000&&TravelRetryMs(1,10000)==3000);
for(unsigned count=1;count<100;++count)for(unsigned jitter=0;jitter<=1000;jitter+=25){
auto value=TravelRetryMs(count,jitter);assert(value>=2000&&value<=30000);
}
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-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('Travel failures: exponential/jittered retry spacing, strict 30s bound and overflow safety passed')
@@ -0,0 +1,73 @@
"""Execute production PvP/Worgen presence and ground-mount methods."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessLuaEngine.cpp').read_text()
def fn(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]
code=r'''
#include <cassert>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <ctime>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
constexpr int CHARACTER_FLAG_PVP_DESIRED=65536,PLAYER_FLAGS=1,PLAYER_FLAGS_IN_PVP=512,RACE_WORGEN=22,
SPELL_TWO_FORMS_RACIAL=68996,SPELL_ALTERED_FORM_RACIAL=97709,SPELL_AURA_WORGEN_ALTERED_FORM=1,UNIT_STATE_CASTING=2,MOVE_RUN=3;
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
unsigned roll=0;unsigned urand(unsigned,unsigned){return roll;}
struct Guid{unsigned GetCounter(){return 26;}};
struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}}map;
struct Player{
unsigned race=22,level=60,flags=0,characterFlags=0,display=0,casts=0,updates=0;
bool world=true,alive=true,combat=false,mounted=false,transport=false,taxi=false,flying=false,teleport=false,casting=false,outdoors=true,pvp=false,fail=false;
float speed=1;std::unordered_set<unsigned>spells,auras;struct{unsigned EndTimer=10;}pvpInfo;
Map*GetMap(){return &map;}Guid GetGUID(){return {};}std::string GetName(){return "Test";}bool IsInWorld(){return world;}void AddCharacterFlag(int f){characterFlags|=f;}void SetFlag(int,int f){flags|=f;}
bool IsPvP(){return pvp;}void UpdatePvP(bool on,bool){pvp=on;pvpInfo.EndTimer=0;++updates;}
unsigned getRace(){return race;}unsigned getLevel(){return level;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}
bool IsMounted(){return mounted;}bool GetTransport(){return transport;}bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}
bool IsBeingTeleported(){return teleport;}bool HasUnitState(int){return casting;}bool HasAuraType(int){return auras.count(97709);}
bool HasSpell(unsigned id){return spells.count(id);}void LearnSpell(unsigned id,bool){spells.insert(id);}
bool HasAura(unsigned id){return auras.count(id);}bool IsOutdoors(){return outdoors;}
void CastSpell(Player*,unsigned id,bool){++casts;if(fail)return;if(id==97709)auras.insert(id);if(id==68996)auras.erase(97709);if(id==87840){auras.insert(id);mounted=true;display=29422;speed=2;}}
void Mount(unsigned id){display=id;mounted=true;}void SetSpeedRate(int,float v){speed=v;}unsigned GetMountID(){return display;}
void RemoveAurasDueToSpell(unsigned id){auras.erase(id);}void Dismount(){mounted=false;display=0;}void UpdateSpeed(int){speed=1;}
};
struct ClientlessAdventureProfile{bool CanGroundMount=true;std::vector<int>GroundMounts{2404};float GroundMountMinDistance=40,GroundMountSpeedRate=2;}profile;
struct ClientlessLuaEngine{
std::unordered_map<unsigned,bool>_worgenForms;std::unordered_map<unsigned,time_t>_worgenNextAttempt;
ClientlessAdventureProfile*GetAdventureProfile(unsigned){return &profile;}unsigned SelectGroundMountDisplayId(unsigned){return 2404;}
void UpdateWorldPresence(Player*,std::string const&,bool=false);bool PrepareGroundMount(Player*,float,char const*);void DismountGroundMount(Player*,char const*);
};
'''+fn('void ClientlessLuaEngine::UpdateWorldPresence(')+fn('bool ClientlessLuaEngine::PrepareGroundMount(')+fn('void ClientlessLuaEngine::DismountGroundMount(')+r'''
int main(){
ClientlessLuaEngine e;Player p;e.UpdateWorldPresence(&p,"adventure");
assert(p.pvp&&p.pvpInfo.EndTimer==0&&(p.flags&512)&&(p.characterFlags&65536)&&p.HasAura(97709));
assert(e.PrepareGroundMount(&p,100,"test")&&p.HasAura(87840)&&p.IsMounted()&&p.display==29422);
e.DismountGroundMount(&p,"test");assert(!p.IsMounted()&&!p.HasAura(87840)&&p.speed==1);
GameTime::now+=6;roll=0;e.UpdateWorldPresence(&p,"leisure",true);assert(!p.HasAura(97709));
assert(e.PrepareGroundMount(&p,100,"test")&&p.display==2404&&!p.HasAura(87840));
e.DismountGroundMount(&p,"test");
p.transport=true;GameTime::now+=6;e.UpdateWorldPresence(&p,"raid");assert(!p.HasAura(97709));
p.transport=false;e.UpdateWorldPresence(&p,"raid");assert(p.HasAura(97709));
// Decide quiet arrival once, defer while mounted, never reroll every update.
p.mounted=true;roll=0;e.UpdateWorldPresence(&p,"rest",true);assert(p.HasAura(97709));
p.mounted=false;GameTime::now+=6;roll=1;e.UpdateWorldPresence(&p,"rest");assert(!p.HasAura(97709));
// Combat and mixed player instances require Worgen even with a quiet personal intent.
GameTime::now+=6;p.combat=true;e.UpdateWorldPresence(&p,"rest");assert(p.HasAura(97709));p.combat=false;
p.auras.clear();map.dungeon=true;GameTime::now+=6;e.UpdateWorldPresence(&p,"rest");assert(p.HasAura(97709));map.dungeon=false;p.auras.clear();
// Failed native casts are throttled, not spammed every world tick.
p.fail=true;GameTime::now+=6;e.UpdateWorldPresence(&p,"instance");auto casts=p.casts;
e.UpdateWorldPresence(&p,"instance");assert(p.casts==casts);
p.race=1;p.pvp=false;p.pvpInfo.EndTimer=5;e.UpdateWorldPresence(&p,"rest");assert(p.pvp&&!p.pvpInfo.EndTimer);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production world presence: persistent PvP, Worgen intent/arrival decisions, travel deferral, native Running Wild, human horse and aura cleanup passed')
+3 -2
View File
@@ -7,12 +7,13 @@ a=s.index('std::pair < uint32, uint32 > ClientlessFlightMgr::AreaZone(')
b=s.index('bool ClientlessFlightMgr::Enabled(',a)
code=r'''
#include <cassert>
#include <cstring>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <memory>
using uint32=uint32_t;
struct Position{float GetPositionX()const{return 1;}float GetPositionY()const{return 2;}float GetPositionZ()const{return 3;}};
struct Position{float x=1,y=2,z=3;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}};
struct Player{int GetPhaseShift()const{return 17;}};
struct TerrainInfo{int calls=0;void GetZoneAndAreaId(int phase,uint32 map,uint32& zone,uint32& area,Position const&){assert(phase==17&&map==530);++calls;zone=3430;area=3460;}};
struct TerrainMgr{std::shared_ptr<TerrainInfo> value=std::make_shared<TerrainInfo>();auto LoadTerrain(uint32 map){assert(map==530);return value;}} sTerrainMgr;
@@ -23,7 +24,7 @@ class ClientlessFlightMgr{public:
};
'''
code+=s[a:b]+r'''
int main(){ClientlessFlightMgr mgr;Player p;Position pos;auto r=mgr.AreaZone(&p,530,pos);assert(r.first==3460&&r.second==3430);assert(mgr.AreaZone(&p,530,pos)==r);assert(sTerrainMgr.value->calls==1);}
int main(){ClientlessFlightMgr mgr;Player p;Position pos;auto r=mgr.AreaZone(&p,530,pos);assert(r.first==3460&&r.second==3430);assert(mgr.AreaZone(&p,530,pos)==r);assert(sTerrainMgr.value->calls==1);pos.x=1.0000001f;mgr.AreaZone(&p,530,pos);assert(sTerrainMgr.value->calls==2);}
'''
with tempfile.TemporaryDirectory(prefix='vcore-area-query-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
+19 -2
View File
@@ -16,6 +16,8 @@ harness=r'''
#include <string>
#include <vector>
#include <sstream>
#include <unordered_set>
#include <unordered_map>
#include <limits>
using uint32=uint32_t;
using namespace ClientlessFlightPath;
@@ -39,7 +41,7 @@ struct Player{
struct Mount{uint32 DisplayId=32235,Weight=100;};
struct Profile{bool CanFreeFly=true;std::vector<Mount> FlightMounts{{}};} profile;
struct Lua{auto GetAdventureProfile(uint32){return &profile;}} lua;
struct Config{bool enabled=true;bool GetBoolDefault(const char*,bool){return enabled;}float GetFloatDefault(const char*,float v){return v;}std::string GetStringDefault(const char*,const char*v){return v;}} config;
struct Config{int reads=0;bool enabled=true;bool GetBoolDefault(const char*,bool){++reads;return enabled;}float GetFloatDefault(const char*,float v){++reads;return v;}std::string GetStringDefault(const char*,const char*v){++reads;return v;}} config;
auto sClientlessLuaEngine=&lua;auto sConfigMgr=&config;
struct SpellMgr{void* GetSpellInfo(uint32){return this;}} spells;auto sSpellMgr=&spells;
uint32 FlightType(void*){return 1;}
@@ -54,6 +56,10 @@ struct Record{std::shared_ptr<ClientlessFlightPath::SharedTrail>Published=std::m
bool Moving=true,Returning=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
};
class ClientlessFlightMgr{public:
Player const* _planningPlayer=nullptr;bool _planningEnabled=false;float _planningMinDistance=600;
std::unordered_set<uint32> _planningPreferredZones;
std::unordered_map<int,int> _planningAreas,_planningTerrain;
void BeginPlanning(Player const* = nullptr);
uint32 areaCode=87,zoneCode=12;
std::pair<uint32,uint32> AreaZone(Player const*,uint32,Position const&)const{return {areaCode,zoneCode};}
bool Enabled(Player const*)const;bool Preferred(Player const*,uint32,Position const&)const;
@@ -61,7 +67,7 @@ class ClientlessFlightMgr{public:
void Fail(Player*,Record&,std::string const&);
};
'''
for signature in ('bool ClientlessFlightMgr::Enabled(', 'bool ClientlessFlightMgr::Preferred(', 'float ClientlessFlightMgr::Estimate(', 'void ClientlessFlightMgr::Fail('):
for signature in ('void ClientlessFlightMgr::BeginPlanning(', 'bool ClientlessFlightMgr::Enabled(', 'bool ClientlessFlightMgr::Preferred(', 'float ClientlessFlightMgr::Estimate(', 'void ClientlessFlightMgr::Fail('):
harness+=function(signature)+'\n'
harness+=r'''
int main(){
@@ -95,6 +101,17 @@ int main(){
mgr.Fail(&p,r,"reverse blocked");assert(r.Status.State==ClientlessFlightState::Failed);
Record mounting;mounting.Status.State=ClientlessFlightState::Mounting;mgr.Fail(&p,mounting,"cast rejected");
assert(mounting.Status.State==ClientlessFlightState::Failed&&p.interrupts==1&&!mounting.Returning);
// A planning scope snapshots repeated config/profile work, while actual
// departure checks and per-map licenses remain live.
profile.CanFreeFly=true;p.skill=225;p.learned=true;p.license=true;config.enabled=true;
area.fly=true;areas.known=true;p.restrictions=false;mgr.zoneCode=12;b.x=1000;
mgr.BeginPlanning(&p);int reads=config.reads;
for(int i=0;i<1000;++i)assert(std::isfinite(mgr.Estimate(&p,0,a,b)));
assert(config.reads==reads);
p.license=false;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));p.license=true;
profile.CanFreeFly=false;assert(!mgr.Enabled(&p));
mgr.BeginPlanning();assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));
profile.CanFreeFly=true;
Record revoked;p.canFly=false;mgr.Fail(&p,revoked,"permission lost");assert(revoked.Status.State==ClientlessFlightState::Failed&&!revoked.Returning);
}
'''
+20 -5
View File
@@ -8,6 +8,9 @@ structs=h[h.index('struct TC_GAME_API ClientlessTravelNode'):h.index('class TC_G
method=s[s.index('bool ClientlessTravelGraphMgr::PlanAdaptiveRoute('):s.index('bool ClientlessTravelGraphMgr::PlanRouteToPosition(')]
signature=h[h.index(' bool PlanAdaptiveRoute('):h.index(' bool FindNearestNodeToPosition(')]
preamble=r'''
#include "ClientlessGraphProbeCache.h"
#include <chrono>
#include <map>
#include <algorithm>
#include <cassert>
#include <cmath>
@@ -24,7 +27,7 @@ using uint32=uint32_t; using int32=int32_t;
#define TC_LOG_DEBUG(...) do {} while(0)
#define TC_LOG_WARN(...) do {} while(0)
constexpr uint32 NAV_GROUND=1, PATHFIND_INCOMPLETE=2, PATHFIND_NOPATH=4;
struct Position { float x=0,y=0,z=0; void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
struct Position { float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;} void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
struct Player { float x=0,y=0,z=0; bool IsInWorld() const{return true;} uint32 GetMapId()const{return 0;} uint32 GetInstanceId()const{return 0;} float GetPositionX()const{return x;} float GetPositionY()const{return y;} float GetPositionZ()const{return z;} };
float dist(Position a,Position b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious=false; uint32 PathType=1,PointCount=2; float DirectDistance=0,PathLength=0,MaxSegmentLength=0,MaxZDelta=0,FinalDistance=0; std::string Reason;};
@@ -32,18 +35,19 @@ struct Travel {void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics&
auto sClientlessTravelMgr=&travel;
struct Flight {
bool enabled=false;float minimumX=-10000;
void BeginPlanning(){}
void BeginPlanning(Player const* = nullptr){}
bool Enabled(Player const*){return enabled;}
float Estimate(Player const*,uint32 map,Position a,Position b){
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&d>=60?180+d*1.5f:std::numeric_limits<float>::infinity();
}
} flight;
auto sClientlessFlightMgr=&flight;
struct VcorePositionPathResult {bool complete=false; uint32 Result=2;float FinalDistance=0,PathLength=0;bool IsComplete()const{return complete;}};
struct PathGenerator {static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){float d=dist(a,b);return {available && d<=maxDistance,2,0,d};}};
struct VcorePositionPathResult {enum class Status{Complete,Partial};Status Result=Status::Partial;float FinalDistance=0,PathLength=0,DirectDistance=0;bool IsComplete()const{return Result==Status::Complete;}};
struct PathGenerator {static inline unsigned probes=0;static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){++probes;float d=dist(a,b);return {available && d<=maxDistance?VcorePositionPathResult::Status::Complete:VcorePositionPathResult::Status::Partial,0,d,d};}};
std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;}
'''
cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+'''
ClientlessGraphProbeCache _positionProbes;std::unordered_map<std::string,std::vector<std::string>> _bridgeNeighborhoods;
std::unordered_map<std::string,ClientlessTravelNode> _nodes;
std::unordered_map<std::string,std::vector<ClientlessTravelEdge>> _edges;
std::unordered_map<uint32,std::vector<ClientlessTravelEdgePoint>> _edgePoints;
@@ -107,6 +111,17 @@ assert(exits.PlanAdaptiveRoute(&start,"D",1,200,route,done,endKey,why));
assert(done&&route.back().NodeKey=="D"&&route.back().EdgeType=="free_flight");
assert(route[route.size()-2].NodeKey=="virtual_ground_exit");
for(auto const& step:route)if(step.EdgeType=="free_flight")assert(step.NodeKey!="virtual_ground_entry");
// A second plan reuses static proofs, but still honors query-specific failures.
flight.enabled=false;PathGenerator::available=true;PathGenerator::maxDistance=10000;
ClientlessTravelGraphMgr cached;node(cached,"A",0);node(cached,"B",150);
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
auto calls=PathGenerator::probes;auto size=route.size();
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done&&route.size()==size);
assert(PathGenerator::probes==calls&&cached._positionProbes.Hits>0);
std::unordered_set<std::string> forbidden{"A->B"};
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,&forbidden)&&!done);
cached._positionProbes.Clear();cached._bridgeNeighborhoods.clear();PathGenerator::available=false;
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&!done);
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
}
'''
@@ -115,5 +130,5 @@ tests=tests.replace('r=plan(g,p,goal,1);assert(r.back().GroundStartIndex<0); //
with tempfile.TemporaryDirectory(prefix="vcore-flight-planner-") as folder:
cpp=Path(folder)/"test.cpp";binary=Path(folder)/"test"
cpp.write_text(preamble+structs+cls+method+tests)
subprocess.run(["c++","-std=c++17","-O0",str(cpp),"-o",str(binary)],check=True)
subprocess.run(["c++","-std=c++17","-O0","-I"+str(root/"src/server/game/World"),str(cpp),"-o",str(binary)],check=True)
subprocess.run([str(binary)],check=True)
@@ -0,0 +1,28 @@
"""Static MMAP proof cache isolation, failure handling and bounded lifetime."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "ClientlessGraphProbeCache.h"
#include <cassert>
#include <limits>
int main(){
ClientlessGraphProbeCache c;auto key=c.MakeKey(0,1,1,2,3,4,5,6);
assert(!c.Find(key));c.Record(key,false,10,0,10);assert(!c.Find(key));
c.Record(key,true,10,2,10);assert(!c.Find(key));
c.Record(key,true,std::numeric_limits<float>::infinity(),0,10);assert(!c.Find(key));
c.Record(key,true,10,0,9);assert(c.Find(key)&&c.Find(key)->Length==10);
assert(!c.Find(c.MakeKey(1,1,1,2,3,4,5,6))); // map isolation
assert(!c.Find(c.MakeKey(0,2,1,2,3,4,5,6))); // instance isolation
assert(!c.Find(c.MakeKey(0,1,4,5,6,1,2,3))); // directed connection
assert(!c.Find(c.MakeKey(0,1,1.0000001f,2,3,4,5,6))); // exact coordinates
c.Clear();assert(!c.Find(key)&&c.Size()==0);
for(unsigned i=0;i<32769;++i)c.Record(c.MakeKey(i,1,1,2,3,4,5,6),true,10,0,9);
assert(c.Size()==1&&c.Find(c.MakeKey(32768,1,1,2,3,4,5,6)));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-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('Graph probe cache: exact directed/map/instance keys, successful geometry only, invalidation and bounded storage passed')