Publish replacement landing targets, bound orphaned trail waits, and plan safe connections from the follower position. Support shared-route regroup landings and preserve catch-up flights after the leader lands. Add geometry, lifecycle and group travel regressions.
172 lines
18 KiB
Python
172 lines
18 KiB
Python
"""Exercise production group formation, safety, transition queue and separation."""
|
|
from pathlib import Path
|
|
import subprocess,tempfile
|
|
root=Path(__file__).resolve().parents[2]
|
|
s=(root/'src/server/game/World/AdventureGroupTravelMgr.cpp').read_text()
|
|
def method(sig):
|
|
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
|
|
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
|
|
return s[a:e]
|
|
h=(root/'src/server/game/World/AdventureGroupTravelMgr.h').read_text()
|
|
h='\n'.join(v for v in h.splitlines() if not v.startswith('#include')).replace('private:','public:')
|
|
code=r'''
|
|
#include "AdventureGroupTravelPolicy.h"
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <ctime>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <set>
|
|
using uint32=unsigned;
|
|
#define TC_GAME_API
|
|
#define TC_LOG_WARN(...) ((void)0)
|
|
constexpr float INVALID_HEIGHT=-100000;
|
|
constexpr int LINEOFSIGHT_ALL_CHECKS=0,CMSG_AREATRIGGER=0;
|
|
namespace VMAP{enum class ModelIgnoreFlags{None};}
|
|
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}bool operator<(ObjectGuid b)const{return id<b.id;}};
|
|
namespace std{template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)const{return g.id;}};}
|
|
struct Position{
|
|
float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;Position()=default;Position(float x,float y,float z=0,float a=0):m_positionX(x),m_positionY(y),m_positionZ(z),o(a){}
|
|
void Relocate(Position const*p){*this=*p;}void Relocate(float x,float y,float z,float a=0){*this={x,y,z,a};}
|
|
float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}
|
|
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);}
|
|
float GetExactDist(Position const*p)const{return GetExactDist(*p);}float GetExactDist(float x,float y,float z)const{return GetExactDist(Position{x,y,z});}
|
|
float GetExactDist2d(Position const&p)const{return std::hypot(m_positionX-p.m_positionX,m_positionY-p.m_positionY);}
|
|
};
|
|
struct Map{bool cliff=false,los=true,narrow=false;bool IsDungeon(){return false;}
|
|
float GetHeight(unsigned phase,float x,float y,float,bool,float){assert(phase==169);if(cliff && x>4 && x<15)return -1000;if(narrow && std::fabs(y)>2)return -1000;return 0;}
|
|
bool isInLineOfSight(unsigned,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return los;}
|
|
}map;
|
|
struct Session{bool clientless=false;bool IsClientless(){return clientless;}void HandleAreaTriggerOpcode(struct WorldPacket&){}};
|
|
struct Group{ObjectGuid leader{21};struct Slot{ObjectGuid guid;};std::vector<Slot>slots{{{21}},{{26}},{{27}}};ObjectGuid GetLeaderGUID(){return leader;}auto const&GetMemberSlots(){return slots;}}group;
|
|
struct Player:Position{
|
|
ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0;Group*g=&group;Session session;
|
|
Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}unsigned GetPhaseShift(){return 169;}
|
|
Session*GetSession(){return &session;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool IsCharmed(){return charmed;}
|
|
bool CanFly(){return mounted && flightCapable;}bool flightCapable=false;bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;}
|
|
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return visible && p->visible;}bool IsInAreaTriggerRadius(void*){return false;}
|
|
};
|
|
struct WorldPacket{WorldPacket(int,int){}template<class T>WorldPacket&operator<<(T){return *this;}};
|
|
struct GameObject{bool IsInPhase(Player*){return true;}void Use(Player*){}};
|
|
namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*>players;Player*FindPlayer(ObjectGuid g){auto it=players.find(g);return it==players.end()?nullptr:it->second;}GameObject*GetGameObject(Player&,ObjectGuid){return nullptr;}}
|
|
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
|
struct Areas{void*LookupEntry(unsigned){return nullptr;}}sAreaTriggerStore;
|
|
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed,Failed};
|
|
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0,adapters=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)> adapter){adapters+=adapter?1:-1;}}executor;auto sClientlessActionExecutorMgr=&executor;
|
|
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
|
|
struct Core{unsigned queued=0;std::vector<std::string>nodes;unsigned GetQueuedActionCount(unsigned){return queued;}bool QueueAction(unsigned,ClientlessAction action){++queued;nodes.push_back(action.Parameters["node"]);return true;}void ClearQueuedActions(unsigned){queued=0;}}core;auto sClientlessPlayerMgr=&core;
|
|
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
|
|
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=20;};
|
|
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0,validators=0;std::vector<Position>targets;
|
|
auto GetStatus(unsigned){return &status;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&d){d.Complete=valid;return valid;}
|
|
bool StartTravel(Player*,Position const&target,bool=true,bool=false){++starts;targets.push_back(target);status.Target=target;status.State=ClientlessTravelState::Moving;return true;}
|
|
std::vector<Position>const*GetActivePath(unsigned){return nullptr;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}void SetPathValidator(unsigned,std::function<bool(Player*,std::vector<Position>const&)> validator){validators+=validator?1:-1;}
|
|
}travel;auto sClientlessTravelMgr=&travel;
|
|
struct PathGenerator{struct Point{float x,y,z;};Player*p;std::vector<Point>points;PathGenerator(Player*who):p(who){}bool CalculatePath(float x,float y,float z){points={{p->GetPositionX(),p->GetPositionY(),p->GetPositionZ()},{x,y,z}};return travel.valid;}auto const&GetPath(){return points;}};
|
|
struct Graph{bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&n,float&distance){n="nearest";distance=5;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
|
enum class ClientlessTransportPhase{Waiting,Riding,Arrived,Failed};
|
|
struct TransportStatus{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;};struct Stop{unsigned Map=0;struct Point{Position Pos;};std::vector<Point>Points{{{0,0,0}}};};
|
|
struct Transport{TransportStatus status;auto GetStatus(unsigned){return &status;}Stop*GetStop(std::string const&){return nullptr;}bool Start(Player*,std::string const&,std::string const&,std::string&){return false;}}transport;auto sClientlessTransportMgr=&transport;
|
|
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
|
|
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*){return false;}
|
|
bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight;
|
|
struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua;
|
|
struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
|
|
struct ChatHandler{void SendSysMessage(char const*){}template<class...T>void PSendSysMessage(char const*,T...){}};
|
|
'''+h+r'''
|
|
AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGroupTravelMgr m;return &m;}
|
|
AdventureGroupTravelMgr::Leader observed;
|
|
void AdventureGroupTravelMgr::SampleLeader(Player*,Leader&state,time_t){state=observed;}
|
|
'''+method(' bool Executing(')+method(' Position OffsetPoint(')
|
|
for sig in ['void AdventureGroupTravelMgr::Observe(', 'bool AdventureGroupTravelMgr::Airborne(', 'bool AdventureGroupTravelMgr::Protected(', 'bool AdventureGroupTravelMgr::Separated(', 'bool AdventureGroupTravelMgr::GroundPoint(', 'bool AdventureGroupTravelMgr::SafePath(', 'unsigned AdventureGroupTravelMgr::Columns(', 'Position AdventureGroupTravelMgr::Formation(', 'bool AdventureGroupTravelMgr::Route(', 'bool AdventureGroupTravelMgr::JourneyFollow(', 'bool AdventureGroupTravelMgr::Follow(', 'bool AdventureGroupTravelMgr::AdaptRoad(', 'bool AdventureGroupTravelMgr::ValidateGroundPath(', 'void AdventureGroupTravelMgr::Clear(']:code+=method(sig)+'\n'
|
|
code+=r'''
|
|
int main(){
|
|
using namespace AdventureGroupTravelPolicy;
|
|
for(unsigned columns:{1,2,3})for(bool moving:{false,true})for(bool mounted:{false,true})for(bool flying:{false,true}){
|
|
std::set<std::tuple<float,float,float>>points;for(unsigned slot=0;slot<20;++slot){auto p=Formation(slot,columns,moving,mounted,flying);assert(points.emplace(p.Behind,p.Side,p.Height).second);}
|
|
}
|
|
unsigned elapsed=0;for(unsigned i=0;i<1199;++i)elapsed=Separation(elapsed,500,true,false);assert(elapsed==599500);
|
|
assert(Separation(elapsed,500,true,true)==elapsed);assert(Separation(elapsed,500,true,false)==600000);assert(Separation(600000,500,false,false)==0);
|
|
Player leader(21),a(26),b(27);a.session.clientless=b.session.clientless=true;leader.m_positionX=20;
|
|
ObjectAccessor::players={{{21},&leader},{{26},&a},{{27},&b}};
|
|
AdventureGroupTravelMgr mgr;mgr.Observe(&a,500);mgr.Observe(&b,500);assert(mgr.Follow(&a));auto first=travel.targets.back();travel.status.State=ClientlessTravelState::Arrived;assert(mgr.Follow(&b));auto second=travel.targets.back();assert(first.GetExactDist(second)>2);
|
|
// A valid-looking MMAP endpoint beyond a hole must never authorize a shortcut.
|
|
travel.status.State=ClientlessTravelState::Arrived;map.cliff=true;unsigned starts=travel.starts;assert(mgr.Follow(&a));assert(travel.starts==starts);map.cliff=false;
|
|
// Formation compresses on narrow ground; this is checked against phased terrain.
|
|
mgr._widths.clear();map.narrow=true;assert(mgr.Columns(&a,&leader)==1);map.narrow=false;mgr._widths.clear();assert(mgr.Columns(&a,&leader)==3);
|
|
travel.status.State=ClientlessTravelState::Arrived;leader.mounted=true;mgr.Follow(&a);assert(a.mounted);leader.mounted=false;a=Player(26); // retain a clean origin
|
|
// A ground-only bot waits safely when the leader gains excessive height.
|
|
leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0;
|
|
// A shared/action-owned catch-up corridor survives a leader landing ahead.
|
|
flight.status.State=ClientlessFlightState::Flying;flight.status.FollowingPlayer=false;
|
|
mgr.Observe(&a,1);leader.flying=false;auto previousLands=flight.lands;
|
|
assert(mgr.Follow(&a)&&flight.lands==previousLands);
|
|
flight.status.State=ClientlessFlightState::Arrived;mgr.Follow(&a);
|
|
// A native-capable follower uses flight, and safely lands after a kick.
|
|
flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a);
|
|
// A stale owned ground catch-up route cannot suppress a visible-phase
|
|
// human leader taking off, even beyond normal visual detection distance.
|
|
leader.mounted=leader.flightCapable=leader.flying=true;leader.visible=false;
|
|
flight.enabled=true;flight.status.State=ClientlessFlightState::Failed;
|
|
mgr._records.at(26).RouteOwned=true;executor.status.State=ClientlessActionExecutionState::Moving;
|
|
auto cleared=executor.clears;auto flown=flight.follows;
|
|
mgr.Follow(&a);assert(executor.clears==cleared+1&&flight.follows==flown+1);
|
|
// Matching a grounded flying mount must never be immediately undone by
|
|
// the ground-follow dismount branch.
|
|
leader.flying=false;leader.visible=true;a.mounted=a.flightCapable=true;
|
|
flight.status.State=ClientlessFlightState::Arrived;leader.m_positionX=8;
|
|
mgr.Follow(&a);assert(a.mounted);
|
|
leader.mounted=leader.flightCapable=false;a.mounted=a.flightCapable=false;
|
|
flight.enabled=false;leader.m_positionX=20;
|
|
// Taxi journey and its arrival grace pause distance counting. Completion must
|
|
// not protect unrelated later ground separation.
|
|
auto& state=observed;state.Kind=AdventureGroupTravelMgr::Journey::Taxi;state.Generation=1;state.Began=GameTime::now;state.To="taxi_42";state.GoalMap=1;state.Goal={1000,0,0};leader.taxi=true;leader.m_positionX=900;
|
|
mgr.Observe(&a,500);assert(mgr.Protected(&a));mgr.Follow(&a);assert(core.nodes.back()=="taxi_42");unsigned queued=core.queued;mgr.Follow(&a);assert(core.queued==queued);
|
|
leader.taxi=false;leader.mapid=1;leader.m_positionX=1000;state.Arrived=++GameTime::now;mgr.Observe(&a,500);assert(mgr.Protected(&a));
|
|
a.mapid=1;a.m_positionX=1000;mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.empty() && !mgr.Protected(&a));
|
|
// Sequential portals retain the first transition until the follower arrives.
|
|
state.Kind=AdventureGroupTravelMgr::Journey::Portal;state.Generation=2;state.SourceMap=1;state.GoalMap=2;state.Goal={100,0,0};state.Began=++GameTime::now;mgr.Observe(&a,500);
|
|
state.Generation=3;state.SourceMap=2;state.GoalMap=3;state.Goal={200,0,0};mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==2 && mgr._records.at(26).Pending.front().GoalMap==2);
|
|
a.mapid=2;a.m_positionX=100;mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==1 && mgr._records.at(26).Pending.front().GoalMap==3);
|
|
// A fixed portal destination forbidden by the area catalog cannot be used,
|
|
// even when the follower is traveling with a player group leader.
|
|
access.allowed=false;
|
|
assert(!mgr.JourneyFollow(&a,mgr._records.at(26),mgr._records.at(26).Pending.front()));
|
|
access.allowed=true;
|
|
// A blocked/unrecognized trip cannot grant unlimited separation protection.
|
|
mgr._records.at(26).Pending.clear();state.Kind=AdventureGroupTravelMgr::Journey::None;state.Generation=4;leader.mapid=0;leader.m_positionX=1000;a.mapid=0;a.m_positionX=0;executor.status.State=ClientlessActionExecutionState::Completed;
|
|
for(unsigned i=0;i<1200;++i){if(i%2==0)++GameTime::now;mgr.Observe(&a,500);}assert(mgr.Separated(&a));leader.m_positionX=10;mgr.Observe(&a,500);assert(!mgr.Separated(&a));
|
|
// Nearby targets belonging to adjacent rows are distinct: a 2yd change
|
|
// cannot reuse an old spline that ends at a different formation position.
|
|
leader.m_positionX=20;leader.m_positionZ=0;leader.moving=false;
|
|
travel.status.State=ClientlessTravelState::Arrived;mgr.Follow(&a);
|
|
auto oldTarget=travel.status.Target;leader.m_positionX+=2;auto startsBefore=travel.starts;
|
|
mgr.Follow(&a);assert(travel.starts==startsBefore+1&&travel.status.Target.GetExactDist(oldTarget)>1.9f);
|
|
// A short stop during an executing route still preserves row distance.
|
|
leader.m_positionX=6;leader.moving=false;executor.status.State=ClientlessActionExecutionState::Moving;
|
|
std::vector<Position>shortRoad{{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}};
|
|
assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()<6);
|
|
// Boarding is released once the leader actually starts the native taxi.
|
|
leader.taxi=true;shortRoad={{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}};
|
|
assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()==8);
|
|
leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed;
|
|
// Recorded roads are bounded and validated, never a spline through a cliff.
|
|
std::vector<Position>road;for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);leader.moving=false;map.cliff=true;assert(!mgr.AdaptRoad(&a,road));map.cliff=false;road.clear();for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);assert(mgr.AdaptRoad(&a,road) && road.back().GetPositionX()<=60);
|
|
// Leaving a group clears formation-owned adapters and validators before the
|
|
// next autonomous route. This prevents valid MMAP paths being rejected by a
|
|
// stale group formation safety check.
|
|
assert(mgr._records.count(26)&&executor.adapters==2&&travel.validators==2);
|
|
auto cancelsBefore=travel.cancels;a.g=nullptr;mgr.Observe(&a,500);
|
|
assert(!mgr._records.count(26)&&executor.adapters==1&&travel.validators==1&&travel.cancels==cancelsBefore+1);
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p=Path(d);(p/'test.cpp').write_text(code)
|
|
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
|
subprocess.run([str(p/'test')],check=True)
|
|
print('Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed')
|