Files
Viano-Core/tests/clientless/test_adventure_intent_state.py
T
Nico 0173a434f4 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.
2026-10-29 01:36:35 +01:00

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 &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;}
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.')