Improve AdventureBot planning and travel performance

This commit is contained in:
Nico committed 2026-10-31 15:35:46 +01:00
1 parent 69b5a1c4a1
commit d1ef007cef
18 files changed
+906 -27

No files matched your search

@@ -55,15 +55,15 @@ namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*>players;Player*Fi
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
struct Areas{void*LookupEntry(unsigned){return nullptr;}}sAreaTriggerStore;
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed,Failed};
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)>){}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0,adapters=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)> adapter){adapters+=adapter?1:-1;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
struct Core{unsigned queued=0;std::vector<std::string>nodes;unsigned GetQueuedActionCount(unsigned){return queued;}bool QueueAction(unsigned,ClientlessAction action){++queued;nodes.push_back(action.Parameters["node"]);return true;}void ClearQueuedActions(unsigned){queued=0;}}core;auto sClientlessPlayerMgr=&core;
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=20;};
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0;std::vector<Position>targets;
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0,validators=0;std::vector<Position>targets;
auto GetStatus(unsigned){return &status;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&d){d.Complete=valid;return valid;}
bool StartTravel(Player*,Position const&target,bool=true,bool=false){++starts;targets.push_back(target);status.Target=target;status.State=ClientlessTravelState::Moving;return true;}
std::vector<Position>const*GetActivePath(unsigned){return nullptr;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}void SetPathValidator(unsigned,std::function<bool(Player*,std::vector<Position>const&)>){}
std::vector<Position>const*GetActivePath(unsigned){return nullptr;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}void SetPathValidator(unsigned,std::function<bool(Player*,std::vector<Position>const&)> validator){validators+=validator?1:-1;}
}travel;auto sClientlessTravelMgr=&travel;
struct PathGenerator{struct Point{float x,y,z;};Player*p;std::vector<Point>points;PathGenerator(Player*who):p(who){}bool CalculatePath(float x,float y,float z){points={{p->GetPositionX(),p->GetPositionY(),p->GetPositionZ()},{x,y,z}};return travel.valid;}auto const&GetPath(){return points;}};
struct Graph{bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&n,float&distance){n="nearest";distance=5;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
@@ -150,6 +150,12 @@ int main(){
leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed;
// Recorded roads are bounded and validated, never a spline through a cliff.
std::vector<Position>road;for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);leader.moving=false;map.cliff=true;assert(!mgr.AdaptRoad(&a,road));map.cliff=false;road.clear();for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);assert(mgr.AdaptRoad(&a,road) && road.back().GetPositionX()<=60);
// Leaving a group clears formation-owned adapters and validators before the
// next autonomous route. This prevents valid MMAP paths being rejected by a
// stale group formation safety check.
assert(mgr._records.count(26)&&executor.adapters==2&&travel.validators==2);
auto cancelsBefore=travel.cancels;a.g=nullptr;mgr.Observe(&a,500);
assert(!mgr._records.count(26)&&executor.adapters==1&&travel.validators==1&&travel.cancels==cancelsBefore+1);
}
'''
with tempfile.TemporaryDirectory() as d:
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Exercise the production parser and plan editor, including persistence and preview."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
start=source.index('bool AdventureBehaviorMgr::EditPlan(')
end=len(source)
code=r'''
#include "AdventurePlanCommand.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <ctime>
#include <map>
#include <string>
#include <vector>
#include <cstdarg>
#include <cstdio>
using uint32=unsigned;constexpr unsigned TEAM_ALLIANCE=0;
namespace AdventureGroupPolicy{bool Eligible(std::string const&s){return s=="instance"||s=="raid"||s=="adventure";}}
namespace AdventureBehaviorPolicy{struct Slot{std::string Intent,Location;};using Day=std::array<Slot,24>;}
struct Player{unsigned guid,level=60,team=0;bool world=true;bool IsInWorld(){return world;}unsigned getLevel(){return level;}unsigned GetTeamId(){return team;}};
struct Session{Player* player;Player*GetPlayer(){return player;}};
struct World{std::map<unsigned,Session> sessions;Session*FindClientlessSession(unsigned g){auto i=sessions.find(g);return i==sessions.end()?nullptr:&i->second;}} world;auto sWorld=&world;
struct Managed{bool IsManaged(unsigned){return true;}} managed;auto sClientlessPlayerMgr=&managed;
struct Location{unsigned ZoneId;struct{std::string Id;}Point;};
struct Locations{std::vector<Location> locations{{1519,{"sw"}},{1637,{"org"}},{1,{"dungeon"}},{1,{"raid"}},{1,{"forbidden"}}};
bool EnsureLoaded(){return true;}auto const& Locations(){return locations;}Location const*Find(std::string const& key){for(auto&l:locations)if(l.Point.Id==key)return &l;return nullptr;}} locations;auto sAdventureLocationMgr=&locations;
struct Tx{std::vector<std::string> sql;void PAppend(char const*fmt,...){char buf[1024];va_list ap;va_start(ap,fmt);vsnprintf(buf,sizeof(buf),fmt,ap);va_end(ap);sql.push_back(buf);}};
struct Database{Tx tx;unsigned commits=0;Tx*BeginTransaction(){tx.sql.clear();return &tx;}void EscapeString(std::string&){}void DirectCommitTransaction(Tx*&){++commits;}}WorldDatabase;
struct ChatHandler{std::vector<std::string> messages;void SendSysMessage(char const*s){messages.push_back(s);}void PSendSysMessage(char const*fmt,...){char buf[1024];va_list ap;va_start(ap,fmt);vsnprintf(buf,sizeof(buf),fmt,ap);va_end(ap);messages.push_back(buf);}};
namespace GameTime{long GetGameTime(){return 100000;}}
std::tm Local(long){std::tm tm{};tm.tm_year=126;tm.tm_mon=9;tm.tm_mday=31;tm.tm_hour=10;return tm;}
unsigned DateOf(std::tm const&){return 20261031;}
bool Known(std::string const&s){return s=="social"||s=="adventure"||s=="instance"||s=="raid"||s=="groupformation";}
struct AdventureBehaviorMgr{
struct DepartureState{long At=0;bool departed=false;bool Departed(long)const{return departed;}};
struct Record{unsigned Date=20261031;bool Schedule=true,Enabled=true;int Hour=10;std::string ManualNextIntent,ManualNextLocation;AdventureBehaviorPolicy::Day Plan;DepartureState Departure;unsigned picks=0;};
std::map<unsigned,Record>_records;unsigned switches=0,clears=0;
bool EditPlan(ChatHandler*,std::string const&);
bool Allowed(Player*p,Location const&l,std::string const&i){return l.Point.Id!="forbidden"&&(i!="raid"||p->level>=60)&&(l.Point.Id!="sw"||p->team==0)&&(l.Point.Id!="org"||p->team==1);}
std::string PickLocation(Player*p,Record&r,std::string const&i){++r.picks;if(i=="raid"&&p->level<60)return "";return i=="instance"?"dungeon":i=="raid"?"raid":p->team?"org":"sw";}
void ClearDeparture(Player*,Record&r){++clears;r.Departure={};}void SetWanted(Player*,Record&,std::string const&,std::string const&,long){++switches;}
};
'''.replace('struct Locations{','struct LocationCatalog{').replace('}} locations;', '}} locations;')
# Class and member names must differ in C++.
code+=source[start:end]
code+=r'''
int main(){
using namespace AdventurePlanCommand;Request q;
assert(Parse("all current social capital both 1 85 preview",10,q));assert(q.First==10&&q.Last==10&&q.Preview);
assert(Parse("43 next+2 raid auto alliance 60 85 apply",10,q));assert(q.First==13&&q.Guid==43&&q.Faction==1);
assert(Parse("all 12-15 keep auto horde 1 85 apply",10,q)&&q.Last==15);
for(auto s:{"all next social auto both 0 85 apply","all next social auto both 85 1 apply","all next social auto both 1 86 apply","all 9 social auto both 1 85 apply","all next social auto both 1 85 typo","0 next social auto both 1 85 apply","all 14-12 social auto both 1 85 apply","all next social auto both 1 85 apply extra","4294967296 next social auto both 1 85 apply"})assert(!Parse(s,10,q));
assert(!Parse("all next social auto both 1 85 apply",23,q));assert(!Parse("all next+23 social auto both 1 85 apply",10,q));
assert(Parse("43 next raid auto alliance 60 85 apply",10,q));assert(Matches(q,43,60,1));assert(!Matches(q,44,60,1));assert(!Matches(q,43,59,1));assert(!Matches(q,43,60,2));
Player a{43,60,0},b{44,30,1},c{45,60,0};world.sessions={{43,{&a}},{44,{&b}},{45,{&c}}};
AdventureBehaviorMgr mgr;for(auto g:{43,44,45}){auto&r=mgr._records[g];for(auto&s:r.Plan)s={"social","sw"};}mgr._records[45].Date=20261030;
ChatHandler h;
assert(mgr.EditPlan(&h,"all next raid auto both 1 85 preview"));assert(WorldDatabase.commits==0&&mgr.switches==0);assert(mgr._records[43].picks==0&&mgr._records[43].Plan[11].Intent=="social");
assert(mgr.EditPlan(&h,"all next raid auto both 1 85 apply"));assert(WorldDatabase.commits==1&&WorldDatabase.tx.sql.size()==1);assert(mgr._records[43].Plan[11].Intent=="raid");assert(mgr._records[44].Plan[11].Intent=="social");assert(mgr.switches==0);
assert(mgr.EditPlan(&h,"44 next instance-or-raid auto horde 15 59 apply"));assert(mgr._records[44].Plan[11].Intent=="instance");
assert(mgr.EditPlan(&h,"43 12-14 adventure capital alliance 60 60 apply"));assert(WorldDatabase.tx.sql.size()==3);assert(mgr._records[43].Plan[14].Location=="sw");assert(mgr._records[43].Plan[15].Intent=="social");
auto commits=WorldDatabase.commits;assert(mgr.EditPlan(&h,"43 next raid forbidden both 1 85 apply"));assert(WorldDatabase.commits==commits);
assert(mgr.EditPlan(&h,"43 22-23 groupformation capital both 1 85 apply"));assert(WorldDatabase.commits==commits&&mgr._records[43].Plan[22].Intent=="social");
assert(mgr.EditPlan(&h,"43 current adventure capital both 1 85 apply"));assert(mgr.switches==1&&mgr.clears==1&&mgr._records[43].Hour==-1);
assert(WorldDatabase.tx.sql[0].find("20261031,10,'adventure','sw'")!=std::string::npos);
// Formation validates its next block, not just the meeting location.
mgr._records[43].Plan[11]={"social","sw"};auto rejected=WorldDatabase.commits;
assert(mgr.EditPlan(&h,"43 current groupformation capital both 1 85 apply"));assert(WorldDatabase.commits==rejected);
mgr._records[43].Plan[11]={"raid","raid"};assert(mgr.EditPlan(&h,"43 current groupformation capital both 1 85 apply"));assert(mgr._records[43].Plan[10].Intent=="groupformation");
mgr._records[43].Departure.At=1;mgr._records[43].Departure.departed=true;auto cleared=mgr.clears;
assert(mgr.EditPlan(&h,"43 next adventure auto both 1 85 apply"));assert(mgr.clears==cleared);
mgr._records[43].Departure.departed=false;assert(mgr.EditPlan(&h,"43 next adventure auto both 1 85 apply"));assert(mgr.clears==cleared+1);
assert(!mgr.EditPlan(&h,"all current nonsense capital both 1 85 apply"));
}
'''
with tempfile.TemporaryDirectory() as td:
p=Path(td);(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('Adventure plan commands: PASS')