Files
Viano-Core/tests/clientless/test_adventure_leisure.py
T

248 lines
19 KiB
Python

"""Execute leisure scene progression, native flight handover and pet-walk gating."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);depth=1;e=b+1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
code=r'''
#include <algorithm>
#include "AdventureInstancePolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cmath>
#include <cstdint>
#include <ctime>
#include <random>
#include <string>
#include <vector>
#include <unordered_set>
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
using uint32=uint32_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;
constexpr int BASE_ATTACK=0,ITEM_SUBCLASS_WEAPON_FISHING_POLE=20,UNIT_NPC_EMOTESTATE=0,EMOTE_STATE_FISHING=379;
struct Player;
struct Position{
float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;
void Relocate(float x,float y,float z){m_positionX=x;m_positionY=y;m_positionZ=z;}
void Relocate(Player*);float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}
float GetExactDist(Position const&p)const{return std::hypot(std::hypot(m_positionX-p.m_positionX,m_positionY-p.m_positionY),m_positionZ-p.m_positionZ);}
};
struct PositionFullTerrainStatus{bool outdoors=true;};
struct Terrain{
bool outdoors=true;float outdoorAfter=1e9;uint32 zone=1519;
void GetFullTerrainStatusForPosition(int,uint32,float x,float y,float,PositionFullTerrainStatus& s){s.outdoors=outdoors || std::hypot(x,y)>outdoorAfter;}
void GetZoneAndAreaId(int,uint32,uint32& z,uint32& a,Position const&){z=zone;a=1;}
}terrain;
struct Map{float water=-1000,ground=0;Terrain* GetTerrain(){return &terrain;}float GetHeight(int,float,float,float,bool,float){return ground;}float GetWaterLevel(int,float,float){return water;}}map;
struct Guid{uint32 GetCounter(){return 26;}};
struct Item{struct Template{uint32 GetSubClass(){return 20;}}t;Template* GetTemplate(){return &t;}}rod;
struct Creature:Position{}cat;
struct Player:Position{
bool alive=true,combat=false,flying=false,outdoors=true,water=false,falling=false,hasRod=false;uint32 emote=0;int stops=0;
uint32 GetMapId(){return 0;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsFlying(){return flying;}bool IsInFlight(){return false;}bool GetTransport(){return false;}
bool IsOutdoors(){return outdoors;}bool IsInWater(){return water;}bool IsFalling(){return falling;}Guid GetGUID(){return {};}
int GetPhaseShift(){return 0;}Map* GetMap(){return &map;}Item* GetWeaponForAttack(int,bool){return hasRod?&rod:nullptr;}
void SetFacingTo(float f){o=f;}float GetOrientation(){return o;}void SetUInt32Value(int,uint32 v){emote=v;}uint32 GetUInt32Value(int){return emote;}
bool IsWithinLOS(float,float,float){return true;}
void StopMoving(){++stops;}float GetExactDist(Position const& p){return Position::GetExactDist(p);}float GetExactDist(Creature* p){return Position::GetExactDist(*p);}
};
void Position::Relocate(Player*p){*this=*p;}
bool Unsafe(Player* p){return p->combat||p->flying;}
constexpr int PATHFIND_NORMAL=1;
struct PathGenerator{
struct Point{float x,y,z;};std::vector<Point> path;
static inline bool reachable=true,broken=false;static inline int type=1;static inline float bend=20;
PathGenerator(Player*){}void SetPathLengthLimit(float){}
bool CalculatePath(Position const&a,Position const&b){
path.clear();int n=int(a.GetExactDist(b)/4)+1;
for(int i=0;i<=n;++i){float t=float(i)/n;path.push_back({a.m_positionX+(b.m_positionX-a.m_positionX)*t,a.m_positionY+(b.m_positionY-a.m_positionY)*t+bend*std::sin(3.14159265f*t),a.m_positionZ+(b.m_positionZ-a.m_positionZ)*t});}
if(broken && path.size()>5) path[5].z+=10;
return reachable;
}
int GetPathType(){return type;}auto const& GetPath(){return path;}
};
struct ClientlessCompanionScene{uint32 LeashSpell=0;bool Active=false,Ready=false,Leashed=false;};
struct ClientlessCompanionMgr{
static inline bool available=true,ready=true,valid=true;static inline int starts=0,resets=0;
static bool Available(Player*,uint32){return available;}
static bool Start(Player*,ClientlessCompanionScene& s,uint32,uint32){s.Active=true;s.Ready=false;++starts;return available;}
static bool Update(Player*,ClientlessCompanionScene& s,uint32){s.Ready=ready;return valid;}
static Creature* Resolve(Player*,ClientlessCompanionScene const&){return valid?&cat:nullptr;}
static void Reset(Player*,ClientlessCompanionScene& s){if(s.Active)++resets;s={};}
};
struct Profile{bool CanGroundMount=true;float GroundMountMinDistance=45;std::vector<int> GroundMounts{1},Companions;uint32 CompanionSpell=10673,CompanionLeashItem=37460;}profile;
struct Lua{uint32 SelectCompanionSpell(uint32){return profile.CompanionSpell;}int mounts=0,dismounts=0;Profile* GetAdventureProfile(uint32){return &profile;}bool PrepareGroundMount(Player*,float,char const*){++mounts;return true;}void DismountGroundMount(Player*,char const*){++dismounts;}}lua;auto sClientlessLuaEngine=&lua;
struct CombatMgr{bool available=true;int starts=0;bool TrainingAvailable(Player*){return available;}bool Train(Player*,uint32 duration,time_t){assert(duration>0);++starts;return available;}}combatMgr;auto sAdventureCombatMgr=&combatMgr;
struct AdventureMicroAction{std::string Requirement;};
struct Location{struct{std::string Category="leisure";float O=0;}Point;bool IsArea=false;uint32 AreaId=0,ZoneId=1519;}location;
struct Locations{Location* Find(std::string const&){return &location;}}locations;auto sAdventureLocationMgr=&locations;
enum class ClientlessTravelState{Moving,Arrived,PathRejected};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float FinalDistance=0,PathLength=1;};
struct Travel{bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return true;}struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;int cancels=0;void CancelTravel(uint32){++cancels;}auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Flight{struct{ClientlessFlightState State=ClientlessFlightState::Mounting;}status;int starts=0;bool enabled=true;Position target;bool Enabled(Player*){return enabled;}bool Start(Player*,Position const&t,std::string&){++starts;target=t;status.State=ClientlessFlightState::Mounting;return true;}auto GetStatus(uint32){return &status;}}flight;auto sClientlessFlightMgr=&flight;
struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerMgr=&manager;
struct Groups{unsigned cancellations=0;void CancelBrowser(Player*){++cancellations;}}groups;auto sAdventureGroupMgr=&groups;
struct Executor{bool RequestStopAtNode(uint32){return false;}}executor;auto sClientlessActionExecutorMgr=&executor;
class AdventureBehaviorMgr{public:
enum class Phase{Idle,OnSite,Switching};
struct Record{
AdventureInstancePolicy::Visit Instance;
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000,RetryTimer=0;std::mt19937 Random{26};
time_t Deadline=0,ExpeditionUntil=0;bool Dirty=false,ExpeditionStarted=false;std::string ExpeditionIntent;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
};
int moves=0,resets=0;bool lastRiding=false,moveAllowed=true;Position requested;
bool LocalMove(Player*,Record&,Position const& target,float,bool riding=false){++moves;lastRiding=riding;requested=target;travel.status.State=ClientlessTravelState::Moving;return moveAllowed;}
void ResetMicro(Player* p,Record&r){++resets;ResetLeisure(p,r);}
void FinishInstance(Player*,Record&r){r.Instance={};}
bool workAvailable=false;bool ProfessionWorkAvailable(Player*,Record&){return workAvailable;}
bool FishingDirection(Player*,float&)const;
bool LeisureAvailable(Player*,Record const&,AdventureMicroAction const&)const;
bool PlanLeisureWalk(Player*,Record&);
bool StartLeisure(Player*,Record&,AdventureMicroAction const&);
bool UpdateLeisure(Player*,Record&,uint32);void ResetLeisure(Player*,Record&);
void SetWanted(Player*,Record&,std::string const&,std::string const&,time_t);
};
'''
for sig in ('bool AdventureBehaviorMgr::FishingDirection(', 'bool AdventureBehaviorMgr::PlanLeisureWalk(', 'bool AdventureBehaviorMgr::LeisureAvailable(', 'bool AdventureBehaviorMgr::StartLeisure(', 'bool AdventureBehaviorMgr::UpdateLeisure(', 'void AdventureBehaviorMgr::ResetLeisure(', 'void AdventureBehaviorMgr::SetWanted('):code+=function(sig)+'\n'
code+=r'''
int main(){
AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;Player p;
assert(mgr.StartLeisure(&p,r,{"walk"})&&r.Leisure.PlannedWalk);
auto expected=r.Leisure.Route.size();auto outward=r.Leisure.OutboundSteps;
assert(outward>=3&&expected==2*outward);
assert(r.Leisure.Origin.GetExactDist(r.Leisure.Route[outward-1])>=65);
for(size_t i=0;i+1<outward;++i) assert(r.Leisure.Route[i].GetExactDist(r.Leisure.Route[2*outward-2-i])==0);
assert(mgr.UpdateLeisure(&p,r,1000)&&mgr.moves==1&&r.Leisure.Step==0&&!mgr.lastRiding);
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==0); // moving is not arrival
bool active=true;for(int i=0;i<150&&active;++i){
if(r.Leisure.Moving){p.Relocate(mgr.requested.GetPositionX(),mgr.requested.GetPositionY(),mgr.requested.GetPositionZ());travel.status.State=ClientlessTravelState::Arrived;}
active=mgr.UpdateLeisure(&p,r,1000);
}
assert(!active&&r.Leisure.CompletedLegs==expected&&p.GetExactDist(r.Leisure.Origin)<0.01f);
mgr.ResetLeisure(&p,r);
// A rejected forward connector triggers the visited return path, never a skip.
assert(mgr.StartLeisure(&p,r,{"walk"}));mgr.moveAllowed=false;
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Returning&&r.Leisure.Step==0&&r.Leisure.CompletedLegs==0);
assert(!mgr.UpdateLeisure(&p,r,1000)&&r.Detail=="leisure return path unavailable");
mgr.moveAllowed=true;mgr.ResetLeisure(&p,r);
// Timer expiry retraces actually reached checkpoints, including bends.
assert(mgr.StartLeisure(&p,r,{"walk"}));
auto first=r.Leisure.Route[0],second=r.Leisure.Route[1];
assert(mgr.UpdateLeisure(&p,r,50));p.Relocate(first.m_positionX,first.m_positionY,first.m_positionZ);travel.status.State=ClientlessTravelState::Arrived;
assert(mgr.UpdateLeisure(&p,r,50)&&r.Leisure.PauseMs==0);
r.MicroTimer=0;p.Relocate(second.m_positionX,second.m_positionY,second.m_positionZ);travel.status.State=ClientlessTravelState::Arrived;
assert(mgr.UpdateLeisure(&p,r,50)&&r.Leisure.Returning&&r.Leisure.Route.size()==2);
assert(r.Leisure.Route[0].GetExactDist(first)==0&&r.Leisure.Route[1].GetExactDist(r.Leisure.Origin)==0);
mgr.ResetLeisure(&p,r);p.Relocate(0,0,0);r.MicroTimer=240000;
PathGenerator::reachable=false;assert(!mgr.StartLeisure(&p,r,{"walk"}));PathGenerator::reachable=true;
PathGenerator::type=2;assert(!mgr.StartLeisure(&p,r,{"walk"}));PathGenerator::type=1;
PathGenerator::broken=true;assert(!mgr.StartLeisure(&p,r,{"walk"}));PathGenerator::broken=false;
// Indoor seating must not trap the leisure selector. Foot outings can leave,
// and use the outdoor anchor rather than returning to the indoor chair.
p.outdoors=false;p.Relocate(5,0,2);
assert(mgr.LeisureAvailable(&p,r,{"walk"})&&mgr.LeisureAvailable(&p,r,{"roam"}));
assert(mgr.LeisureAvailable(&p,r,{"pet_walk"}));
assert(!mgr.LeisureAvailable(&p,r,{"ride"})&&!mgr.LeisureAvailable(&p,r,{"flight"}));
assert(mgr.StartLeisure(&p,r,{"walk"})&&r.Leisure.Origin.GetExactDist(r.Anchor)==0);
assert(r.Leisure.Route.back().GetExactDist(r.Anchor)==0);
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Moving&&!mgr.lastRiding);
mgr.ResetLeisure(&p,r);p.outdoors=true;p.Relocate(0,0,0);
// No route is invented where native terrain rejects every candidate or zone differs.
terrain.outdoors=false;assert(!mgr.StartLeisure(&p,r,{"walk"}));terrain.outdoors=true;
terrain.zone=12;assert(!mgr.StartLeisure(&p,r,{"walk"}));terrain.zone=1519;
map.ground=8;assert(!mgr.StartLeisure(&p,r,{"walk"}));map.ground=0; // roof is not a ground stroll waypoint
// Summon and leash must be ready before owner movement starts.
assert(mgr.StartLeisure(&p,r,{"pet_walk"}));ClientlessCompanionMgr::ready=false;
int before=mgr.moves;assert(mgr.UpdateLeisure(&p,r,1000)&&mgr.moves==before);
ClientlessCompanionMgr::ready=true;cat.Relocate(p.GetPositionX(),p.GetPositionY(),0);
assert(mgr.UpdateLeisure(&p,r,1000)&&mgr.moves==before+1);
cat.Relocate(100,100,0);assert(mgr.UpdateLeisure(&p,r,1000)&&!r.Leisure.Moving&&p.stops>0);
bool following=true;for(int i=0;i<25&&following;++i)following=mgr.UpdateLeisure(&p,r,1000);
assert(!following);mgr.ResetLeisure(&p,r);assert(ClientlessCompanionMgr::resets==1);
cat.Relocate(0,0,0);p.Relocate(0,0,0);
// Leashed walking stops before native 5 yd range and waits for a real catch-up.
assert(mgr.StartLeisure(&p,r,{"pet_walk"}));r.Leisure.Companion.Leashed=true;
before=mgr.moves;cat.Relocate(4.1f,0,0);
assert(mgr.UpdateLeisure(&p,r,50)&&mgr.moves==before&&r.Leisure.CompanionWait==50);
cat.Relocate(3.7f,0,0);assert(mgr.UpdateLeisure(&p,r,50)&&mgr.moves==before);
cat.Relocate(3,0,0);assert(mgr.UpdateLeisure(&p,r,50)&&mgr.moves==before+1&&r.Leisure.CompanionWait==0);
mgr.ResetLeisure(&p,r);
// Mounted outings use the same navigation with an explicitly selected riding gait.
assert(mgr.StartLeisure(&p,r,{"ride"}));assert(mgr.UpdateLeisure(&p,r,1000)&&mgr.lastRiding&&lua.mounts>0);mgr.ResetLeisure(&p,r);
// A flight switch waits for native landing and a return to its origin.
assert(mgr.StartLeisure(&p,r,{"flight"}));assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Flying);
p.flying=true;int resets=mgr.resets;mgr.SetWanted(&p,r,"rest","inn",1234);
assert(r.Dirty&&r.Leisure.StopRequested&&mgr.resets==resets&&r.WantedIntent=="rest");
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==0); // mounting/flying cannot fabricate arrival
p.flying=false;flight.status.State=ClientlessFlightState::Arrived;
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Moving);
p.Relocate(mgr.requested.GetPositionX(),mgr.requested.GetPositionY(),0);travel.status.State=ClientlessTravelState::Arrived;
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Returning&&r.Leisure.Route.size()==1&&r.Leisure.Flying);
flight.status.State=ClientlessFlightState::Arrived;assert(mgr.UpdateLeisure(&p,r,1000));
p.Relocate(mgr.requested.GetPositionX(),mgr.requested.GetPositionY(),0);travel.status.State=ClientlessTravelState::Arrived;
assert(!mgr.UpdateLeisure(&p,r,1000)&&p.GetExactDist(r.Leisure.Origin)<0.01f);mgr.ResetLeisure(&p,r);
// Angling needs an authored bank, equipped fishing rod and real water.
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));location.Point.Category="leisure_fishing";p.hasRod=true;
assert(!mgr.StartLeisure(&p,r,{"fish"}));map.water=2;
assert(mgr.StartLeisure(&p,r,{"fish"})&&p.emote==EMOTE_STATE_FISHING);
r.MicroTimer=0;assert(!mgr.UpdateLeisure(&p,r,1000));mgr.ResetLeisure(&p,r);
// Adventure foot exploration reuses coherent native routes, never pet/flight scenes.
r.Intent="adventure";r.MicroTimer=180000;p.outdoors=true;p.Relocate(0,0,0);
assert(mgr.LeisureAvailable(&p,r,{"explore"})&&mgr.LeisureAvailable(&p,r,{"patrol"}));
assert(!mgr.LeisureAvailable(&p,r,{"pet_walk"})&&!mgr.LeisureAvailable(&p,r,{"flight"}));
assert(mgr.StartLeisure(&p,r,{"explore"})&&r.Leisure.PlannedWalk&&r.Leisure.Route.size()>=6);
mgr.ResetLeisure(&p,r);p.outdoors=false;assert(!mgr.LeisureAvailable(&p,r,{"explore"}));
// Profession area search progresses outward and stops only at confirmed arrival.
p.outdoors=true;p.Relocate(0,0,0);r.Intent="profession";r.MicroTimer=180000;
location.IsArea=true;location.AreaId=1519;
assert(mgr.StartLeisure(&p,r,{"work_search"})&&r.Leisure.Progressive);
assert(r.Leisure.Route.size()==r.Leisure.OutboundSteps);
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Moving);
mgr.workAvailable=true;
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==0);
p.Relocate(mgr.requested.GetPositionX(),mgr.requested.GetPositionY(),mgr.requested.GetPositionZ());
travel.status.State=ClientlessTravelState::Arrived;
assert(!mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==1&&r.Leisure.Route.size()==1);
mgr.ResetLeisure(&p,r);mgr.workAvailable=false;
// Profession fishing needs real shore water and a rod, without a fishing location key.
location.Point.Category="zone";map.water=-1000;p.hasRod=true;
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));map.water=2;
assert(mgr.LeisureAvailable(&p,r,{"fish"}));p.hasRod=false;
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.hasRod=true;p.water=true;
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.water=false;
r.Intent="leisure";combatMgr.available=false;assert(!mgr.LeisureAvailable(&p,r,{"training"}));
combatMgr.available=true;assert(mgr.LeisureAvailable(&p,r,{"training"}));
assert(mgr.StartLeisure(&p,r,{"training"}) && combatMgr.starts==1);
r.Intent="adventure";assert(!mgr.LeisureAvailable(&p,r,{"training"}));
// Recruitment walks leave the inn, stay in the capital and retrace the route.
mgr.ResetLeisure(&p,r);r.Intent="groupformation";r.MicroTimer=180000;terrain.outdoors=false;terrain.outdoorAfter=40;p.outdoors=false;PathGenerator::bend=2;
assert(mgr.StartLeisure(&p,r,{"walk"}));
assert(r.Leisure.Origin.GetExactDist(r.Leisure.Route[r.Leisure.OutboundSteps-1])>=65);
for(auto const& point:r.Leisure.Route)assert(point.GetExactDist(r.Leisure.Origin)<=250);
mgr.ResetLeisure(&p,r);terrain.outdoorAfter=1e9;assert(!mgr.StartLeisure(&p,r,{"walk"}));
terrain.outdoorAfter=40;assert(mgr.StartLeisure(&p,r,{"walk"}));
r.Deadline=GameTime::now+300;assert(!mgr.LeisureAvailable(&p,r,{"walk"}));r.Deadline=GameTime::now+301;assert(mgr.LeisureAvailable(&p,r,{"walk"}));r.Deadline=0;
assert(!mgr.LeisureAvailable(&p,r,{"ride"}));mgr.ResetLeisure(&p,r);terrain.outdoors=true;PathGenerator::bend=20;
r.RetryTimer=30000;r.Tried.insert("old");mgr.SetWanted(&p,r,"rest","new",1234);assert(r.RetryTimer==0&&r.Tried.empty()&&r.Deadline==1234);
// Group travel latches survive the formation-to-activity transition but expire.
r.WantedIntent="groupformation";r.ExpeditionIntent="instance";r.ExpeditionUntil=GameTime::now+600;
mgr.SetWanted(&p,r,"instance","entrance",GameTime::now+600);assert(r.ExpeditionStarted);
r.WantedIntent="groupformation";r.ExpeditionUntil=GameTime::now;
mgr.SetWanted(&p,r,"instance","entrance",GameTime::now+600);assert(!r.ExpeditionStarted);
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') 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('Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.')