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:
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);
|
||||
|
||||
@@ -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 ↦}
|
||||
} 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;}
|
||||
|
||||
@@ -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')
|
||||
@@ -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 ↦}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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user