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.
This commit is contained in:
Nico committed 2026-10-26 11:27:56 +01:00
1 parent 01a9ba4b0b
commit 1164a72ea3
40 files changed
+4533 -163

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
"""Compile the production reload/bindings with real Lua and deterministic server doubles."""
import pathlib
import subprocess
import tempfile
ROOT = pathlib.Path(__file__).resolve().parents[2]
SOURCE = (ROOT / "src/server/game/World/ClientlessLuaEngine.cpp").read_text()
def function(signature):
start = SOURCE.index(signature)
opening = SOURCE.index("{", start)
depth = 1
end = opening + 1
while depth:
depth += (SOURCE[end] == "{") - (SOURCE[end] == "}")
end += 1
return SOURCE[start:end]
HARNESS = r'''
#include <lua.hpp>
#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <utility>
using uint32 = uint32_t;
#define TC_LOG_INFO(...) ((void)0)
struct ClientlessIntent {
std::string Name;
std::unordered_map<std::string,std::string> Parameters;
bool Empty() const { return Name.empty(); }
};
using ClientlessAction = ClientlessIntent;
struct ClientlessAdventureProfile { int Revision = 0; };
struct Guid { uint32 GetCounter() const { return 26; } };
struct Player {
bool aboard = false, flying = false, teleport = false;
Guid GetGUID() { return {}; }
bool IsInWorld() { return true; }
bool GetTransport() { return aboard; }
bool IsInFlight() { return flying; }
bool IsBeingTeleported() { return teleport; }
};
enum class ClientlessActionExecutionState { Idle, SelectingTarget, Moving, Completed, Failed };
enum class ClientlessTravelState { Idle, Moving, Arrived };
struct ActionStatus { ClientlessActionExecutionState State = ClientlessActionExecutionState::Idle; };
struct TravelStatus { ClientlessTravelState State = ClientlessTravelState::Idle; };
struct Manager {
bool managed = true;
std::vector<ClientlessAction> actions;
ClientlessIntent intent;
bool IsManaged(uint32) { return managed; }
unsigned GetQueuedActionCount(uint32) { return actions.size(); }
bool QueueAction(uint32, ClientlessAction a) { actions.push_back(std::move(a)); return true; }
bool SetIntent(uint32, ClientlessIntent i) { intent = std::move(i); return true; }
bool ClearIntent(uint32) { intent = {}; return true; }
} manager;
struct Executor { ActionStatus status; ActionStatus* GetStatus(uint32) { return &status; } } executor;
struct Travel { TravelStatus status; TravelStatus* GetStatus(uint32) { return &status; } } travel;
auto* sClientlessPlayerMgr = &manager;
auto* sClientlessActionExecutorMgr = &executor;
auto* sClientlessTravelMgr = &travel;
class ClientlessLuaEngine {
public:
std::unordered_map<uint32,ClientlessAdventureProfile> _profiles;
std::string _scriptRoot = "/test";
std::string script;
bool profileOK = true;
bool LoadAdventureProfile(Player*) {
if (!profileOK) return false;
_profiles[26].Revision++;
return true;
}
bool ExecuteAdventureScript(Player*);
bool ReloadAdventureScript(Player*, std::string&);
};
'''
HARNESS += SOURCE[SOURCE.index("struct ReloadEffects"):SOURCE.index("uint32 BoundGuid")]
for signature in ["uint32 BoundGuid", "std::unordered_map<std::string, std::string> ReadMap",
"int LuaIntend", "int LuaClearIntent", "int LuaQueueAction"]:
HARNESS += function(signature) + "\n"
HARNESS += r'''
bool ClientlessLuaEngine::ExecuteAdventureScript(Player*) {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
for (auto entry : {std::pair{"intend", LuaIntend}, {"clear_intent", LuaClearIntent}, {"queue_action", LuaQueueAction}}) {
lua_pushinteger(L,26); lua_pushcclosure(L,entry.second,1); lua_setglobal(L,entry.first);
}
bool success = luaL_loadstring(L, script.c_str()) == LUA_OK && lua_pcall(L,0,0,0) == LUA_OK;
lua_close(L);
return success;
}
'''
HARNESS += function("bool ClientlessLuaEngine::ReloadAdventureScript")
HARNESS += r'''
int main() {
Player p;
ClientlessLuaEngine engine;
std::string detail;
engine._profiles[26].Revision = 4;
manager.intent.Name = "old";
engine.script = "queue_action('move', {node='tram_if'}); intend('new'); error('failure')";
assert(!engine.ReloadAdventureScript(&p,detail));
assert(manager.actions.empty() && manager.intent.Name == "old");
assert(engine._profiles.at(26).Revision == 4 && !pendingReload);
engine.script = "this is invalid lua";
assert(!engine.ReloadAdventureScript(&p,detail));
assert(engine._profiles.at(26).Revision == 4 && manager.actions.empty());
engine.profileOK = false;
assert(!engine.ReloadAdventureScript(&p,detail));
engine.profileOK = true;
engine.script = "assert(not queue_action('')); queue_action('move', {node='tram_if'}); clear_intent(); intend('new')";
assert(engine.ReloadAdventureScript(&p,detail));
assert(manager.actions.size() == 1 && manager.actions[0].Parameters.at("node") == "tram_if");
assert(manager.intent.Name == "new" && engine._profiles.at(26).Revision == 5);
assert(!engine.ReloadAdventureScript(&p,detail)); // queued actions must not duplicate
manager.actions.clear();
executor.status.State = ClientlessActionExecutionState::Moving;
assert(!engine.ReloadAdventureScript(&p,detail));
executor.status.State = ClientlessActionExecutionState::Completed;
travel.status.State = ClientlessTravelState::Moving;
assert(!engine.ReloadAdventureScript(&p,detail));
travel.status.State = ClientlessTravelState::Arrived;
p.aboard = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.aboard = false;
p.flying = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.flying = false;
p.teleport = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.teleport = false;
assert(!engine.ReloadAdventureScript(nullptr,detail));
manager.managed = false; assert(!engine.ReloadAdventureScript(&p,detail)); manager.managed = true;
engine._profiles.clear(); engine.script = "error('failure')";
assert(!engine.ReloadAdventureScript(&p,detail) && engine._profiles.empty());
engine.script = "clear_intent()";
assert(engine.ReloadAdventureScript(&p,detail) && manager.intent.Empty());
}
'''
with tempfile.TemporaryDirectory(prefix="vcore-lua-reload-") as folder:
source = pathlib.Path(folder) / "test.cpp"
binary = pathlib.Path(folder) / "test"
source.write_text(HARNESS)
flags = subprocess.check_output(["pkg-config", "--cflags", "--libs", "lua5.4"], text=True).split()
subprocess.run(["c++", "-std=c++17", str(source), "-o", str(binary), *flags], check=True)
subprocess.run([str(binary)], check=True)
print("Lua reload regression tests passed (production reload/bindings, real Lua 5.4).")
+83
View File
@@ -0,0 +1,83 @@
"""Exercise the actual portal handler/ACK/destination checks with Core doubles."""
from pathlib import Path
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
source = (ROOT / "src/server/game/World/ClientlessActionExecutorMgr.cpp").read_text()
start = source.index(" WorldPacket areaTriggerPacket(CMSG_AREATRIGGER, 4);")
end = source.index(" record.Status.State = ClientlessActionExecutionState::Moving;", start)
fragment = source[start:end]
assert "same-map portal transition is not supported" not in source
harness = r'''
#include <cassert>
#include <cmath>
#include <string>
#define TC_LOG_WARN(...) ((void)0)
enum class ClientlessActionExecutionState { Idle, Failed, Moving };
constexpr int CMSG_AREATRIGGER=1;
struct WorldPacket { WorldPacket(int,int){} void operator<<(int){} };
struct Player;
struct Session {
Player* p;
int accepted=0,ackCount=0;
bool clientless=true,ackWorks=true;
void HandleAreaTriggerOpcode(WorldPacket&);
void CompleteClientlessTeleport();
bool IsClientless() { return clientless; }
};
struct Player {
int map=1; float x=8393,y=1003,z=30;
bool pending=false,world=true;
Session session{this};
bool IsBeingTeleported() { return pending; }
bool IsBeingTeleportedFar() { return pending&&map==0; }
bool IsInWorld() { return world; }
int GetMapId() { return map; }
float GetPositionX() { return x; } float GetPositionY() { return y; } float GetPositionZ() { return z; }
Session* GetSession() { return &session; }
};
struct Teleport { int target_mapId=1; float target_X=9946,target_Y=2612,target_Z=1316; } destination;
void Session::HandleAreaTriggerOpcode(WorldPacket&) { if (accepted) p->pending=true; }
void Session::CompleteClientlessTeleport() {
++ackCount;
if (!ackWorks) return;
p->pending=false;p->map=destination.target_mapId;
p->x=destination.target_X;p->y=destination.target_Y;p->z=destination.target_Z;
}
struct Record {
struct {ClientlessActionExecutionState State=ClientlessActionExecutionState::Idle; std::string Detail,TargetName;} Status;
bool Active=true;
} record;
struct {int EdgeDatabaseId=1;std::string NodeKey="portal_at_542_target";} step;
bool transfer(Player* player) {
int triggerId=542,characterGuid=26;
auto teleport=&destination;
'''
harness += fragment
harness += r'''
return true;
}
int main() {
Player denied;
assert(!transfer(&denied)&&!record.Active&&record.Status.State==ClientlessActionExecutionState::Failed);
Player near;near.session.accepted=1;record={};
assert(transfer(&near)&&near.session.ackCount==1&&near.map==1&&near.x==9946&&!near.pending);
Player far;far.map=0;far.session.accepted=1;record={};
assert(transfer(&far)&&far.session.ackCount==1&&far.map==1&&!far.pending);
Player pending;pending.session.accepted=1;pending.session.ackWorks=false;record={};
assert(!transfer(&pending)&&pending.pending&&!record.Active);
Player network;network.session.accepted=1;network.session.clientless=false;record={};
assert(!transfer(&network)&&!record.Active);
Player wrongMap;wrongMap.map=0;record={};assert(!transfer(&wrongMap));
Player alreadyAtTarget;alreadyAtTarget.x=destination.target_X;alreadyAtTarget.y=destination.target_Y;alreadyAtTarget.z=destination.target_Z;
record={};assert(transfer(&alreadyAtTarget)); // destination reached independently is valid
}
'''
with tempfile.TemporaryDirectory(prefix="vcore-portal-test-") as folder:
cpp, binary = Path(folder) / "test.cpp", Path(folder) / "test"
cpp.write_text(harness)
subprocess.run(["c++", "-std=c++17", str(cpp), "-o", str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print("Portal near/far ACK and denied same-map transfer regressions passed.")
@@ -0,0 +1,23 @@
"""Execute production TaxiImport UPDATE statements against enabled/disabled rows."""
from pathlib import Path
import ast,re,sqlite3
source=(Path(__file__).resolve().parents[2]/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
source=source[source.index('static bool HandleClientlessTaxiImport'):source.index('static bool HandleClientlessPortalImport')]
statements=[]
for group in re.findall(r'WorldDatabase\.DirectPExecute\(\s*((?:"[^"\n]*"\s*)+),',source):
query=''.join(ast.literal_eval(s) for s in re.findall(r'"[^"\n]*"',group))
if query.startswith('UPDATE '): statements.append(query)
assert len(statements)==2
for enabled in (0,1):
db=sqlite3.connect(':memory:')
db.create_function('IF',3,lambda condition,yes,no:yes if condition else no)
db.executescript('CREATE TABLE vcore_clientless_world_point (point_key TEXT,category TEXT,map INT,position_x REAL,position_y REAL,position_z REAL,orientation REAL,enabled INT,comment TEXT); CREATE TABLE vcore_clientless_travel_node (node_key TEXT,node_type TEXT,world_point_key TEXT,enabled INT,comment TEXT);')
db.execute('INSERT INTO vcore_clientless_world_point VALUES (?,?,?,?,?,?,?,?,?)',('taxi_585','travel_taxi',1,0,0,0,0,enabled,'manual state'))
db.execute('INSERT INTO vcore_clientless_travel_node VALUES (?,?,?,?,?)',('taxi_585','taxi','taxi_585',enabled,'manual state'))
db.execute(statements[0]%(1,8334.76,1029.83,22.7917,585,'taxi_585'))
db.execute(statements[1]%('taxi_585',585,'taxi_585'))
for table in ('vcore_clientless_world_point','vcore_clientless_travel_node'):
flag,comment=db.execute('SELECT enabled,comment FROM '+table).fetchone()
assert flag==enabled
assert comment.startswith('Auto-imported') if enabled else comment=='manual state'
print('Taxi importer preserves disabled rows and their reasons.')
+214
View File
@@ -0,0 +1,214 @@
#!/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)
@@ -0,0 +1,110 @@
"""Exercise actual core grid initialization without bots or a world database."""
from pathlib import Path
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
def block(source, signature):
start = source.index(signature)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]
maps = (ROOT / "src/server/game/Maps/Map.cpp").read_text()
objects = (ROOT / "src/server/game/Globals/ObjectMgr.cpp").read_text()
manager = (ROOT / "src/server/game/Maps/MapManager.cpp").read_text()
registration = block(block(objects, "void ObjectMgr::LoadGameObjects()"), "if (gameEvent == 0)")
for function in ("Map* MapManager::CreateWorldMap", "InstanceMap* MapManager::CreateInstance",
"BattlegroundMap* MapManager::CreateBattleground"):
assert "map->LoadTransportGameObjectGrids();" in block(manager, function)
assert manager.index("map->LoadRespawnTimes();") < manager.index("map->LoadTransportGameObjectGrids();")
harness = r'''
#include <cassert>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <set>
#include <utility>
using uint32 = uint32_t;
struct ObjectGuid { using LowType = uint32; };
#define TC_LOG_INFO(...) ((void)0)
constexpr int GAMEOBJECT_TYPE_TRANSPORT = 11;
struct Position {
float x,y;
float GetPositionX() const { return x; }
float GetPositionY() const { return y; }
};
struct GameObjectData { uint32 mapId,spawnMask; Position spawnPoint; };
struct Template { int type; };
struct ObjectManager {
std::unordered_map<uint32,std::vector<uint32>> _transportGameObjectSpawns;
std::unordered_map<uint32,GameObjectData> data;
unsigned ordinaryGridRegistrations = 0;
void AddGameobjectToGrid(uint32,GameObjectData*) { ++ordinaryGridRegistrations; }
void Register(int gameEvent, uint32 guid, GameObjectData data, Template const* gInfo) {
'''
harness += registration
harness += r'''
}
auto GetTransportGameObjectSpawns(uint32 id) const -> std::vector<uint32> const* {
auto itr = _transportGameObjectSpawns.find(id);
return itr==_transportGameObjectSpawns.end() ? nullptr : &itr->second;
}
auto GetGameObjectData(uint32 id) const -> GameObjectData const* {
auto itr=data.find(id); return itr==data.end() ? nullptr : &itr->second;
}
} objects;
auto* sObjectMgr = &objects;
struct Cell { float x,y; Cell(float a,float b):x(a),y(b){} };
class Map {
public:
uint32 id=369, mode=0;
std::set<std::pair<int,int>> loaded;
uint32 GetId() { return id; }
uint32 GetInstanceId() { return 0; }
uint32 GetSpawnMode() { return mode; }
bool EnsureGridLoaded(Cell c) { return loaded.emplace(int(c.x/533),int(c.y/533)).second; }
void LoadTransportGameObjectGrids();
};
'''
harness += block(maps, "void Map::LoadTransportGameObjectGrids()")
harness += r'''
int main() {
Template tram{11}, ship{15}, chest{3};
GameObjectData sw{369,1,{4,2490}}, ironforge{369,1,{4,8}};
objects.Register(0,18803,ironforge,&tram);
objects.Register(0,18804,sw,&tram);
objects.Register(7,1,sw,&tram); // inactive/active events retain their own lifecycle
objects.Register(-7,2,sw,&tram);
objects.Register(0,3,sw,&ship); // separate native moving-transport loader
objects.Register(0,4,sw,&chest);
assert(objects.ordinaryGridRegistrations==4);
assert(objects.GetTransportGameObjectSpawns(369)->size()==2);
objects.data.emplace(18803,ironforge); objects.data.emplace(18804,sw);
Map map;
// Both station grids must load without any player, bot or transport node.
map.LoadTransportGameObjectGrids();
assert(map.loaded.size()==2);
map.LoadTransportGameObjectGrids(); assert(map.loaded.size()==2);
Map unrelated; unrelated.id=0; unrelated.LoadTransportGameObjectGrids();
assert(unrelated.loaded.empty());
Map differentDifficulty; differentDifficulty.mode=1;
differentDifficulty.LoadTransportGameObjectGrids(); assert(differentDifficulty.loaded.empty());
objects.data.erase(18803); // removed spawn indexed earlier must be skipped
objects.data[18804].mapId=0; // likewise a moved spawn
Map removed; removed.LoadTransportGameObjectGrids(); assert(removed.loaded.empty());
}
'''
with tempfile.TemporaryDirectory(prefix="vcore-grid-transport-") as folder:
source, executable = Path(folder) / "test.cpp", Path(folder) / "test"
source.write_text(harness)
subprocess.run(["c++", "-std=c++17", str(source), "-o", str(executable)], check=True)
subprocess.run([str(executable)], check=True)
print("Core transport grid initialization regressions passed (no CPC dependency).")