#!/usr/bin/env python3 """Exercise production dock-state/recorder/executor methods with deterministic world doubles. Run: python3 tests/clientless/test_transport.py Requires g++ and Python; does not connect to a server or database. Real collision, network visibility and map-transfer integration still require server tests. """ from pathlib import Path import subprocess import tempfile ROOT = Path(__file__).resolve().parents[2] def function(text, marker): start = text.index(marker) opening = text.index('{', start) depth = 1 end = opening + 1 while depth: depth += (text[end] == '{') - (text[end] == '}') end += 1 return text[start:end] source = (ROOT / 'src/server/game/World/ClientlessTransportMgr.cpp').read_text() header = (ROOT / 'src/server/game/World/ClientlessTransportMgr.h').read_text() header = '\n'.join(line for line in header.splitlines() if not line.startswith('#')) header = header.replace('private:', 'public:').replace('TC_GAME_API ', '') core = (ROOT / 'src/server/game/Entities/GameObject/GameObject.cpp').read_text() base = (ROOT / 'src/server/game/Entities/Vehicle/VehicleDefines.h').read_text() position = function(base, ' static void CalculatePassengerPosition(') offset = function(base, ' static void CalculatePassengerOffset(') preamble = r''' #include #include #include #include #include #include #include #include #include #include using uint8=uint8_t; using uint32=uint32_t; using uint64=uint64_t; using int32=int32_t; #define TC_LOG_INFO(...) do {} while(0) #define TC_LOG_WARN(...) do {} while(0) #define TC_LOG_ERROR(...) do {} while(0) struct ObjectGuid {uint32 id=0;uint32 GetCounter() const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}}; struct Position { float x=0,y=0,z=0,o=0; static float NormalizeOrientation(float a){a=std::fmod(a,float(2*M_PI));return a<0?a+2*M_PI:a;} void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;} void Relocate(Position const& p){*this=p;} Position const& GetPosition()const{return *this;} void GetPosition(float&a,float&b,float&c,float&d)const{a=x;b=y;c=z;d=o;} float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;} float GetExactDist(Position const*p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));} }; constexpr int MOVE_RUN=0,SPELL_AURA_MOUNTED=1; struct TransportDockState {uint32 Progress=0,Period=0,RemainingPause=0;bool Stopped=false;}; class Player; class GameObject; class TransportBase { public: GameObject* go=nullptr; void AddPassenger(Player*);void RemovePassenger(Player*); void CalculatePassengerPosition(float&,float&,float&,float*)const; void CalculatePassengerOffset(float&,float&,float&,float*)const; ''' world = r''' }; class GameObject:public Position { public: TransportBase base;uint32 map=0,entry=1,spawn=1;TransportDockState state{0,30000,10000,true}; GameObject(){base.go=this;} bool IsInWorld()const{return true;}uint32 GetMapId()const{return map;} ObjectGuid GetGUID()const{return {spawn};} TransportBase* ToTransportBase(){return &base;} bool GetTransportDockState(TransportDockState&s)const{s=state;return true;} uint32 deadline=0; uint32 GetUInt32Value(int)const{return deadline;} }; struct Session {int completed=0;void CompleteClientlessTeleport() {++completed;}} session; struct Motion {Player*p; void MoveSmoothPath(int,Position* path,int,bool);}; class Player:public Position { public: TransportBase* transport=nullptr;uint32 map=0;GameObject* object=nullptr;Motion motion{this};bool pending=false; struct {struct {Position pos;ObjectGuid guid;uint32 time=0;void Reset(){pos={};guid={};time=0;}}transport;}m_movementInfo; int teleports=0;bool falling=false; bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){} bool TeleportTo(uint32 m,float a,float b,float c,float d){++teleports;map=m;Relocate(a,b,c,d);falling=false;return true;} bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;} bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;} bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;} ObjectGuid GetGUID()const{return {26};}uint32 GetMapId()const{return map;} float GetSpeed(int)const{return 7;}void StopMoving(){}bool IsSplineEnabled()const{return false;} Motion* GetMotionMaster(){return &motion;}Session* GetSession(){return &session;} }; void TransportBase::CalculatePassengerPosition(float&x,float&y,float&z,float*o)const{CalculatePassengerPosition(x,y,z,o,go->x,go->y,go->z,go->o);} void TransportBase::CalculatePassengerOffset(float&x,float&y,float&z,float*o)const{CalculatePassengerOffset(x,y,z,o,go->x,go->y,go->z,go->o);} void TransportBase::AddPassenger(Player*p){p->transport=this;p->m_movementInfo.transport.guid=go->GetGUID();} void TransportBase::RemovePassenger(Player*p){p->transport=nullptr;p->m_movementInfo.transport.Reset();} void Motion::MoveSmoothPath(int,Position* path,int,bool){p->Relocate(path[1]);if(p->transport){Position pos=*p;p->transport->CalculatePassengerOffset(pos.x,pos.y,pos.z,&pos.o);p->m_movementInfo.transport.pos=pos;}} enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected}; struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=50;}; struct Travel{bool path=false,present=false;int starts=0;struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status; auto GetStatus(uint32)->Status*{return present?&status:nullptr;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return path;} bool StartTravel(Player*p,Position const&point,bool){++starts;present=true;p->Relocate(point);return true;}void CancelTravel(uint32){present=false;}}travel; auto sClientlessTravelMgr=&travel; // Model the real Field API's strict debug type checks for SMALLINT versus INT. struct Field { bool small=false;uint32 number=0;float real=0; uint32 GetUInt32()const{return small?0:number;} uint16_t GetUInt16()const{return small?uint16_t(number):0;} float GetFloat()const{return real;} }; struct Result {Field fields[5]={{true,369,0},{false,0,1},{false,0,2},{false,0,3},{false,0,0}};Field* Fetch(){return fields;}}; using QueryResult=Result*; struct Database {Result result;templateQueryResult PQuery(char const*,Args...){return &result;}} WorldDatabase; bool Safe(std::string const& key){return !key.empty();} namespace Trinity {bool IsValidMapCoord(float,float,float,float=0){return true;}} ''' helpers = '\n'.join(function(source, marker) for marker in [ ' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength(' ]) methods = '\n'.join(function(source, marker) for marker in [ 'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(', 'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(', 'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(', 'void ClientlessTransportMgr::RecoverPassenger(', 'void ClientlessTransportMgr::UpdateRide(' ]) stubs = r''' void PublishPassengerState(Player*){} bool ClientlessTransportMgr::EnsureLoaded(){return true;} GameObject* ClientlessTransportMgr::FindObject(Player*p,ClientlessTransportStop const&s)const{return p->object && p->object->entry==s.Entry && p->object->spawn==s.Spawn ? p->object : nullptr;} ''' animated = r''' namespace GameTime {uint32 now=0;uint32 GetGameTimeMS(){return now;}} constexpr int GAMEOBJECT_LEVEL=0; struct TransportAnimationEntry {uint32 TimeIndex=0;struct{float X=0,Y=0,Z=0;}Pos;}; struct TransportRotationEntry {float X=0,Y=0,Z=0,W=1;}; struct TransportAnimation {uint32 TotalTime=30033;std::mapPath;std::mapRotations;}; struct Animated { GameObject _owner;TransportAnimation* _animationInfo=nullptr;uint32 _pathProgress=0;std::vector_stopFrames; '''+function(core, ' bool GetTransportDockState(TransportDockState& state) const override').replace(' override','')+'\n};\n' tests = r''' ClientlessTransportPoint point(bool on,float x){ClientlessTransportPoint p;p.OnTransport=on;p.Pos.x=x;return p;} ClientlessTransportStop stop(std::string node,float dock,float land){ClientlessTransportStop s;s.Node=node;s.Entry=1;s.Spawn=1;s.Map=0;s.Period=30000;s.Dock.x=dock;s.Points={point(false,land),point(false,land>dock?land-1:land+1),point(true,1)};return s;} using Phase=ClientlessTransportPhase; void run(ClientlessTransportMgr&m,Player&p,ClientlessTransportMgr::Ride&r,int n=20){for(int i=0;i::max()); // Round-trip the actual core transform for a rotated transport. float x=3,y=4,z=5,o=0.7f;TransportBase::CalculatePassengerPosition(x,y,z,&o,100,200,300,1.5708f);TransportBase::CalculatePassengerOffset(x,y,z,&o,100,200,300,1.5708f); assert(std::fabs(x-3)<0.01f&&std::fabs(y-4)<0.01f&&std::fabs(z-5)<0.01f&&std::fabs(o-0.7f)<0.01f); ClientlessTransportMgr mgr;mgr._stops["A"]=stop("A",5,0);mgr._stops["B"]=stop("B",100,104); assert(mgr.HasConnection("A","B"));mgr._stops["B"].Spawn=2;assert(!mgr.HasConnection("A","B"));mgr._stops["B"].Spawn=1; Player p;GameObject go;go.x=5;p.object=&go;std::string error; assert(mgr.Start(&p,"A","B",error));auto&r=mgr._rides.at(26); go.state.RemainingPause=100;run(mgr,p,r);assert(r.Status.Phase==Phase::Waiting&&!p.transport); go.state.RemainingPause=10000;run(mgr,p,r);assert(r.Status.Phase==Phase::Riding&&p.transport==&go.base);assert(std::fabs(p.m_movementInfo.transport.pos.x-1)<0.01); // Wrong location despite a stopped object must not allow disembarkation. go.x=50;p.x=51;run(mgr,p,r);assert(r.Status.Phase==Phase::Riding&&p.transport); go.x=100;p.x=101;run(mgr,p,r);assert(r.Status.Phase==Phase::Arrived&&!p.transport&&p.x==104); // Cancellation on land does not attach a passenger. p.x=0;go.x=5;assert(mgr.Start(&p,"A","B",error));mgr.Cancel(26);auto&cancel=mgr._rides.at(26);run(mgr,p,cancel);assert(cancel.Status.Phase==Phase::Failed&&cancel.Status.Cancelled&&!p.transport); // Midride cancellation retains the Passenger until a recorded safe dock. p.x=0;assert(mgr.Start(&p,"A","B",error));auto&abort=mgr._rides.at(26);run(mgr,p,abort);assert(p.transport); go.x=50;p.x=51;mgr.Cancel(26);run(mgr,p,abort);assert(abort.Status.Phase==Phase::Riding&&p.transport); go.x=5;p.x=6;run(mgr,p,abort);assert(abort.Status.Phase==Phase::Failed&&abort.Status.Cancelled&&!p.transport&&p.x==0); // A timeout aboard never removes the passenger over sea/in midair. assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Failed&&p.transport);go.base.RemovePassenger(&p); // Departure during a boarding transition keeps the Passenger until safe recovery. p.x=0;go.x=5;go.state.Stopped=true;assert(mgr.Start(&p,"A","B",error));auto&late=mgr._rides.at(26); for(int i=0;i<10&&!p.transport;i++)mgr.UpdateRide(&p,late,100); assert(p.transport);go.state.Stopped=false;mgr.UpdateRide(&p,late,100);assert(late.Status.Phase==Phase::Riding&&p.transport); go.state.Stopped=true;run(mgr,p,late);assert(late.Status.Phase==Phase::Failed&&!p.transport); // Recorder captures the coordinate-frame switch and rejects leaving the vessel. p.x=0;ClientlessTransportMgr::Recording rec;rec.Stop=mgr._stops.at("A");rec.Stop.Points={point(false,0)};rec.Object=go.GetGUID(); p.x=1;assert(mgr.Sample(&p,rec,true));p.x=6;go.base.AddPassenger(&p);assert(mgr.Sample(&p,rec,true));assert(rec.Stop.Points.back().OnTransport&&std::fabs(rec.Stop.Points.back().Pos.x-1)<0.01); go.base.RemovePassenger(&p);assert(!mgr.Sample(&p,rec,true));assert(!rec.Error.empty()); // Boarding onto the wrong vehicle and position jumps do not produce recordings. ClientlessTransportMgr::Recording jump;jump.Stop=mgr._stops.at("A");jump.Stop.Points={point(false,0)};jump.Object=go.GetGUID();p.x=20;assert(!mgr.Sample(&p,jump,true)); // Native ships switch between per-map copies of the same spawn. The executor // must resolve the new copy, preserve the ride and leave only at the target. ClientlessTransportMgr ships; ships._stops["harbor_a"]=stop("harbor_a",5,0);ships._stops["harbor_a"].Map=1; ships._stops["harbor_b"]=stop("harbor_b",100,104);ships._stops["harbor_b"].Map=0; Player sailor;GameObject departure;departure.x=5;departure.map=1;sailor.map=1;sailor.object=&departure; assert(ships.Start(&sailor,"harbor_a","harbor_b",error));auto& crossing=ships._rides.at(26); run(ships,sailor,crossing);assert(crossing.Status.Phase==Phase::Riding&&sailor.transport==&departure.base); sailor.pending=true;sailor.object=nullptr; ships.UpdateRide(&sailor,crossing,100); assert(session.completed==1&&crossing.Status.Phase==Phase::Riding); // do not fail during ACK // Model the native worldport ACK's transfer to the destination-map copy. GameObject destination;destination.x=50;destination.map=0; sailor.pending=false;departure.base.RemovePassenger(&sailor);sailor.map=0;sailor.object=&destination; destination.base.AddPassenger(&sailor);sailor.x=51; run(ships,sailor,crossing);assert(crossing.Status.Phase==Phase::Riding&&sailor.transport==&destination.base); destination.x=100;sailor.x=101; run(ships,sailor,crossing);assert(crossing.Status.Phase==Phase::Arrived&&!sailor.transport&&sailor.x==104&&sailor.map==0); // A lost, previously boarded passenger returns to the recorded departure, // never to the moving leader or the destination harbor. Player lost;lost.object=&go;lost.x=50;auto recovery=mgr._rides.at(26); recovery.Status.Phase=Phase::Riding;recovery.Status.Recovering=false;recovery.Abort=false; travel.path=false;mgr.UpdateRide(&lost,recovery,100); assert(recovery.Status.Recovering && lost.teleports==1 && lost.x==0); mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering && recovery.Status.Phase==Phase::Waiting); // A valid walking return uses native point travel, without teleporting. lost.x=50;lost.teleports=0;travel.path=true;recovery.Status.Phase=Phase::Riding; mgr.UpdateRide(&lost,recovery,100);assert(lost.teleports==0 && travel.starts==1); mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering); std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n"; } ''' with tempfile.TemporaryDirectory(prefix='vcore-transport-test-') as directory: path = Path(directory) cpp = path / 'transport.cpp' cpp.write_text(preamble + position + '\n' + offset + world + header + helpers + stubs + methods + animated + tests) subprocess.run(['g++', '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(path / 'test')], check=True) subprocess.run([str(path / 'test')], check=True)