Require 500ms remaining dwell to begin elevator boarding, allow exits at stopped docks without a dwell budget, and preserve journeys across interrupted transitions. Recover elevator passengers to their original destination after five minutes. Add regression coverage and elevator recording documentation. Verified production build and live round trips with a dedicated testbot; Mokrak also completed a natural elevator trip.
359 lines
27 KiB
Python
359 lines
27 KiB
Python
#!/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 <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
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;}
|
|
float GetExactDist(Position const&p)const{return GetExactDist(&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;
|
|
constexpr int GAMEOBJECT_TYPE_TRANSPORT=11;
|
|
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;int type=15;TransportDockState state{0,30000,10000,true};
|
|
int GetGoType()const{return type;}
|
|
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,teleportAllowed=true;
|
|
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){if(!teleportAllowed)return false;++teleports;if(transport)transport->RemovePassenger(this);map=m;Relocate(a,b,c,d);falling=false;return true;}
|
|
bool IsInCombat(){return false;}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;}
|
|
};
|
|
struct Node{std::string Key;} node;
|
|
struct Graph{unsigned loads=0;bool EnsureLoaded(){++loads;return true;}Node* FindNode(std::string const&key){node.Key=key;return &node;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
|
struct Core{bool managed=true;bool IsManaged(unsigned){return managed;}}clientless;auto sClientlessPlayerMgr=&clientless;
|
|
struct AreaAccess{std::string denied;bool allowed=true;bool Applies(Player*){return true;}bool AllowsNode(Player*,Node const&n){return allowed&&n.Key!=denied;}}areas;auto sAdventureAreaMgr=&areas;
|
|
namespace ClientlessTransportDeckMgr {bool ready=true;int calls=0;void Clear(uint32){} bool Update(Player*,GameObject*,Position const&,bool,uint32){++calls;return ready;}}
|
|
namespace ClientlessPositioning{bool available=false;bool Nearby(Player*p,Position const&,float,Position&spot,std::vector<Position>&path){if(!available)return false;spot=*p;spot.x+=2;path={*p,spot};return true;}}
|
|
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;template<class...Args>QueryResult 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 [
|
|
' char const* PhaseName(', ' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength('
|
|
])
|
|
methods = '\n'.join(function(source, marker) for marker in [
|
|
'bool ClientlessTransportMgr::ResumePassenger(', 'bool ClientlessTransportMgr::SelectResumedDock(',
|
|
'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(',
|
|
'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(',
|
|
'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(',
|
|
'void ClientlessTransportMgr::RecoverPassenger(', 'bool ClientlessTransportMgr::RecoverStuckElevator(',
|
|
'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::map<uint32,TransportAnimationEntry const*>Path;std::map<uint32,TransportRotationEntry const*>Rotations;};
|
|
struct Animated {
|
|
GameObject _owner;TransportAnimation* _animationInfo=nullptr;uint32 _pathProgress=0;std::vector<uint32>_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<n;i++)m.UpdateRide(&p,r,100);}
|
|
int main(){
|
|
// Regression: a real-world-point SMALLINT map must survive loading as 369.
|
|
uint32 loadedMap=0;Position loadedPosition;assert(LoadNode("tram_sw",loadedMap,loadedPosition));assert(loadedMap==369);assert(loadedPosition.x==1&&loadedPosition.y==2&&loadedPosition.z==3);
|
|
// Actual animation-state method: no state change is required for an animation pause.
|
|
TransportAnimationEntry a{0,{0,0,0}},b{5000,{0,0,0}},c{15000,{0,0,-60}},d{20000,{0,0,-60}},e{30033,{0,0,0}};
|
|
TransportAnimation anim;anim.Path={{0,&a},{5000,&b},{15000,&c},{20000,&d},{30033,&e}};
|
|
Animated animated;animated._animationInfo=&anim;TransportDockState snapshot;
|
|
animated._pathProgress=1000;assert(animated.GetTransportDockState(snapshot));assert(snapshot.Stopped&&snapshot.RemainingPause==4000);
|
|
animated._pathProgress=5000;animated.GetTransportDockState(snapshot);assert(!snapshot.Stopped);
|
|
animated._pathProgress=17000;animated.GetTransportDockState(snapshot);assert(snapshot.Stopped&&snapshot.RemainingPause==3000);
|
|
TransportRotationEntry r0,r1;r1.Z=1;anim.Rotations={{0,&r0},{30033,&r1}};animated.GetTransportDockState(snapshot);assert(!snapshot.Stopped);
|
|
anim.Rotations.clear();animated._stopFrames={1000};animated._owner.deadline=2000;GameTime::now=1000;animated.GetTransportDockState(snapshot);assert(!snapshot.Stopped);
|
|
GameTime::now=2000;animated.GetTransportDockState(snapshot);assert(snapshot.Stopped&&snapshot.RemainingPause==std::numeric_limits<uint32>::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;ClientlessTransportDeckMgr::ready=false;
|
|
run(mgr,p,r);assert(r.Status.Phase==Phase::Riding&&p.transport);
|
|
ClientlessTransportDeckMgr::ready=true;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::Riding&&timeout.Abort&&timeout.RetryDelay==3000&&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());
|
|
// Native boarding height snaps and coalesced movement may exceed eight
|
|
// yards. Accept the measured path; retain the total sixty-yard bound.
|
|
ClientlessTransportMgr::Recording tall;tall.Stop=mgr._stops.at("A");tall.Stop.Points={point(false,0)};tall.Object=go.GetGUID();
|
|
p.Relocate(6,0,12);go.base.AddPassenger(&p);assert(mgr.Sample(&p,tall,true));assert(tall.Stop.Points.back().OnTransport);
|
|
go.base.RemovePassenger(&p);p.Relocate(20,0,0);
|
|
ClientlessTransportMgr::Recording gap;gap.Stop=mgr._stops.at("A");gap.Stop.Points={point(false,0)};gap.Object=go.GetGUID();assert(mgr.Sample(&p,gap,true));
|
|
ClientlessTransportMgr::Recording excessive;excessive.Stop=mgr._stops.at("A");excessive.Stop.Points={point(false,0)};excessive.Object=go.GetGUID();p.x=61;assert(!mgr.Sample(&p,excessive,true));assert(excessive.Error.find("60 yards")!=std::string::npos);p.z=0;
|
|
// 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);
|
|
// Waiting spots return along the observed path before the existing boarding sequence.
|
|
Player waiting;GameObject dock;dock.x=5;dock.state.Stopped=false;waiting.object=&dock;
|
|
ClientlessPositioning::available=true;
|
|
assert(mgr.Start(&waiting,"A","B",error));auto& spaced=mgr._rides.at(26);
|
|
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==1&&waiting.x==2&&!waiting.transport);
|
|
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==2&&spaced.WaitTrace.size()==2);
|
|
dock.state.Stopped=true;dock.state.RemainingPause=10000;
|
|
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==3&&waiting.x==0&&!waiting.transport);
|
|
run(mgr,waiting,spaced);assert(spaced.Status.Phase==Phase::Riding&&waiting.transport);
|
|
dock.x=100;waiting.x=101;run(mgr,waiting,spaced);
|
|
assert(spaced.Status.Phase==Phase::Arrived&&!waiting.transport&&waiting.x==104);
|
|
ClientlessPositioning::available=false;
|
|
Player denied;denied.object=&dock;areas.allowed=false;
|
|
assert(!mgr.Start(&denied,"A","B",error));areas.allowed=true;
|
|
// Restored attachment without transient CPC state is adopted, never reboarded.
|
|
ClientlessTransportMgr resumed;resumed._stops["A"]=stop("A",5,0);resumed._stops["B"]=stop("B",100,104);
|
|
Player orphan;GameObject vessel;vessel.x=50;vessel.state.Stopped=false;orphan.object=&vessel;orphan.x=51;
|
|
vessel.base.AddPassenger(&orphan);orphan.m_movementInfo.transport.pos.x=1;
|
|
assert(resumed.ResumePassenger(&orphan));auto& adopted=resumed._rides.at(26);
|
|
assert(adopted.Resumed&&adopted.Status.Phase==Phase::Riding&&orphan.teleports==0&&graph.loads>0);
|
|
run(resumed,orphan,adopted);assert(orphan.transport==&vessel.base&&adopted.Status.Phase==Phase::Riding);
|
|
// B is the first actual permitted stop, even though seed A was selected.
|
|
vessel.x=100;vessel.state.Stopped=true;orphan.x=101;
|
|
resumed.UpdateRide(&orphan,adopted,100);assert(adopted.To.Node=="B");
|
|
assert(resumed.ResumePassenger(&orphan));run(resumed,orphan,adopted);
|
|
assert(adopted.Status.Phase==Phase::Arrived&&!orphan.transport&&orphan.x==104);
|
|
assert(!resumed.ResumePassenger(&orphan));
|
|
// After detachment the remaining land transition still owns the bot until done.
|
|
adopted.Status.Phase=Phase::Leaving;assert(resumed.ResumePassenger(&orphan));
|
|
adopted.Status.Phase=Phase::Arrived;assert(!resumed.ResumePassenger(&orphan));
|
|
// Existing normal rides are never replaced by passenger adoption.
|
|
vessel.x=5;orphan.x=0;assert(resumed.Start(&orphan,"A","B",error));run(resumed,orphan,resumed._rides.at(26));
|
|
assert(!resumed.ResumePassenger(&orphan)&&!resumed._rides.at(26).Resumed);
|
|
// Unknown vehicles, unmanaged players and entirely forbidden services are untouched.
|
|
resumed._rides.clear();clientless.managed=false;assert(!resumed.ResumePassenger(&orphan));clientless.managed=true;
|
|
vessel.spawn=99;assert(!resumed.ResumePassenger(&orphan));vessel.spawn=1;
|
|
areas.allowed=false;assert(!resumed.ResumePassenger(&orphan));areas.allowed=true;
|
|
// An adopted passenger must not leave at a subsequently forbidden dock.
|
|
vessel.x=50;orphan.x=51;assert(resumed.ResumePassenger(&orphan));areas.denied="B";
|
|
vessel.x=100;orphan.x=101;run(resumed,orphan,resumed._rides.at(26));assert(orphan.transport);
|
|
areas.denied.clear();run(resumed,orphan,resumed._rides.at(26));assert(!orphan.transport&&orphan.x==104);
|
|
// Native cross-map ship restoration keeps the same service after worldport ACK.
|
|
resumed._rides.clear();resumed._stops["A"].Map=1;
|
|
GameObject copy1;copy1.map=1;copy1.x=50;copy1.state.Stopped=false;
|
|
orphan.map=1;orphan.object=©1;copy1.base.AddPassenger(&orphan);orphan.x=51;
|
|
assert(resumed.ResumePassenger(&orphan));run(resumed,orphan,resumed._rides.at(26));assert(orphan.transport);
|
|
copy1.base.RemovePassenger(&orphan);orphan.map=0;orphan.object=&vessel;vessel.x=100;
|
|
vessel.base.AddPassenger(&orphan);orphan.x=101;orphan.m_movementInfo.transport.pos.x=1;
|
|
run(resumed,orphan,resumed._rides.at(26));assert(!orphan.transport&&orphan.map==0&&orphan.x==104);
|
|
// Lost attachment during both boarding and disembarkation uses recorded departure rescue.
|
|
travel.path=false;travel.present=false;
|
|
for(auto phase:{Phase::Boarding,Phase::Leaving}){
|
|
Player fallen;fallen.object=&vessel;fallen.x=50;fallen.falling=true;
|
|
auto ride=adopted;ride.From=stop("A",5,0);ride.To=stop("B",100,104);
|
|
ride.Status.Phase=phase;ride.Status.Recovering=false;ride.Abort=false;ride.Elapsed=0;ride.RetryDelay=0;
|
|
ride.Point=phase==Phase::Boarding?3:2;ride.Moving=false;
|
|
resumed.UpdateRide(&fallen,ride,100);
|
|
assert(ride.Status.Recovering&&fallen.teleports==1&&fallen.x==0);
|
|
}
|
|
// Elevators need 500ms to begin boarding; exiting needs only the stopped dock.
|
|
for(bool reverse:{false,true}){
|
|
ClientlessTransportMgr lift;lift._stops["A"]=stop("A",5,0);lift._stops["B"]=stop("B",100,104);
|
|
Player rider;GameObject platform;platform.type=11;rider.object=&platform;
|
|
auto from=reverse?"B":"A",to=reverse?"A":"B";
|
|
platform.x=reverse?100:5;rider.x=reverse?104:0;platform.state.RemainingPause=499;platform.state.Stopped=false;
|
|
assert(lift.Start(&rider,from,to,error));auto& ride=lift._rides.at(26);
|
|
run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport);
|
|
platform.state.Stopped=true;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport);
|
|
platform.state.RemainingPause=500;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
|
platform.x=50;rider.x=51;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
|
platform.x=reverse?5:100;rider.x=platform.x+1;platform.state.Stopped=false;
|
|
run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
|
platform.state.Stopped=true;platform.state.RemainingPause=1;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Arrived&&!rider.transport&&rider.x==(reverse?0:104));
|
|
// Actual passenger time, not waiting on land, gates the five-minute rescue.
|
|
rider.x=reverse?104:0;platform.x=reverse?100:5;platform.state.RemainingPause=500;assert(lift.Start(&rider,from,to,error));run(lift,rider,lift._rides.at(26));
|
|
auto& stuck=lift._rides.at(26);platform.x=50;rider.x=51;platform.state.Stopped=false;
|
|
stuck.ElevatorPassengerMs=0;stuck.Elapsed=0;lift.UpdateRide(&rider,stuck,299999);
|
|
assert(rider.teleports==0&&rider.transport&&stuck.ElevatorDestination==to&&!stuck.Abort);
|
|
lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==1&&!rider.transport&&rider.x==(reverse?0:104));
|
|
lift.UpdateRide(&rider,stuck,1);assert(stuck.Status.Phase==Phase::Arrived);
|
|
lift.UpdateRide(&rider,stuck,300000);assert(rider.teleports==1);
|
|
// A cancelled/retried exit cannot change the original recovery destination.
|
|
stuck.Status.Phase=Phase::Riding;stuck.ElevatorFallback=false;stuck.Abort=true;
|
|
stuck.To=stuck.From;stuck.RetryDelay=3000;platform.base.AddPassenger(&rider);
|
|
lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==2&&rider.x==(reverse?0:104));
|
|
}
|
|
// A ship, a missing/foreign attachment or a forbidden endpoint cannot use elevator rescue.
|
|
ClientlessTransportMgr rescue;rescue._stops["A"]=stop("A",5,0);rescue._stops["B"]=stop("B",100,104);
|
|
Player passenger;GameObject vehicle;passenger.object=&vehicle;vehicle.base.AddPassenger(&passenger);
|
|
ClientlessTransportMgr::Ride stranded;stranded.From=rescue._stops["A"];stranded.To=rescue._stops["B"];
|
|
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports);
|
|
vehicle.type=11;vehicle.base.RemovePassenger(&passenger);
|
|
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!stranded.ElevatorPassengerMs);
|
|
vehicle.base.AddPassenger(&passenger);areas.denied="B";
|
|
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports);areas.denied.clear();
|
|
passenger.teleportAllowed=false;assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1)&&!stranded.ElevatorFallback);
|
|
passenger.teleportAllowed=true;assert(rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1));
|
|
// Departure during an elevator transition retains the live journey, whereas
|
|
// walking the final land segment no longer depends on the departed platform.
|
|
ClientlessTransportMgr quick;quick._stops["A"]=stop("A",5,0);quick._stops["B"]=stop("B",100,104);
|
|
Player commuter;GameObject cabin;cabin.type=11;cabin.state.Stopped=false;commuter.object=&cabin;cabin.base.AddPassenger(&commuter);
|
|
ClientlessTransportMgr::Ride interrupted;interrupted.From=quick._stops["A"];interrupted.To=quick._stops["B"];
|
|
interrupted.Status.Phase=Phase::Boarding;interrupted.Point=2;
|
|
quick.UpdateRide(&commuter,interrupted,100);
|
|
assert(interrupted.Status.Phase==Phase::Riding&&!interrupted.Abort&&commuter.transport);
|
|
cabin.base.RemovePassenger(&commuter);commuter.x=103;
|
|
interrupted.Status.Phase=Phase::Leaving;interrupted.Point=1;interrupted.Moving=false;
|
|
run(quick,commuter,interrupted);assert(interrupted.Status.Phase==Phase::Arrived&&commuter.x==104);
|
|
std::cout<<"Transport dock, elevator pause/five-minute rescue, 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++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-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)
|