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.
132 lines
8.3 KiB
Python
132 lines
8.3 KiB
Python
"""Execute the production intent state machine with deterministic clock/travel doubles."""
|
|
from pathlib import Path
|
|
import subprocess,tempfile
|
|
root=Path(__file__).resolve().parents[2]
|
|
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
|
a=s.index('void AdventureBehaviorMgr::Update(');b=s.index('bool AdventureBehaviorMgr::Command(',a)
|
|
code=r'''
|
|
#include "AdventureBehaviorPolicy.h"
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
using uint32=uint32_t;using uint64=uint64_t;
|
|
#define TC_LOG_INFO(...) ((void)0)
|
|
constexpr int SPELL_VISUAL_KIT_DRINK=438,UNIT_NPC_EMOTESTATE=0;
|
|
struct Position {float x=0;void Relocate(void const*){}};
|
|
struct Guid{uint32 GetCounter(){return 26;}bool operator!=(Guid)const{return false;}};
|
|
struct Group{Guid GetLeaderGUID(){return {};}};
|
|
struct Object{} object;
|
|
struct Map{Object* GetGameObject(int){return &object;}} map;
|
|
struct Player {
|
|
bool combat=false;uint32 area=87,zone=12,mapId=0;
|
|
Group*GetGroup(){return nullptr;}Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}
|
|
uint32 GetMapId(){return mapId;}uint32 GetAreaId(){return area;}uint32 GetZoneId(){return zone;}
|
|
float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 0;}
|
|
float GetExactDist(Position const&){return 0;}float GetExactDist(float,float,float){return 0;}float GetExactDist(Object*){return 0;}
|
|
void SendPlaySpellVisualKit(int,int,int){}void SetUInt32Value(int,uint32){}
|
|
bool IsWithinLOSInMap(Object*){return true;}void SetFacingToObject(Object*){}
|
|
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;}
|
|
bool busy=false;bool Busy(uint32){return busy;}
|
|
time_t now=100;int hour=5;
|
|
namespace GameTime {time_t GetGameTime(){return now;}}
|
|
std::tm Local(time_t){std::tm t{};t.tm_year=126;t.tm_mon=9;t.tm_mday=26;t.tm_hour=hour;return t;}
|
|
uint32 DateOf(std::tm const&){return 20261026;}
|
|
enum class ClientlessActionExecutionState{Idle,Moving,Completed,Failed};
|
|
struct Status {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;std::string Detail;};
|
|
struct Executor{Status status;auto GetStatus(uint32){return &status;}} executor;auto sClientlessActionExecutorMgr=&executor;
|
|
struct Manager{uint32 queued=0;uint32 GetQueuedActionCount(uint32){return queued;}} manager;auto sClientlessPlayerMgr=&manager;
|
|
struct Graph{
|
|
bool nearest=true;void EnsureLoaded(){}bool FindNearestNodeToPosition(uint32,float,float,float,std::string& key,float& distance){key="node";distance=10;return nearest;}
|
|
} graph;auto sClientlessTravelGraphMgr=&graph;
|
|
struct AdventureLocation{struct{uint32 MapId=0;float X=0,Y=0,Z=0;}Point;bool IsArea=false;uint32 AreaId=12;} location;
|
|
struct Locations{auto Find(std::string const&){return &location;}} locations;auto sAdventureLocationMgr=&locations;
|
|
enum class ClientlessTravelState{Moving,Arrived};
|
|
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Arrived;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
|
|
class AdventureBehaviorMgr {
|
|
public:
|
|
enum class Phase{Idle,Travel,OnSite,Switching,Handover};
|
|
struct Record{
|
|
bool Enabled=true,Schedule=false,Queued=false,Dirty=false;
|
|
Phase State=Phase::Travel;
|
|
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation;
|
|
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
|
|
struct{bool Active=false;}Social;
|
|
struct{bool Active=false;}Recovery;
|
|
struct{bool Active=false;}Instance;
|
|
struct{bool Active=false,Flying=false,Progressive=false;std::vector<Position> Route;size_t Step=0;}Leisure;
|
|
AdventureBehaviorPolicy::Day Plan;Position Anchor;uint32 AnchorMap=0;int WorkObject=0;uint32 WorkEmote=0;bool WorkPending=false;
|
|
};
|
|
std::unordered_map<uint32,Record> _records{{26,{}}};
|
|
int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0;
|
|
bool AdoptGroup(Player*,Player*){return false;}
|
|
void Update(Player*,uint32);
|
|
void EnterPlayerInstance(Player*);
|
|
void Generate(Player*,Record&,uint32){}
|
|
void SetWanted(Player*,Record& r,std::string const&i,std::string const&l,time_t d){++switches;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=d;r.Dirty=true;r.State=Phase::Idle;}
|
|
void Begin(Player*,Record&){++begun;}
|
|
void Fallback(Player*,Record&){++fallbacks;}
|
|
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();}
|
|
void Micro(Player*,Record&){++micros;}
|
|
void FinishInstance(Player*,Record&r){r.Instance.Active=false;}
|
|
bool UpdateInstance(Player*,Record&,time_t){return false;}
|
|
bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;}
|
|
bool UpdateSocial(Player*,Record&,uint32){return true;}
|
|
bool UpdateRecovery(Player*,Record&,uint32){return true;}
|
|
void Queue(Player*,Record&,std::string const&,std::string const&,std::string const&,bool=false){++queues;}
|
|
};
|
|
'''
|
|
start=s.index('void AdventureBehaviorMgr::EnterPlayerInstance(');end=s.index('void AdventureBehaviorMgr::RefreshDay(',start)
|
|
code+=s[start:end]
|
|
code+=s[a:b]+r'''
|
|
int main(){
|
|
AdventureBehaviorMgr mgr;auto& r=mgr._records.at(26);
|
|
r.Instance.Active=true;mgr.EnterPlayerInstance(&player);assert(!r.Instance.Active);
|
|
encounters.active=true;r.Schedule=true;r.Deadline=1;r.Plan[6]={"profession","mine"};hour=6;
|
|
mgr.Update(&player,1000);assert(mgr.switches==0 && mgr.micros==0 && r.Hour==5);
|
|
encounters.active=false;r.Schedule=false;r.Deadline=1000;hour=5;
|
|
// A completed travel at the exact location opens micro actions.
|
|
mgr.Update(&player,1000);assert(r.State==AdventureBehaviorMgr::Phase::OnSite&&mgr.micros==1);
|
|
// Best-effort graph progress must not pretend an area has been reached.
|
|
r.State=AdventureBehaviorMgr::Phase::Travel;location.IsArea=true;
|
|
executor.status.Detail="best-effort travel completed at nearest useful reachable point";
|
|
mgr.Update(&player,1000);assert(mgr.fallbacks==1);
|
|
location.IsArea=false;executor.status.Detail="";
|
|
// A failed route triggers replacement selection only after the executor finishes.
|
|
executor.status.State=ClientlessActionExecutionState::Failed;
|
|
mgr.Update(&player,1000);assert(mgr.fallbacks==2);
|
|
busy=true;mgr.Update(&player,1000);assert(mgr.fallbacks==2);busy=false;
|
|
// Hour switch occurs despite an active transport/flight, without starting a new trip.
|
|
r.Schedule=true;r.Plan[6]={"profession","mine"};hour=6;unsafe=true;
|
|
mgr.Update(&player,1000);assert(mgr.switches==1&&mgr.begun==0);
|
|
unsafe=false;r.Schedule=false;r.Deadline=1000;
|
|
// Safe handover waits for active travel, then queues a hard node destination.
|
|
r.State=AdventureBehaviorMgr::Phase::Switching;busy=true;
|
|
mgr.Update(&player,1000);assert(mgr.queues==0);busy=false;
|
|
mgr.Update(&player,1000);assert(mgr.queues==1&&r.State==AdventureBehaviorMgr::Phase::Handover);
|
|
// Deadline discards the old intent rather than extending the visit.
|
|
now=1001;r.State=AdventureBehaviorMgr::Phase::OnSite;
|
|
mgr.Update(&player,1000);assert(mgr.switches==2&&r.WantedIntent.empty()&&mgr.begun==1);
|
|
// Leashed companion distance is serviced at world cadence, not the 1 s planning clock.
|
|
r.Timer=1000;r.Leisure.Active=true;mgr.Update(&player,50);
|
|
assert(mgr.leisureUpdates==1&&r.Timer==950);
|
|
// Preparation ends on its deadline without waiting for a full/assembled party.
|
|
r.Leisure.Active=false;r.Timer=0;r.Intent="groupformation";r.Deadline=now;
|
|
r.ManualNextIntent="social";r.ManualNextLocation="leader-goal";
|
|
mgr.Update(&player,1000);
|
|
assert(r.WantedIntent=="social"&&r.WantedLocation=="leader-goal"&&r.Deadline==now+3600&&r.ManualNextIntent.empty());
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='vcore-adventure-state-') as folder:
|
|
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
|
|
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
|
subprocess.run([str(binary)],check=True)
|
|
print('Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.')
|