Implement native bot recruitment and player admission, leader-owned destinations, intent-specific capacities, preparation scheduling and shared graph and flight travel. Support native group instance visits and separate raid entrances. Include pending personality-driven faction PvP and solo fleeing, preserving the absolute group fleeing exclusion. Add regression fixtures, migration, documentation and test-server evidence.
122 lines
7.6 KiB
Python
122 lines
7.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 ↦}
|
|
} player;
|
|
namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}}
|
|
struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups;
|
|
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 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;}
|
|
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;}
|
|
};
|
|
'''
|
|
code+=s[a:b]+r'''
|
|
int main(){
|
|
AdventureBehaviorMgr mgr;auto& r=mgr._records.at(26);
|
|
// 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.')
|