Files
Viano-Core/tests/clientless/test_adventure_intent_state.py
T
Nico 3aa780662e Complete AdventureBot navigation, instance travel and shutdown fixes
Finish capital waiting travel and adaptive flight entry up to 10,000 yards.
Preserve checked vertical climbs, recover blocked ground departures and find
validated dry landings. Retain group routes and Undercity arrival constraints.

Restore defense from native combat references, coordinate instance rendezvous
and browser reentry, finalize clientless sessions before map destruction and
transfer Halaa capture-point ownership safely.

Include the associated regression tests, investigation reports and reusable
diagnostic scripts. Keep generated runtime evidence and server binaries local
through scoped ignore rules. Validation/build/live evidence is recorded in the
session checkpoint and individual reports.
2026-11-05 10:35:34 +01:00

166 lines
11 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('void AdventureBehaviorMgr::UpdateActivities(',a)
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.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)
#define TC_LOG_WARN(...) ((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{bool lfg=false;Guid GetLeaderGUID(){return {};}bool isLFGGroup(){return lfg;}};
struct Object{} object;
struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}Object* GetGameObject(int){return &object;}} map;
struct Player {
bool combat=false,battleground=false;uint32 area=87,zone=12,mapId=0;
Group*GetGroup(){return nullptr;}bool InBattleground(){return battleground;}bool isUsingLfg(){return false;}bool InBattlegroundQueue(){return false;}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*){}
template<class T>bool IsInAreaTriggerRadius(T*){return true;}
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 full=false;unsigned queued=0;bool Complete(Player*){return full;}void QueueBrowser(Player*,std::string const&){++queued;}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,ZoneId=12;} location;
struct Locations{auto Find(std::string const&){return &location;}} locations;auto sAdventureLocationMgr=&locations;
struct TriggerStore{struct Trigger{int ContinentID=0;}t;Trigger*LookupEntry(unsigned){return &t;}}sAreaTriggerStore;
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,WaitingGroup};
struct Record{
bool WaitingTravel=false,WaitingArrived=false;bool Enabled=true,Schedule=false,Queued=false,Dirty=false,ExpeditionStarted=false;AdventureGroupPolicy::Departure Departure;
Phase State=Phase::Travel;
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,WaitingLocation,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;unsigned Entry=0;}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;}
bool UpdateDeparture(Player*,Record&,time_t){return false;}
unsigned capitalChecks=0;bool UpdateWaitingCapital(Player*,Record&,time_t){++capitalChecks;return false;}
void UpdateActivities(Player*p,Record&r){Micro(p,r);}
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);
// Waiting for a valid party cannot start travel or ordinary micro actions.
r.State=AdventureBehaviorMgr::Phase::WaitingGroup;r.Intent="instance";
mgr.Update(&player,1000);assert(botGroups.queued==1&&mgr.begun==0&&mgr.micros==0);
botGroups.full=true;mgr.Update(&player,1000);
assert(r.State==AdventureBehaviorMgr::Phase::Idle&&r.Dirty);
// A latched departure survives a vacancy while another member catches up.
botGroups.full=false;r.State=AdventureBehaviorMgr::Phase::WaitingGroup;r.Dirty=false;r.ExpeditionStarted=true;
mgr.Update(&player,1000);assert(r.State==AdventureBehaviorMgr::Phase::Idle&&r.Dirty&&botGroups.queued==1);
mgr.switches=0;
// A graph continent transition is not mistaken for native dungeon admission.
r.State=AdventureBehaviorMgr::Phase::WaitingGroup;r.ExpeditionStarted=false;
r.WaitingLocation="capital";r.WaitingTravel=true;player.mapId=530;
auto checked=mgr.capitalChecks;mgr.Update(&player,1000);
assert(mgr.capitalChecks==checked+1&&r.WaitingLocation=="capital"&&r.WaitingTravel);
// Native dungeon admission suspends capital movement/activities immediately.
map.dungeon=true;checked=mgr.capitalChecks;mgr.Update(&player,1000);
assert(mgr.capitalChecks==checked&&r.WaitingLocation.empty()&&!r.WaitingTravel);
map.dungeon=false;player.mapId=0;
// An active battleground suspends the day planner, including hour transitions.
player.battleground=true;r.Schedule=true;r.Deadline=1;r.Plan[6]={"profession","mine"};hour=6;
mgr.Update(&player,1000);assert(mgr.switches==0&&r.Hour==5);
player.battleground=false;hour=5;r=AdventureBehaviorMgr::Record{};
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++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-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.')