302 lines
22 KiB
Python
302 lines
22 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;
|
|
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 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(', '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());
|
|
// 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);
|
|
// 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);
|
|
}
|
|
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++','-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)
|