Files
Viano-Core/tests/clientless/test_transport.py
T
Nico 1164a72ea3 Implement clientless transports, Lua reload and same-map portals
Add native transport recording and execution with grid lifecycle handling, clientless teleport acknowledgements, and regression tests. Support targeted same-map portal imports and preserve disabled taxi nodes. Document live transport and portal verification and the remaining portal inventory.
2026-10-26 11:27:56 +01:00

215 lines
15 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;}
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;
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;
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;}}
struct Travel {void CancelTravel(uint32){}}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 [
' 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::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;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);
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)