230 lines
17 KiB
Python
230 lines
17 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 <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <unordered_set>
|
|
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;uint32 zone=1519;
|
|
void GetFullTerrainStatusForPosition(int,uint32,float,float,float,PositionFullTerrainStatus& s){s.outdoors=outdoors;}
|
|
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 ↦}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;
|
|
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+20*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 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;std::mt19937 Random{26};
|
|
time_t Deadline=0;bool Dirty=false;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"}));
|
|
|
|
}
|
|
'''
|
|
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.')
|