#!/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 #include #include #include #include #include #include #include #include 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;} 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 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 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 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 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_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')