Files
Viano-Core/tests/clientless/test_adventure_intent_state.py
T
Nico 54d77d1e4d
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Fix AdventureBot group preparation, public portals and flight recovery
Add preparation outings and equipment checks with a five-minute return to the meeting point. Correct static portal visitor casting and level conditions, with native interaction diagnostics. Recover suspended flying mounts, walk to verified takeoff sites, and validate landing and return volumes. Include SQL migrations, targeted regression tests and live validation checkpoints.
2026-10-29 18:21:43 +01:00

136 lines
8.6 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;
struct Areas{unsigned Revision(){return 0;}} areas;auto sAdventureAreaMgr=&areas;
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 AreaRevision=0,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 Allowed(Player*,AdventureLocation const&,std::string const&){return true;}
bool AdoptGroup(Player*,Player*){return false;}
bool UpdateNavigationRescue(Player*,Record&){return false;}
bool UpdateGathering(Player*,Record&,time_t){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.')