Add local deck navigation for AdventureBot transport passengers
This commit is contained in:
1 parent
49712aadfe
commit
2841ee973b
19 files changed
+693
-5
No files matched your search
@@ -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.
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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(¶ms,&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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user