Add local deck navigation for AdventureBot transport passengers

This commit is contained in:
Nico committed 2026-10-29 12:02:25 +01:00
1 parent 49712aadfe
commit 2841ee973b
19 files changed
+693 -5

No files matched your search

+44
View File
@@ -1,3 +1,47 @@
# Aktuelle Aufgabe: freie Bordnavigation (2026-10-29)
Vorheriger Schritt lokal committed als49712aadfe. Bordnavigation implementiert
und getestet; lokaler Commit vom Nutzer angefordert.
Nutzer hat lokale Navigationsnetze für Tiefenbahnwagen und The Bravery beauftragt.
TransportDeckMesh baut Recast/Detour-Netze aus vorhandenen VMAP-Modellen;
ClientlessTransportDeckMgr steuert Platzwahl bei Halt und Rückkehr vor Ausstieg.
Modelle werden pro Display einmal erzeugt, nur Bravery176310 und Tram176080-86.
Recast ist jetzt als private game-Abhängigkeit eingebunden, daher großer Build.
Build erfolgreich: /tmp/vcore-deck-build.log; installiert und neu gestartet.
Build3 mit korrigiertem Abfahrtsbudget erfolgreich und installiert.
Finaler Testserver PID270857, tmux vcore-worldserver, Konsole:
/tmp/vcore-deck-final-20261029.log.
Frischer ready-Start und TCP8085 verifiziert.
Echte Geometrietests: Schiff1678Polygone/259 erreichbare Rasterziele mit Rückweg,
Tram105/59. Alle sechs gezielten Tests erfolgreich, auch Controller-/Transport-/Gruppenflugtests.
Native Diagnose bestätigt beide Netze. Kontrollierte Fahrt Renaisia70 von
ship_sw_br nach ship_ruttheran_br: abgeschlossen; Schedule70 ist wieder ON
(confirmed Schedule mode updated). Sie hat einen
2m-Netzweg an Bord zurückgelegt; gespeicherte lokale Position bestätigt Ziel
(-1.74993,7.03908,6.14418) während fahrendem Schiff. Rückweg0.79m am Ziel,
Leaving und Arrived/passenger0 bestätigt. Noralin80 wurde per nativem tele name
(offline) an DeeprunTram gesetzt und eingeloggt; normaler Tagesplan führt sie
von tram_if nach tram_sw. Erste Tramfahrt abgeschlossen:11:40:47Riding;11:41:55Rückweg1.07m;
11:41:57Arrived/passenger0. Keine manuelle Scheduleänderung bei80.
Das anfängliche Bordverteilen war zu konservativ zeitlich reserviert; Build3
reserviert an Abfahrt nur Hinweg+2s, Rückweg/Ausstieg am Ziel. Controltest passt.
Finaler Start erfolgreich. Native Diagnose für weiteres Tram-Entry176080
bestätigt ebenfalls105Polygone/59Ziele+Rückwege. Noralin80 offline erneut per
tele name an DeeprunTram gesetzt und eingeloggt; ihr normaler Tagesplan nimmt
wieder tram_if->tram_sw. KEIN manueller Scheduleoverride80 (daher nichts
zurückzustellen). Finale Tram-Bordverteilung bestätigt11:48:02,2m, lokales Ziel per saveall
bestätigt. Rückweg11:49:13,Leaving11:49:19; unter massiven Worldtick-Stalls
(50.8s/8ticks) Abfahrt11:49:26 während Ausstieg: bestehende Sicherung bleibt
angehängt/Riding+Abort bis sicherem Dock. KEIN vollständiger finaler Ausstieg
behaupten. Frühere vollständige Schiff-/Tramfahrten waren erfolgreich.
Recovery80 abgeschlossen11:51:36 Failed/passenger0='cancelled at safe dock';
sicher an Land ausgestiegen, normaler Tagesplan wieder aktiv. Kein manueller
Override bleibt; Schiff70ScheduleON,80war nie überschrieben.
Siehe transport-deck-navigation.md. Keine Schema-/Config-Änderung; native Testpositionierung80 dokumentiert.
Änderungen werden lokal committed; kein Push.
---
# Umsetzung 2026-10-29: Navigation und Verhaltensgrenzen
Die unten festgehaltene Liste ist implementiert; lokaler Commit vom Nutzer angefordert.
@@ -0,0 +1,21 @@
2026-10-29_11:36:29 INFO [server.adventurebot.navigation] DECK_MESH entry=176310 polygons=1678 error=''
2026-10-29_11:36:29 INFO [server.adventurebot.navigation] DECK_MESH entry=176081 polygons=105 error=''
2026-10-29_11:36:57 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=70 phase='Boarding' from='ship_sw_br' to='ship_ruttheran_br' passenger=0 detail='boarding recorded land-to-deck transition'
2026-10-29_11:37:04 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=70 phase='Riding' from='ship_sw_br' to='ship_ruttheran_br' passenger=1 detail='aboard; waiting for destination dock'
2026-10-29_11:37:04 INFO [server.adventurebot.navigation] DECK_WALK guid=70 points=3 length=2.00 target=(-1.750 7.039 6.144)
2026-10-29_11:38:42 INFO [server.adventurebot.navigation] DECK_WALK guid=70 points=2 length=0.79 target=(-1.878 7.822 6.144)
2026-10-29_11:38:42 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=70 phase='Leaving' from='ship_sw_br' to='ship_ruttheran_br' passenger=1 detail='leaving via reversed recorded transition'
2026-10-29_11:38:44 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=70 phase='Arrived' from='ship_sw_br' to='ship_ruttheran_br' passenger=0 detail='arrived at shared destination node'
2026-10-29_11:40:44 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Boarding' from='tram_if' to='tram_sw' passenger=0 detail='boarding recorded land-to-deck transition'
2026-10-29_11:40:47 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Riding' from='tram_if' to='tram_sw' passenger=1 detail='aboard; waiting for destination dock'
2026-10-29_11:41:55 INFO [server.adventurebot.navigation] DECK_WALK guid=80 points=2 length=1.07 target=(-0.265 -1.246 -10.363)
2026-10-29_11:41:55 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Leaving' from='tram_if' to='tram_sw' passenger=1 detail='leaving via reversed recorded transition'
2026-10-29_11:41:57 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Arrived' from='tram_if' to='tram_sw' passenger=0 detail='arrived at shared destination node'
2026-10-29_11:48:00 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Boarding' from='tram_if' to='tram_sw' passenger=0 detail='boarding recorded land-to-deck transition'
2026-10-29_11:48:02 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Riding' from='tram_if' to='tram_sw' passenger=1 detail='aboard; waiting for destination dock'
2026-10-29_11:48:02 INFO [server.adventurebot.navigation] DECK_WALK guid=80 points=2 length=2.00 target=(-2.101 -0.396 -10.349)
2026-10-29_11:49:13 INFO [server.adventurebot.navigation] DECK_WALK guid=80 points=2 length=2.02 target=(-0.265 -1.246 -10.363)
2026-10-29_11:49:19 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Leaving' from='tram_if' to='tram_sw' passenger=1 detail='leaving via reversed recorded transition'
2026-10-29_11:49:26 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Riding' from='tram_if' to='tram_sw' passenger=1 detail='departure during transition; retaining passenger until safe dock'
2026-10-29_11:51:35 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Leaving' from='tram_if' to='tram_sw' passenger=1 detail='leaving via reversed recorded transition'
2026-10-29_11:51:36 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=80 phase='Failed' from='tram_if' to='tram_sw' passenger=0 detail='cancelled at safe dock'
@@ -0,0 +1 @@
Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed
@@ -0,0 +1 @@
Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.
@@ -0,0 +1 @@
Moving-target flight: bounded phased corridors, obstacle hold, ceiling rejection, aerial formation hold, safe landing and return fallback passed
@@ -0,0 +1 @@
Transport dock, frame, recorder, cross-map copy and ride regressions passed
@@ -0,0 +1 @@
Production deck controller: halt budgets, departure interruption, return-before-exit, blocked paths and reservations passed
@@ -0,0 +1,3 @@
/tmp/tmp2fpgi1re/Transportship.wmo.fixture polygons=1678 reachable=259 returns=259
/tmp/tmp2fpgi1re/Subwaycar.m2.fixture polygons=105 reachable=59 returns=59
Real deck meshes: connectivity, floor isolation, edge clearance and return paths passed
@@ -0,0 +1,65 @@
# Deck navigation validation
Build and installation succeeded with the existing Debug configuration. Added
private Recast dependency; no external data extraction/reinstallation needed.
Fresh worldserver PID270232 reached ready and listens on TCP8085. Console:
/tmp/vcore-deck-start-20261029.log. Six targeted test programs passed.
Native diagnostics:
- Bravery176310:1678 polygons,259 reachable sample targets,259 return paths.
- Subway176081:105 polygons,59 reachable sample targets,59 return paths.
Controlled live ship journey, Renaisia70:
- 11:36:57 Boarding at ship_sw_br.
- 11:37:04 Riding/passenger1; DECK_WALK2m,3 points.
- Native saveall during transit persisted local position
(-1.74993,7.03908,6.14418), matching the mesh walk target.
- Native inspect simultaneously reported stopped0 on The Bravery.
- 11:38:42 DECK_WALK0.79m back to the recorded exit anchor.
- 11:38:42 Leaving;11:38:44 Arrived/passenger0 at ship_ruttheran_br.
- Temporary manual test schedule override restored to ON and acknowledged.
Noralin80 was positioned offline using native tele name at DeeprunTram and
logged in for a second live check. Her normal schedule takes tram_if to tram_sw;
no manual schedule override. Native journey completed:
11:40:44 Boarding,11:40:47 Riding/passenger1;11:41:55 mesh return1.07m to
Stormwind exit anchor, followed by11:41:57 Arrived/passenger0. Initial spreading
was skipped because of an overly conservative departure budget; adjusted the
budget to outward duration+2s, reserving return/exit time at the destination.
The updated production scheduling regression passes.
No visual acceptance claimed; native geometry, controller tests and logs are
separate evidence. Remaining user acceptance: multiple visible passengers,
clothing/body sizes, interruption close to departure and crowded boarding.
Final build3 was installed after another verified clean shutdown. Fresh console
/tmp/vcore-deck-final-20261029.log reached ready; PID270857, TCP8085 listening.
The diagnostic for subway entry176080 also confirms105 polygons/59 return paths,
sharing the same model cache as176081.
Final live subway check, Noralin80 (normal schedule, no manual override):
- Native offline tele name to DeeprunTram, followed by clientless login80.
- 11:48:00 Boarding,11:48:02 Riding/passenger1 and outward DECK_WALK2.00m.
- Native saveall persisted local(-2.1012,-0.396121,-10.3489), matching the target.
Native inspect reported stopped0 and distance10.57 from the wagon origin,
consistent with that local offset while the vehicle was travelling.
- 11:49:13 return mesh walk2.02m;11:49:19 Leaving.
- 11:49:26 vehicle departed during the recorded exit; existing safety retained
the passenger and entered Riding/abort pending a safe recorded dock.
- This final journey does NOT count as a successful full exit. Prior ship and
subway journeys completed, but the final stress run exposed exit timing under
heavy world-update stalls (CPC_PERF reported50830ms with8 ticks). Normal travel
duration reserves cannot guarantee completing a multi-step exit across such
stalls. No vehicle schedule was altered to hide this limitation.
Final source diff check passed. No schema/config changes, no commit/push.
Ship70's manual schedule override is restored; Noralin80 remains under her normal
schedule and the existing safe transport cancellation/recovery logic.
Recovery completed at11:51:36: native Leaving followed by Failed/passenger0
with 'cancelled at safe dock'. The bot was safely detached on land and normal
day-plan travel resumed. This is successful safety recovery, not an uninterrupted
journey. No manual override remains.
@@ -0,0 +1,74 @@
# Local transport deck navigation
Supported passengers: The Bravery (entry176310, display3015), Tiefenbahn subway
entries176080 through176086 (display3831). No new routes or boarding recordings
are created. Existing recorded dock transitions and native passenger ownership
remain authoritative.
The controller lazily builds and caches one model-local Recast/Detour mesh per
supported model from vmaps/Transportship.wmo.vmo and vmaps/Subwaycar.m2.vmo.
Existing collision extraction is sufficient; no world MMAP regeneration or new
external data format is required. Missing/unsupported models retain the recorded
shared stand behaviour. Cached meshes are rebuilt on process restart.
Raster cells are0.15m with0.10m vertical resolution, minimum headroom2m,
climb0.4m, slope45 degrees and erosion radius0.6m. Model-local XYZ is converted
to Detour YZX; extracted M2 triangle winding is reversed, like the map generator.
Queries reject incomplete corridors, distant floor projections and truncated
results. Polygon crossings preserve obstacle corners and floor geometry.
Only reachable points near the current deck level are selected; this does not
intentionally send bots on tours of other floors.
After boarding, bots may select a nearby reachable point with a validated return
path. Targets prefer spacing from reserved destinations and other nearby native
passengers. Crowding can leave bots at the original stand. Meshes are shared;
reservations and movement state are per passenger and vehicle identity.
Native splines use local transport coordinates, disable a second world-to-local
transformation, retain passenger ownership and walk at2m/s. A walk is scheduled
only during an actual pause with enough time for the outward path and a2s
reserve. The return path is validated then, but its time is reserved at the
destination, not at the departure dock. Departure, combat, death, timeout or the
last1.5s of a pause stop an active deck spline without detaching the passenger.
The current safe local position remains fixed during transit.
At a recorded destination dock, the bot finds a complete local path to the
recorded exit anchor. Return duration, exit duration and a2s reserve must fit the
remaining pause. The recorded disembarkation begins only after reaching that
anchor. If no safe path/time exists, the bot remains aboard rather than moving
through geometry. Existing ride timeout, cancellation and lost-passenger recovery
still apply. Group formation resumes only after complete disembarkation.
Read-only diagnostics (may populate the mesh cache):
```
clientless transport deck 176310
clientless transport deck 176081
```
Reports polygon counts and a bounded grid of reachable test targets/return paths.
Navigation log events: DECK_MESH and DECK_WALK. A DECK_WALK event records a spline
launch, not proof that the destination was reached. Normal TRANSPORT_STATE events
and passenger position traces are needed for live acceptance.
Regression tests:
- test_transport_deck_mesh.py: actual production mesh builder against synthetic
walls, low ceilings, disconnected floors, edges, invalid inputs, and both real
collision models. Data/build directory arguments are configurable.
- test_transport_deck_control.py: production scheduling/arrival method with
deterministic passenger/docking doubles.
- test_transport.py: existing native transition regressions plus the deck-return
gate that prevents premature disembarkation.
Visual acceptance should cover multiple passengers spreading at a dock, stopping
when departure begins, retaining their positions in transit and returning before
normal disembarkation. Geometry/controller tests do not replace this acceptance.
Live validation: both meshes were generated natively and a ship journey and a
first subway journey completed. After reducing the departure-only time budget,
a second subway journey exercised initial spreading too. That final journey hit
severe world-update stalls during disembarkation: the wagon departed and native
safety kept the passenger aboard. Exit timing under that load remains a known
limitation; see the dated checkpoint for exact evidence. Fixed travel-time
reserves do not compensate for entire skipped/late transport update windows.
+3
View File
@@ -96,3 +96,6 @@ install(
DESTINATION "bin/scripts/clientless"
FILES_MATCHING PATTERN "*.lua"
)
# Local transport deck meshes are built once per supported collision model.
target_link_libraries(game PRIVATE Recast)
@@ -0,0 +1,162 @@
#include "ClientlessTransportDeckMgr.h"
#include "TransportDeckMesh.h"
#include "WorldModel.h"
#include "World.h"
#include "Player.h"
#include "GameObject.h"
#include "Creature.h"
#include "GameTime.h"
#include "MotionMaster.h"
#include "MoveSplineInit.h"
#include "Chat.h"
#include "Log.h"
#include <algorithm>
#include <cmath>
#include <unordered_map>
namespace
{
using Point=TransportDeckMesh::Point;
struct Model {bool Tried=false;TransportDeckMesh Mesh;std::string Error;};
std::unordered_map<uint32,Model> Models;
struct Passenger
{
ObjectGuid Transport;
bool Tried=false,Moving=false,Reserved=false;
Point Target{};
time_t Deadline=0;
};
std::unordered_map<uint32,Passenger> Passengers;
float Distance(Point const& a,Point const& b)
{return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}
Point Local(Unit const* player)
{auto const& p=player->m_movementInfo.transport.pos;return {p.GetPositionX(),p.GetPositionY(),p.GetPositionZ()};}
float Length(std::vector<Point> const& path)
{float length=0;for(size_t i=1;i<path.size();++i)length+=Distance(path[i-1],path[i]);return length;}
Model* Get(uint32 entry)
{
bool tram=entry>=176080 && entry<=176086;
char const* filename=entry==176310?"Transportship.wmo.vmo":tram?"Subwaycar.m2.vmo":nullptr;
if(!filename)return nullptr;
auto& model=Models[tram?3831:3015];if(model.Tried)return &model;model.Tried=true;
VMAP::WorldModel geometry;
if(!geometry.readFile(sWorld->GetDataPath()+"vmaps/"+filename))model.Error="collision model unavailable";
else
{
std::vector<VMAP::GroupModel> groups;geometry.getGroupModels(groups);
std::vector<float> xyz;std::vector<int> triangles;
for(auto& group:groups)
{
std::vector<G3D::Vector3> vertices;std::vector<VMAP::MeshTriangle> indices;VMAP::WmoLiquid* liquid=nullptr;
group.getMeshData(vertices,indices,liquid);int offset=int(xyz.size()/3);
for(auto const& v:vertices)xyz.insert(xyz.end(),{v.x,v.y,v.z});
for(auto const& t:indices)
{
// Extracted M2 collision winding is reversed by the map generator too.
triangles.insert(triangles.end(),{offset+int(t.idx0),offset+int(tram?t.idx2:t.idx1),offset+int(tram?t.idx1:t.idx2)});
}
}
model.Mesh.Build(xyz,triangles,model.Error);
}
TC_LOG_INFO("server.adventurebot.navigation","DECK_MESH entry=%u polygons=%u error='%s'",entry,model.Mesh.Polygons(),model.Error.c_str());
return &model;
}
void Launch(Player* player,Passenger& state,std::vector<Point> const& path)
{
Movement::PointsArray points;
// Already in transport coordinates; never transform these a second time.
for(auto const& p:path)points.emplace_back(p[0],p[1],p[2]);
Movement::MoveSplineInit init(player);init.DisableTransportPathTransformations();
init.MovebyPath(points);init.SetWalk(true);init.SetVelocity(2.0f);
player->GetMotionMaster()->LaunchMoveSpline(std::move(init));
state.Moving=true;state.Target=path.back();
state.Deadline=GameTime::GetGameTime()+time_t(std::ceil(Length(path)/2.0f))+5;
TC_LOG_INFO("server.adventurebot.navigation","DECK_WALK guid=%u points=%u length=%.2f target=(%.3f %.3f %.3f)",
player->GetGUID().GetCounter(),uint32(path.size()),Length(path),state.Target[0],state.Target[1],state.Target[2]);
}
}
void ClientlessTransportDeckMgr::Clear(uint32 guid){Passengers.erase(guid);}
bool ClientlessTransportDeckMgr::Update(Player* player,GameObject* object,Position const& exitAnchor,bool arriving,uint32 exitBudget)
{
auto model=Get(object->GetEntry());
if(!model || !model->Mesh.Polygons() || std::fabs(object->GetObjectScale()-1.0f)>0.001f)return true;
uint32 guid=player->GetGUID().GetCounter();auto& state=Passengers[guid];
if(state.Transport!=object->GetGUID()){state=Passenger{};state.Transport=object->GetGUID();}
TransportDockState dock;bool stopped=object->GetTransportDockState(dock) && dock.Stopped;
Point current=Local(player);
if(!stopped || !player->IsAlive() || player->IsInCombat())
{
if(state.Moving){player->StopMoving();state.Moving=false;state.Reserved=false;}
return false;
}
if(state.Moving)
{
// Every active walk retains a reserve before departure, even after a lag spike.
if(dock.RemainingPause<1500 || GameTime::GetGameTime()>state.Deadline)
{player->StopMoving();state.Moving=false;state.Reserved=false;return false;}
if(player->IsSplineEnabled())return false;
state.Moving=false;
if(Distance(current,state.Target)>0.8f){state.Reserved=false;return false;}
}
Point anchor{exitAnchor.GetPositionX(),exitAnchor.GetPositionY(),exitAnchor.GetPositionZ()};
if(arriving)
{
state.Reserved=false;
if(Distance(current,anchor)<=0.65f)return dock.RemainingPause>=exitBudget;
std::vector<Point> path;
if(!model->Mesh.Path(current,anchor,path))return false; // remain attached, never cut across the deck
uint64 budget=uint64(std::ceil(Length(path)/2.0f)*1000)+exitBudget+2000;
if(path.size()<2 || dock.RemainingPause<budget)return false;
Launch(player,state,path);return false;
}
if(state.Tried)return false;
state.Tried=true;
if(player->IsSplineEnabled())return false;
std::vector<Point> best;float bestScore=-1;
unsigned seed=guid*2654435761u;
for(unsigned i=0;i<32;++i)
{
float angle=float((seed%360)+i*137)*0.01745329252f;
float radius=2.0f+float(i%5)*1.5f;
Point candidate{current[0]+std::cos(angle)*radius,current[1]+std::sin(angle)*radius,current[2]};
std::vector<Point> path,back;
if(!model->Mesh.Path(current,candidate,path) || path.size()<2 ||
!model->Mesh.Path(path.back(),anchor,back))continue;
// This is the departure dock: stay at the reached spot during transit.
// Validate the return now, but reserve its duration at the destination.
uint64 budget=uint64(std::ceil(Length(path)/2.0f)*1000)+2000;
if(dock.RemainingPause<budget)continue;
float clearance=10;
for(auto const& pair:Passengers)if(pair.first!=guid && pair.second.Transport==state.Transport && pair.second.Reserved)
clearance=std::min(clearance,Distance(path.back(),pair.second.Target));
// Include real nearby players/NPCs aboard the same vehicle, in local coordinates.
std::list<Player*> players;player->GetPlayerListInGrid(players,25);
for(auto other:players)if(other!=player && other->GetTransport()==player->GetTransport())
clearance=std::min(clearance,Distance(path.back(),Local(other)));
std::list<Creature*> creatures;player->GetCreatureListWithEntryInGrid(creatures,0,25);
for(auto other:creatures)if(other->GetTransport()==player->GetTransport())
clearance=std::min(clearance,Distance(path.back(),Local(other)));
float score=std::min(clearance,3.0f)-Length(path)*0.02f;
if(clearance>=1.5f && score>bestScore){bestScore=score;best=std::move(path);}
}
if(!best.empty())
{Launch(player,state,best);state.Reserved=true;}
return false;
}
void ClientlessTransportDeckMgr::Diagnose(ChatHandler* handler,uint32 entry)
{
auto model=Get(entry);
if(!model){handler->SendSysMessage("Supported deck entries: 176310 (The Bravery), 176080-176086 (Subway).");return;}
Point seed=entry==176310?Point{-1.87779f,7.8222f,6.21784f}:Point{-0.112158f,-0.187065f,-10.468f};
unsigned reachable=0,returns=0;std::vector<Point> path,back;
for(int x=-10;x<=10;++x)for(int y=-10;y<=10;++y)
{
Point target{seed[0]+x,seed[1]+y,seed[2]};
if(model->Mesh.Path(seed,target,path))
{++reachable;if(model->Mesh.Path(path.back(),seed,back))++returns;}
}
handler->PSendSysMessage("Deck mesh entry=%u polygons=%u reachable=%u returnPaths=%u error='%s'",entry,
model->Mesh.Polygons(),reachable,returns,model->Error.c_str());
}
@@ -0,0 +1,15 @@
#ifndef VCORE_CLIENTLESS_TRANSPORT_DECK_MGR_H
#define VCORE_CLIENTLESS_TRANSPORT_DECK_MGR_H
#include "Define.h"
class Player;
class GameObject;
class Position;
class ChatHandler;
namespace ClientlessTransportDeckMgr
{
// Return true only when the passenger is ready for the recorded exit.
bool Update(Player* player,GameObject* object,Position const& exitAnchor,bool arriving,uint32 exitBudget);
void Clear(uint32 guid);
void Diagnose(ChatHandler* handler,uint32 entry);
}
#endif
@@ -2,6 +2,7 @@
#include "ClientlessPositioning.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessTransportDeckMgr.h"
#include "Chat.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessRouteRecorderMgr.h"
@@ -397,6 +398,7 @@ bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std:
auto const& source = _stops.at(from);
if (source.Map != player->GetMapId() || Distance(*player, source.Points.front().Pos) > 3.0f)
{ error = "Player has not reached the shared departure node."; return false; }
ClientlessTransportDeckMgr::Clear(player->GetGUID().GetCounter());
Ride ride;
ride.From = source; ride.To = _stops.at(to);
ride.RequiredPause = uint32(PathLength(source) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000.0f) + uint32(source.Points.size()) * 100 + 1500;
@@ -421,6 +423,8 @@ ClientlessTransportStatus const* ClientlessTransportMgr::GetStatus(uint32 guid)
void ClientlessTransportMgr::SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail)
{
if(phase==ClientlessTransportPhase::Arrived || phase==ClientlessTransportPhase::Failed)
ClientlessTransportDeckMgr::Clear(player->GetGUID().GetCounter());
ride.Status = { phase, detail, ride.Abort }; ride.PhaseElapsed = 0; ride.Moving = false;
TC_LOG_INFO("server.adventurebot.navigation", "TRANSPORT_STATE guid=%u phase='%s' from='%s' to='%s' passenger=%u detail='%s'",
player->GetGUID().GetCounter(), PhaseName(phase), ride.From.Node.c_str(), ride.To.Node.c_str(), player->GetTransport() ? 1u : 0u, detail);
@@ -513,6 +517,7 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
{
if(!player->GetTransport())
{
ClientlessTransportDeckMgr::Clear(player->GetGUID().GetCounter());
player->StopMoving();ride.Status.Recovering=true;ride.Status.Detail="lost passenger; returning to recorded safe departure";
ride.RecoveryMs=0;ride.RecoveryMoving=false;++ride.Recoveries;
RecoverPassenger(player,ride,diff);return;
@@ -520,9 +525,12 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
fail("transport passenger identity changed; attachment retained");return;
}
ClientlessTransportStop const* arrival = nullptr;
if (AtDock(player, object, ride.To, uint32(PathLength(ride.To) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000) + uint32(ride.To.Points.size()) * 100 + 1500)) arrival = &ride.To;
else if (ride.Abort && AtDock(player, object, ride.From, ride.RequiredPause)) arrival = &ride.From;
if (!arrival) return;
if (AtDock(player, object, ride.To, 0)) arrival = &ride.To;
else if (ride.Abort && AtDock(player, object, ride.From, 0)) arrival = &ride.From;
auto const& exit = arrival ? *arrival : ride.From;
uint32 exitBudget=uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*100+1500;
bool ready=ClientlessTransportDeckMgr::Update(player,object,exit.Points.back().Pos,arrival!=nullptr,exitBudget);
if (!arrival || !ready || !AtDock(player,object,*arrival,exitBudget)) return;
if (arrival == &ride.From) ride.To = ride.From;
ride.Point = ride.To.Points.size();
SetPhase(player, ride, Phase::Leaving, "leaving via reversed recorded transition");
@@ -632,7 +640,7 @@ void ClientlessTransportMgr::Update(uint32 diff)
{
WorldSession* session = sWorld->FindClientlessSession(itr->first);
if (!session || sWorld->GetClientlessPlayerState(itr->first) != ClientlessPlayerState::Online)
{ itr = _rides.erase(itr); continue; }
{ ClientlessTransportDeckMgr::Clear(itr->first);itr = _rides.erase(itr); continue; }
Player* player = session->GetPlayer();
if (player) UpdateRide(player, itr->second, diff);
++itr;
@@ -644,6 +652,10 @@ bool ClientlessTransportMgr::Command(ChatHandler* handler, char const* args)
std::istringstream input(args ? args : "");
std::string command; input >> command;
Player* gm = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
if (command == "deck")
{
uint32 entry=0;input>>entry;ClientlessTransportDeckMgr::Diagnose(handler,entry);return true;
}
if (command == "reload") Reload();
if (!EnsureLoaded())
{
+118
View File
@@ -0,0 +1,118 @@
#include "TransportDeckMesh.h"
#include "Recast.h"
#include "DetourNavMesh.h"
#include "DetourNavMeshBuilder.h"
#include "DetourNavMeshQuery.h"
#include <algorithm>
#include <cmath>
#include <limits>
struct TransportDeckMesh::Data
{
dtNavMesh* Mesh = nullptr;
dtNavMeshQuery* Query = nullptr;
unsigned Polygons = 0;
~Data(){dtFreeNavMeshQuery(Query);dtFreeNavMesh(Mesh);}
};
TransportDeckMesh::TransportDeckMesh() = default;
TransportDeckMesh::~TransportDeckMesh() = default;
unsigned TransportDeckMesh::Polygons() const{return _data?_data->Polygons:0;}
bool TransportDeckMesh::Build(std::vector<float> const& xyz,std::vector<int> const& triangles,std::string& error)
{
_data.reset();error.clear();
auto fail=[&](char const* why){error=why;return false;};
if(xyz.empty() || xyz.size()%3 || triangles.empty() || triangles.size()%3 || xyz.size()>300000 || triangles.size()>300000)
return fail("invalid or oversized deck geometry");
std::vector<float> vertices(xyz.size());
for(size_t i=0;i<xyz.size();i+=3)
{
for(size_t j=0;j<3;++j)if(!std::isfinite(xyz[i+j]) || std::fabs(xyz[i+j])>500)return fail("invalid model coordinate");
vertices[i]=xyz[i+1];vertices[i+1]=xyz[i+2];vertices[i+2]=xyz[i];
}
for(int index:triangles)if(index<0 || size_t(index)>=xyz.size()/3)return fail("invalid deck triangle index");
rcContext context(false);rcConfig config{};
config.cs=0.15f;config.ch=0.10f;config.walkableSlopeAngle=45;
config.walkableHeight=20;config.walkableClimb=4;config.walkableRadius=4;
config.maxEdgeLen=80;config.maxSimplificationError=0.5f;
config.minRegionArea=8;config.mergeRegionArea=20;config.maxVertsPerPoly=6;
config.detailSampleDist=0.9f;config.detailSampleMaxError=0.10f;
rcCalcBounds(vertices.data(),int(vertices.size()/3),config.bmin,config.bmax);
for(int i:{0,2}){config.bmin[i]-=1;config.bmax[i]+=1;}
config.bmin[1]-=1;config.bmax[1]+=3;
rcCalcGridSize(config.bmin,config.bmax,config.cs,&config.width,&config.height);
if(int64_t(config.width)*config.height>2000000)return fail("deck raster exceeds budget");
std::unique_ptr<rcHeightfield,decltype(&rcFreeHeightField)> solid(rcAllocHeightfield(),rcFreeHeightField);
std::unique_ptr<rcCompactHeightfield,decltype(&rcFreeCompactHeightfield)> compact(rcAllocCompactHeightfield(),rcFreeCompactHeightfield);
std::unique_ptr<rcContourSet,decltype(&rcFreeContourSet)> contours(rcAllocContourSet(),rcFreeContourSet);
std::unique_ptr<rcPolyMesh,decltype(&rcFreePolyMesh)> poly(rcAllocPolyMesh(),rcFreePolyMesh);
std::unique_ptr<rcPolyMeshDetail,decltype(&rcFreePolyMeshDetail)> detail(rcAllocPolyMeshDetail(),rcFreePolyMeshDetail);
if(!solid || !compact || !contours || !poly || !detail)return fail("deck builder allocation failed");
int count=int(triangles.size()/3);std::vector<unsigned char> areas(count,RC_NULL_AREA);
rcMarkWalkableTriangles(&context,config.walkableSlopeAngle,vertices.data(),int(vertices.size()/3),triangles.data(),count,areas.data());
if(!rcCreateHeightfield(&context,*solid,config.width,config.height,config.bmin,config.bmax,config.cs,config.ch) ||
!rcRasterizeTriangles(&context,vertices.data(),int(vertices.size()/3),triangles.data(),areas.data(),count,*solid,config.walkableClimb))
return fail("deck rasterization failed");
rcFilterLowHangingWalkableObstacles(&context,config.walkableClimb,*solid);
rcFilterLedgeSpans(&context,config.walkableHeight,config.walkableClimb,*solid);
rcFilterWalkableLowHeightSpans(&context,config.walkableHeight,*solid);
if(!rcBuildCompactHeightfield(&context,config.walkableHeight,config.walkableClimb,*solid,*compact) ||
!rcErodeWalkableArea(&context,config.walkableRadius,*compact) ||
!rcBuildDistanceField(&context,*compact) ||
!rcBuildRegions(&context,*compact,0,config.minRegionArea,config.mergeRegionArea) ||
!rcBuildContours(&context,*compact,config.maxSimplificationError,config.maxEdgeLen,*contours) ||
!rcBuildPolyMesh(&context,*contours,config.maxVertsPerPoly,*poly) || !poly->npolys ||
!rcBuildPolyMeshDetail(&context,*poly,*compact,config.detailSampleDist,config.detailSampleMaxError,*detail))
return fail("no usable deck navigation mesh");
for(int i=0;i<poly->npolys;++i){poly->areas[i]=0;poly->flags[i]=1;}
dtNavMeshCreateParams params{};
params.verts=poly->verts;params.vertCount=poly->nverts;params.polys=poly->polys;
params.polyAreas=poly->areas;params.polyFlags=poly->flags;params.polyCount=poly->npolys;params.nvp=poly->nvp;
params.detailMeshes=detail->meshes;params.detailVerts=detail->verts;params.detailVertsCount=detail->nverts;
params.detailTris=detail->tris;params.detailTriCount=detail->ntris;
std::copy(poly->bmin,poly->bmin+3,params.bmin);std::copy(poly->bmax,poly->bmax+3,params.bmax);
params.walkableHeight=2;params.walkableRadius=0.6f;params.walkableClimb=0.4f;
params.cs=config.cs;params.ch=config.ch;params.buildBvTree=true;
unsigned char* bytes=nullptr;int size=0;
if(!dtCreateNavMeshData(&params,&bytes,&size))return fail("deck Detour conversion failed");
auto data=std::make_unique<Data>();data->Mesh=dtAllocNavMesh();data->Query=dtAllocNavMeshQuery();
if(!data->Mesh || !data->Query){dtFree(bytes);return fail("deck query allocation failed");}
if(dtStatusFailed(data->Mesh->init(bytes,size,DT_TILE_FREE_DATA)))
{dtFree(bytes);return fail("deck mesh initialization failed");}
if(dtStatusFailed(data->Query->init(data->Mesh,4096)))return fail("deck query initialization failed");
data->Polygons=unsigned(poly->npolys);_data=std::move(data);return true;
}
bool TransportDeckMesh::Path(Point const& from,Point const& to,std::vector<Point>& path) const
{
path.clear();if(!_data)return false;
for(auto const& p:{from,to})for(float c:p)if(!std::isfinite(c))return false;
float a[]{from[1],from[2],from[0]},b[]{to[1],to[2],to[0]},na[3],nb[3];
float extents[]{0.65f,0.5f,0.65f};dtPolyRef start=0,end=0;dtQueryFilter filter;
filter.setIncludeFlags(1);filter.setExcludeFlags(0);
auto query=_data->Query;
if(dtStatusFailed(query->findNearestPoly(a,extents,&filter,&start,na)) || !start ||
dtStatusFailed(query->findNearestPoly(b,extents,&filter,&end,nb)) || !end)return false;
for(auto pair:{std::make_pair(a,na),std::make_pair(b,nb)})
if(std::hypot(pair.first[0]-pair.second[0],pair.first[2]-pair.second[2])>0.65f ||
std::fabs(pair.first[1]-pair.second[1])>0.5f)return false;
dtPolyRef corridor[512];int count=0;
dtStatus status=query->findPath(start,end,na,nb,&filter,corridor,&count,512);
if(dtStatusFailed(status) || dtStatusDetail(status,DT_OUT_OF_NODES) || dtStatusDetail(status,DT_BUFFER_TOO_SMALL) ||
!count || corridor[count-1]!=end)return false;
float points[1536];int n=0;
status=query->findStraightPath(na,nb,corridor,count,points,nullptr,nullptr,&n,512,DT_STRAIGHTPATH_ALL_CROSSINGS);
if(dtStatusFailed(status) || dtStatusDetail(status,DT_BUFFER_TOO_SMALL) || n<1)return false;
float length=0;
for(int i=0;i<n;++i)
{
Point p{points[3*i+2],points[3*i],points[3*i+1]};
if(!path.empty())
{
auto const& last=path.back();float d=std::sqrt(std::pow(p[0]-last[0],2)+std::pow(p[1]-last[1],2)+std::pow(p[2]-last[2],2));
length+=d;if(length>100){path.clear();return false;}
}
path.push_back(p);
}
return true;
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef VCORE_TRANSPORT_DECK_MESH_H
#define VCORE_TRANSPORT_DECK_MESH_H
#include <array>
#include <memory>
#include <string>
#include <vector>
// Model-local XYZ. No map grids, world transforms or passenger ownership here.
class TransportDeckMesh
{
public:
using Point = std::array<float,3>;
TransportDeckMesh();
~TransportDeckMesh();
TransportDeckMesh(TransportDeckMesh const&) = delete;
TransportDeckMesh& operator=(TransportDeckMesh const&) = delete;
bool Build(std::vector<float> const& xyz, std::vector<int> const& triangles, std::string& error);
bool Path(Point const& from, Point const& to, std::vector<Point>& path) const;
unsigned Polygons() const;
private:
struct Data;
std::unique_ptr<Data> _data;
};
#endif
+4 -1
View File
@@ -100,6 +100,7 @@ public:
struct Node{} node;
struct Graph{Node* FindNode(std::string const&){return &node;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct AreaAccess{bool allowed=true;bool Applies(Player*){return true;}bool AllowsNode(Player*,Node const&){return allowed;}}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);}
@@ -177,7 +178,9 @@ int main(){
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);
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.
@@ -0,0 +1,74 @@
"""Run the actual deck controller against deterministic docking/movement doubles."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessTransportDeckMgr.cpp').read_text()
a=s.index('bool ClientlessTransportDeckMgr::Update(');b=s.index('\nvoid ClientlessTransportDeckMgr::Diagnose(',a)
code=r'''
#include <array>
#include <vector>
#include <list>
#include <unordered_map>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <cassert>
#include <iostream>
using uint32=unsigned;using uint64=unsigned long long;using Point=std::array<float,3>;
using ObjectGuid=unsigned;
struct Creature;
struct Position{float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}};
struct TransportDockState{bool Stopped=true;uint32 RemainingPause=60000;};
struct GameObject{TransportDockState dock;unsigned entry=176310;unsigned GetEntry(){return entry;}unsigned GetGUID(){return 123;}float GetObjectScale(){return 1;}bool GetTransportDockState(TransportDockState& d){d=dock;return true;}}object;
struct Guid{unsigned id;unsigned GetCounter(){return id;}};
struct Player{Point current{0,0,0};unsigned id=1;bool spline=false,alive=true,combat=false;int stops=0;void* transport=&object;
Guid GetGUID(){return {id};}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsSplineEnabled(){return spline;}
void StopMoving(){++stops;spline=false;}void* GetTransport(){return transport;}void GetPlayerListInGrid(std::list<Player*>&,int){} void GetCreatureListWithEntryInGrid(std::list<Creature*>&,int,int){}};
struct Creature:Player{};
struct Passenger{unsigned Transport=0;bool Tried=false,Moving=false,Reserved=false;Point Target{};time_t Deadline=0;};
std::unordered_map<unsigned,Passenger> Passengers;
float Distance(Point const&a,Point const&b){return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}
Point Local(Player*p){return p->current;}float Length(std::vector<Point>const&path){float n=0;for(unsigned i=1;i<path.size();++i)n+=Distance(path[i-1],path[i]);return n;}
struct Mesh{bool blocked=false;unsigned Polygons(){return 50;}bool Path(Point const&a,Point const&b,std::vector<Point>&p){p={a,b};return !blocked;}};
struct Model{Mesh Mesh;}model;
Model* Get(unsigned entry){return entry==176310?&model:nullptr;}
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
int launches=0;
void Launch(Player*p,Passenger&s,std::vector<Point>const&path){++launches;p->spline=true;s.Moving=true;s.Target=path.back();s.Deadline=GameTime::now+20;}
namespace ClientlessTransportDeckMgr{bool Update(Player*,GameObject*,Position const&,bool,uint32);}
'''+s[a:b]+r'''
int main(){Player p;Position anchor;
// Insufficient halt time never starts a walk.
object.dock.RemainingPause=1000;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(!launches);
// Departure does not need time for disembarkation at this station.
Passengers.clear();object.dock.RemainingPause=4500;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(launches==1);
Passengers.clear();p.spline=false;launches=0;object.dock.RemainingPause=60000;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(launches==1&&Passengers[1].Moving);
// Departure stops the spline but keeps ownership.
object.dock.Stopped=false;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(p.stops==1&&!p.spline&&p.transport==&object);
// Arriving at the next dock starts a real return, provided it fits the halt.
p.current={4,0,0};object.dock.Stopped=true;object.dock.RemainingPause=6000;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(launches==1);
object.dock.RemainingPause=60000;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(launches==2);
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000)); // still moving
p.spline=false;p.current={0,0,0};assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
// Interrupted arrival never claims the exit anchor was reached.
p.current={4,0,0};model.Mesh.blocked=true;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(launches==2);
model.Mesh.blocked=false;p.combat=true;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));p.combat=false;
Passengers.clear();p.current={0,0,0};p.spline=false;launches=0;
ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000);Point first=Passengers[1].Target;
Player other;other.id=2;ClientlessTransportDeckMgr::Update(&other,&object,anchor,false,5000);
assert(launches==2&&Distance(first,Passengers[2].Target)>=1.5f);
// Near departure, a running walk is stopped on the deck.
object.dock.RemainingPause=1000;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(!p.spline);
object.entry=999;assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
std::cout<<"Production deck controller: halt budgets, departure interruption, return-before-exit, blocked paths and reservations passed\n";
}
'''
# Avoid a type/member name collision in the standalone double.
code=code.replace('struct Mesh{','struct FakeMesh{').replace('struct Model{Mesh Mesh;}','struct Model{FakeMesh Mesh;}')
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
@@ -0,0 +1,65 @@
"""Compile the real local mesh builder and test synthetic and extracted vehicle geometry."""
from pathlib import Path
import subprocess, tempfile, struct, argparse
root=Path(__file__).resolve().parents[2]
parser=argparse.ArgumentParser();parser.add_argument('--data',default='/home/wow_admin/wow_cata_server/data/vmaps');parser.add_argument('--build',default='/home/wow_admin/wow_cata_server/build/debug');args=parser.parse_args()
code=r'''
#include "TransportDeckMesh.h"
#include <cassert>
#include <fstream>
#include <iostream>
#include <cmath>
using P=TransportDeckMesh::Point;
void quad(std::vector<float>& v,std::vector<int>& t,float x,float y,float z,float w,float h){int i=v.size()/3;
for(auto p:{P{x,y,z},P{x+w,y,z},P{x+w,y+h,z},P{x,y+h,z}})v.insert(v.end(),p.begin(),p.end());
t.insert(t.end(),{i,i+1,i+2,i,i+2,i+3});}
void wall(std::vector<float>& v,std::vector<int>& t,float x,float y0,float y1){int i=v.size()/3;
for(auto p:{P{x,y0,-1},P{x,y1,-1},P{x,y1,4},P{x,y0,4}})v.insert(v.end(),p.begin(),p.end());
t.insert(t.end(),{i,i+1,i+2,i,i+2,i+3});}
int main(int argc,char**argv){TransportDeckMesh mesh;std::string error;std::vector<float> v;std::vector<int> t;std::vector<P> path;
quad(v,t,-5,-5,0,10,10);quad(v,t,12,-5,0,10,10);quad(v,t,-5,-5,5,10,10);
assert(mesh.Build(v,t,error));assert(mesh.Path({0,0,0},{3,3,0},path));
assert(!mesh.Path({0,0,0},{15,0,0},path)); // disconnected deck
assert(!mesh.Path({0,0,0},{0,0,5},path)); // cannot jump floors
assert(mesh.Path({0,0,0},{5,0,0},path) && path.back()[0]<4.5f); // snap stays inside eroded edge
assert(!mesh.Path({0,0,0},{6,0,0},path));
assert(!mesh.Path({0,0,0},{0,0,2},path)); // no projection to another floor
assert(!mesh.Path({NAN,0,0},{0,0,0},path));
v.clear();t.clear();quad(v,t,-8,-8,0,16,16);wall(v,t,0,-8,8);
assert(mesh.Build(v,t,error));assert(!mesh.Path({-3,0,0},{3,0,0},path)); // solid wall
v.clear();t.clear();quad(v,t,-8,-8,0,16,16);wall(v,t,0,-8,4);
assert(mesh.Build(v,t,error));assert(mesh.Path({-3,0,0},{3,0,0},path));
bool around=false;for(auto const& point:path)if(point[1]>4)around=true;assert(around); // routed around obstruction
v.clear();t.clear();quad(v,t,-5,-5,0,10,10);quad(v,t,-1,-5,1.3f,2,10);
assert(mesh.Build(v,t,error));assert(!mesh.Path({-3,0,0},{3,0,0},path)); // insufficient headroom
for(int a=1;a<argc;++a){std::ifstream f(argv[a],std::ios::binary);unsigned nv,nt;f.read((char*)&nv,4);f.read((char*)&nt,4);
v.resize(nv);t.resize(nt);f.read((char*)v.data(),nv*4);f.read((char*)t.data(),nt*4);P seed;f.read((char*)seed.data(),12);
assert(mesh.Build(v,t,error));assert(mesh.Path(seed,seed,path));int reachable=0,returns=0;float longest=0;
for(float x=-10;x<=10;x+=1)for(float y=-10;y<=10;y+=1){P p{seed[0]+x,seed[1]+y,seed[2]};
if(mesh.Path(seed,p,path)){++reachable;std::vector<P> back;if(mesh.Path(path.back(),seed,back))++returns;}}
std::cout<<argv[a]<<" polygons="<<mesh.Polygons()<<" reachable="<<reachable<<" returns="<<returns<<"\n";
assert(reachable>8 && returns==reachable);
}
assert(!mesh.Build({},t,error));std::cout<<"Real deck meshes: connectivity, floor isolation, edge clearance and return paths passed\n";
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);fixtures=[]
for name,seed,m2 in [('Transportship.wmo',(-1.87779,7.8222,6.21784),False),('Subwaycar.m2',(-.112158,-.187065,-10.468),True)]:
data=(Path(args.data)/(name+'.vmo')).read_bytes();start=0;verts=[];tris=[]
while True:
q=data.find(b'VERT',start)
if q<0:break
size,n=struct.unpack_from('<II',data,q+4)
if size!=4+n*12:start=q+4;continue
end=q+8+size
if data[end:end+4]!=b'TRIM':start=end;continue
ts,nt=struct.unpack_from('<II',data,end+4);offset=len(verts)//3
verts.extend(struct.unpack_from('<'+str(n*3)+'f',data,q+12))
for ids in struct.iter_unpack('<3I',data[end+12:end+12+nt*12]):
a,b,c=ids;tris.extend((offset+a,offset+c,offset+b) if m2 else (offset+a,offset+b,offset+c))
start=end+8+ts
f=p/(name+'.fixture');f.write_bytes(struct.pack('<II',len(verts),len(tris))+struct.pack('<'+str(len(verts))+'f',*verts)+struct.pack('<'+str(len(tris))+'i',*tris)+struct.pack('<3f',*seed));fixtures.append(str(f))
(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-O1','-I'+str(root/'src/server/game/World'),'-I'+str(root/'dep/recastnavigation/Recast/Include'),'-I'+str(root/'dep/recastnavigation/Detour/Include'),str(p/'test.cpp'),str(root/'src/server/game/World/TransportDeckMesh.cpp'),str(Path(args.build)/'dep/recastnavigation/Recast/libRecast.a'),str(Path(args.build)/'dep/recastnavigation/Detour/libDetour.a'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test'),*fixtures],check=True)