diff --git a/doc/clientless/fahrstuehle-aufzeichnen.md b/doc/clientless/fahrstuehle-aufzeichnen.md new file mode 100644 index 0000000000..3a280ac492 --- /dev/null +++ b/doc/clientless/fahrstuehle-aufzeichnen.md @@ -0,0 +1,210 @@ +# Fahrstühle im TravelGraph erfassen + +Diese Anleitung beschreibt den vorhandenen Recorder. Sie verändert weder die +Fahrstuhlbewegung noch dessen Haltezeiten. Alle Aufnahmen erfolgen mit einem +normalen GM-Client im Spiel; `SPAWN_ID` und `BOT_GUID` sind zu ersetzen. + +## Technische Einordnung + +Ein geeigneter Fahrstuhl ist ein natives Transport-GameObject, üblicherweise +`GAMEOBJECT_TYPE_TRANSPORT` (Typ11). Schiffe und Zeppeline verwenden normalerweise +`GAMEOBJECT_TYPE_MO_TRANSPORT` (Typ15). Beide stellen `TransportBase` bereit; +daher werden dieselben Recorderbefehle verwendet. Ein Fahrstuhl ist dabei kein +Unit-Fahrzeug mit Sitzplätzen: Der GM läuft auf die Plattform und muss vom Server +als Passenger erkannt werden. + +Der Graph bekommt je Haltestelle einen gemeinsamen Ein-/Ausstiegsnode auf festem +Boden. Separat werden Bodenanschluss und Land-Plattform-Übergang erfasst. Die +Fahrt selbst kommt aus der nativen Transportanimation. + +```text +Bodennetz unten ↔ lift_ort_unten ↔ [Fahrstuhl] ↔ lift_ort_oben ↔ Bodennetz oben +``` + +## 1. Den richtigen Fahrstuhl bestimmen + +Mit dem GM neben der Plattform stehen: + +```text +.clientless transport near 30 +``` + +Aus der Antwort `spawn=...` notieren. Das ist die konkrete **Spawn-ID**, nicht +`entry=...` und nicht der vollständige Objekt-GUID. + +```text +.clientless transport inspect SPAWN_ID +``` + +Vor der Aufnahme muss `stopped=1` gelten. `remainingPause` ist die verbleibende +Haltezeit in Millisekunden; `4294967295` bedeutet einen zeitlich nicht begrenzten +Halt. Die Aufnahme braucht einen echten Halt und eine gültige positive Periode. +Ein sichtbarer Fahrstuhl mit `stopped=0` gilt noch nicht als sicher angedockt. + +`transport services` listet die nativen Schiffs-/Zeppelinservices aus der +`transports`-Tabelle. Für gewöhnliche Typ11-Fahrstühle stattdessen `near`/`inspect` +verwenden. + +## 2. Untere Haltestelle und Einstieg aufnehmen + +Auf **festem Boden neben** dem unteren Einstieg stehen, nicht auf der beweglichen +Plattform. Den Node so nah an der Einstiegskante setzen, dass er sicher erreichbar +bleibt, wenn der Fahrstuhl gerade oben ist. + +```text +.clientless node add lift_ort_unten elevator +``` + +Auf die nächste vollständige Ankunft des Fahrstuhls warten. Höchstens5 Yards vom +Node entfernt auf festem Boden die Aufnahme starten: + +```text +.clientless transport record start lift_ort_unten SPAWN_ID +``` + +Nun normal auf die Plattform und zu einem sicheren Platz laufen. Nicht fliegen, +springen oder teleportieren. Noch während **desselben Halts** prüfen und speichern: + +```text +.clientless transport record info +.clientless transport record stop lift_ort_mitte +``` + +Vor dem Speichern muss `aboard=1` erscheinen. `stop` speichert den Übergang und +legt den benannten Stehplatz bei Bedarf automatisch an. Eine separate +`transport stand add`-Aufnahme ist für diesen Ablauf nicht nötig. + +Nur den kurzen Weg vom Land auf die Plattform aufnehmen. **Nicht erst oben +`record stop` ausführen:** Sobald der Fahrstuhl abfährt, wird die Aufnahme ungültig. +Die gesamte Einstiegsstrecke darf höchstens60 Yards lang sein. + +## 3. Obere Haltestelle aufnehmen + +Mit demselben Fahrstuhl nach oben fahren, aussteigen und auf festem Boden den +oberen Node anlegen: + +```text +.clientless node add lift_ort_oben elevator +``` + +Am besten den nächsten vollständigen Halt abwarten, damit genügend Zeit bleibt. +Vom oberen Node wieder auf **dieselbe Plattform und denselben lokalen Stehplatz** +laufen und dort speichern: + +```text +.clientless transport record start lift_ort_oben SPAWN_ID +.clientless transport record info +.clientless transport record stop lift_ort_mitte +.clientless transport stops +``` + +Denselben Standnamen verwenden. Weil er für den Transport-Entry bereits existiert, +muss der GM beim zweiten Speichern innerhalb1,5 Yards des gespeicherten lokalen +Punkts stehen. Für eine Verbindung dürfen die lokalen Deck-Endpunkte beider +Aufnahmen höchstens3 Yards auseinanderliegen. Ein Nachbaraufzug mit demselben +Entry ist trotzdem ein anderes Fahrzeug: Beide Aufnahmen brauchen denselben Spawn. + +Die Aufnahme wird beim Aussteigen umgekehrt abgespielt. Deshalb müssen beide +Einstiegswege auch rückwärts begehbar sein. + +## 4. Fahrtkante und Bodenanschlüsse anlegen + +Für einen Fahrstuhl, der beide Richtungen bedient: + +```text +.clientless transport link lift_ort_unten lift_ort_oben elevator 100 both +``` + +`both` erzeugt die Verbindung in beiden Richtungen. Ohne `both` wird nur die +angegebene Richtung angelegt. Der Befehl prüft die vorhandenen Haltestellen und +lädt den Graphen anschließend neu. + +Die `100` ist weiterhin ein erforderlicher positiver Parameter des Linkbefehls. +Seit dem Entfernungskostenmodell bestimmt dieser alte DB-Kostenwert **nicht** +mehr die tatsächliche Routenbewertung. Diese verwendet die Länge der nativen +Transportbewegung × Multiplikator mit der Variation der jeweiligen Reise. +`elevator` verwendet aktuell denselben Multiplikator wie `tram`: +`Clientless.Travel.CostMultiplier.Tram`. Es gibt noch keinen eigenen Elevator-Faktor. + +Beide Etagen an vorhandene Bodennodes anbinden. Beispiel: auf einem bereits +vorhandenen unteren Bodennode `ort_weg_unten` stehen: + +```text +.clientless route start ort_weg_unten lift_ort_unten +``` + +Den normalen Bodenweg bis `lift_ort_unten` laufen, dann: + +```text +.clientless route stop +``` + +Die Groundaufnahme wird bidirektional gespeichert. Oben entsprechend aufnehmen. +**Keine Groundkante durch den Schacht und keine Groundaufnahme während der Fahrt.** +Vollständig geprüfte MMAP-Anschlüsse sind ebenfalls möglich; eine aufgezeichnete +Bodenanbindung macht den gewünschten Zugang ausdrücklich nachvollziehbar. + +## 5. Beide Richtungen mit einem Bot prüfen + +Einen untätigen CPC-Bot am unteren Node verwenden. Ein noch laufender Tagesauftrag +wird durch `transport travel` nicht unterbrochen; für einen isolierten Test muss +der vorherige Auftrag erst sicher beendet sein. + +```text +.clientless transport travel BOT_GUID lift_ort_oben +.clientless transport status BOT_GUID +.clientless actioninfo BOT_GUID +``` + +Der erwartete Ablauf ist `Waiting → Boarding → Riding → Leaving → Arrived`. +`Arrived` muss am oberen **Landnode** erreicht werden, nicht nur an Bord. +Anschließend dieselbe Prüfung zurück nach `lift_ort_unten` durchführen und danach +auch eine normale Weiterreise in das obere/untere Bodennetz testen. + +`travel` fordert eine Reise zum Node über den normalen Planer an; es erzwingt +nicht die Fahrstuhlkante. Für den ersten Test möglichst einen Bot ohne Freiflug +und eine Strecke ohne günstigere Alternativverbindung wählen. Am Status prüfen, +ob tatsächlich eine Transportfahrt gestartet wurde. + +## Fehlerbehebung und Grenzen + +- `spawn ... not loaded`: GM auf die richtige Map und in Sichtweite des Fahrstuhls + bringen. Bei mehreren Plattformen den konkreten Spawn erneut mit `near` prüfen. +- `aboard=0`: Der Server erkennt den GM nicht als Passenger. So lässt sich keine + gültige Fahrstuhlverbindung speichern; reine sichtbare Mitbewegung genügt nicht. +- Abfahrt vor `record stop`, falscher Stehplatz oder falscher Transport: + `record info` lesen, dann `record cancel` und beim nächsten Halt neu aufnehmen. +- `Dock already recorded`: Eine gespeicherte Aufnahme wird nicht überschrieben. + Nur die betreffende Haltestelle mit `transport forget lift_ort_unten` entfernen + und neu aufnehmen. Node und Bodenrouten bleiben dabei erhalten. +- Wurde ein Haltestellennode verschoben, dessen Einstiegsaufnahme neu erstellen. +- `both` ist kein Nachweis erfolgreicher Fahrten: Ein-/Ausstieg, beide Richtungen + und verpasste Halte am echten Fahrstuhl testen. +- Der Controller wartet auf Halte und fährt als Passenger mit. Er hat keine + allgemeine Bedienlogik für Rufknöpfe, Hebel oder Etagenwahl. Manuell zu + aktivierende Fahrstühle benötigen dafür gegebenenfalls eine zusätzliche Logik. +- Beliebige Fahrstuhlmodelle haben nicht automatisch eine freie Decknavigation. + Der gespeicherte gemeinsame Stehplatz bleibt deshalb wichtig. + +Diagnosen stehen in `adventurebot-navigation.log`. `transport reload` lädt die +Aufnahmen erneut; laufende Fahrten behalten ihre beim Start übernommenen Daten. + +Grundlagen und weitere Verkehrsmittel: [transport-tools.md](transport-tools.md). + +## Laufzeitverhalten der Bots + +Bei nativen Fahrstühlen (GameObject-Typ 11) beginnen Einstieg und Ausstieg +nur am aufgezeichneten Halt, wenn die Plattform tatsächlich steht. Der Einstieg +beginnt erst bei mindestens 500 Millisekunden Reststandzeit; der Ausstieg +benötigt keine Mindestreststandzeit. Fährt sie während eines +Übergangs wieder los, bleibt eine vorhandene Fahrgastbindung erhalten. + +Bleibt ein Bot fünf Minuten als Fahrgast hängen, bringt ihn ein nativer +Teleport zum aufgezeichneten Zielnode seiner Fahrt: oben eingestiegen führt +nach unten, unten eingestiegen nach oben. Fehlversuche oder Abbruch ändern +dieses ursprüngliche Fallbackziel nicht. Schiffe, Zeppeline und die Tiefenbahn +behalten ihre bisherigen Restzeitprüfungen und erhalten diesen Fallback nicht. + +Bei einem nach Serverneustart übernommenen Fahrgast fehlt das ursprüngliche +Einstiegsereignis. Für den Fallback wird dann die nächstgelegene aufgezeichnete +Etage als Ausgangspunkt und ihre eindeutige Gegenetage als Ziel verwendet. diff --git a/doc/clientless/transport-tools.md b/doc/clientless/transport-tools.md index 57a1045bd3..a53dd07bf2 100644 --- a/doc/clientless/transport-tools.md +++ b/doc/clientless/transport-tools.md @@ -176,10 +176,14 @@ entsprechend wichtig. Ein anderer Wagen ist eine andere Aufnahmeverbindung. .clientless transport link lift_lower lift_upper elevator 100 both ``` -Der Kostenwert ist ein positiver **Graphkostenwert**, keine automatisch mit -Groundlängen vergleichbare Sekundenzahl. Zum ersten isolierten Funktionstest -ist der genaue Wert unkritisch; konkurrierende Fernrouten benötigen später eine -gemeinsame Kostenkalibrierung. `both` ist optional und erzeugt eine bidirektionale +Der Linkbefehl verlangt weiterhin einen positiven Kostenparameter (hier100). +Seit dem Entfernungskostenmodell wird der alte DB-Wert bei der Routenplanung +jedoch durch die native Transportstrecke × Verkehrsmittel-Multiplikator und +die Variation der jeweiligen Reise ersetzt. Fahrstühle verwenden derzeit den +Tram-Multiplikator `Clientless.Travel.CostMultiplier.Tram`. + +Für den vollständigen Fahrstuhlablauf siehe +[fahrstuehle-aufzeichnen.md](fahrstuehle-aufzeichnen.md). `both` ist optional und erzeugt eine bidirektionale Kante. Es darf nur verwendet werden, wenn das reale Verkehrsmittel beide Richtungen bedient. Ohne `both` entsteht nur die angegebene Fahrtrichtung. diff --git a/src/server/game/World/ClientlessTransportDeckMgr.cpp b/src/server/game/World/ClientlessTransportDeckMgr.cpp index 228fa430e9..57b5cea9a7 100644 --- a/src/server/game/World/ClientlessTransportDeckMgr.cpp +++ b/src/server/game/World/ClientlessTransportDeckMgr.cpp @@ -85,7 +85,8 @@ bool ReturnToRecordedStand(Player* player,GameObject* object,Position const& anc auto transport=object->ToTransportBase(); if(!transport || player->GetTransport()!=transport || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || player->GetMap()!=object->GetMap() || - !object->GetTransportDockState(dock) || !dock.Stopped || uint64(dock.RemainingPause)GetTransportDockState(dock) || !dock.Stopped || + (object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT && uint64(dock.RemainingPause)100 || std::fabs(y)>100 || std::fabs(z)>100)return false; @@ -120,7 +121,8 @@ bool ClientlessTransportDeckMgr::Update(Player* player,GameObject* object,Positi Point anchor{exitAnchor.GetPositionX(),exitAnchor.GetPositionY(),exitAnchor.GetPositionZ()}; TransportDockState dock; if(!object->GetTransportDockState(dock) || !dock.Stopped)return false; - if(Distance(Local(player),anchor)<=0.65f)return dock.RemainingPause>=exitBudget; + if(Distance(Local(player),anchor)<=0.65f) + return object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT || dock.RemainingPause>=exitBudget; return ReturnToRecordedStand(player,object,exitAnchor,exitBudget); } uint32 guid=player->GetGUID().GetCounter();auto& state=Passengers[guid]; diff --git a/src/server/game/World/ClientlessTransportMgr.cpp b/src/server/game/World/ClientlessTransportMgr.cpp index 37dedaa3ac..ebbfb5ac49 100644 --- a/src/server/game/World/ClientlessTransportMgr.cpp +++ b/src/server/game/World/ClientlessTransportMgr.cpp @@ -464,6 +464,8 @@ bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std: 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; + if(auto object=FindObject(player,source);object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT) + ride.RequiredPause=500; ride.Status.Phase = ClientlessTransportPhase::Waiting; ride.Status.Detail = "waiting for recorded transport at departure dock"; player->RemoveAurasByType(SPELL_AURA_MOUNTED); @@ -494,10 +496,21 @@ bool ClientlessTransportMgr::ResumePassenger(Player* player) auto node=sClientlessTravelGraphMgr->FindNode(stop.Node); if(!object || object->ToTransportBase()!=player->GetTransport() || !node || !sAdventureAreaMgr->AllowsNode(player,*node))continue; - if(!source || stop.NodeNode)source=&stop; + if(!source || (object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ? + Distance(*player,stop.Dock)Dock) : stop.NodeNode))source=&stop; } if(!source)return false; // unknown vehicles and forbidden stops retain their owner Ride ride;ride.From=ride.To=*source;ride.Resumed=true; + // A restored passenger has no live boarding event. Use the nearest recorded + // floor as its recovery origin, only when there is one unambiguous opposite. + auto object=FindObject(player,*source); + if(object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT) + for(auto const& entry:_stops) + if(entry.first!=source->Node && SameService(*source,entry.second)) + { + if(!ride.ElevatorDestination.empty()){ride.ElevatorDestination.clear();break;} + ride.ElevatorDestination=entry.first; + } ride.Status.Phase=ClientlessTransportPhase::Riding; ride.Status.Detail="restored passenger; waiting for a permitted recorded dock"; ClientlessTransportDeckMgr::Clear(guid); @@ -549,6 +562,34 @@ void ClientlessTransportMgr::Cancel(uint32 guid) if (itr != _rides.end()) itr->second.Abort = true; } +bool ClientlessTransportMgr::RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff) +{ + if(!object || object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT || + player->GetTransport()!=object->ToTransportBase())return false; + if(ride.ElevatorDestination.empty() && ride.From.Node!=ride.To.Node) + ride.ElevatorDestination=ride.To.Node; + ride.ElevatorPassengerMs=uint32(std::min(300000,uint64(ride.ElevatorPassengerMs)+diff)); + if(ride.ElevatorPassengerMs<300000 || !player->IsAlive())return false; + auto destination=_stops.find(ride.ElevatorDestination); + if(destination==_stops.end() || destination->second.Points.empty() || + !SameService(ride.From,destination->second))return false; + auto node=sClientlessTravelGraphMgr->FindNode(destination->first); + if(!node || (sAdventureAreaMgr->Applies(player) && !sAdventureAreaMgr->AllowsNode(player,*node)))return false; + auto const& target=destination->second.Points.front().Pos; + player->StopMoving(); + // The native teleport removes the real passenger attachment and completes + // through the normal clientless teleport ACK, including same-map transfers. + if(!player->TeleportTo(destination->second.Map,target.GetPositionX(),target.GetPositionY(), + target.GetPositionZ(),target.GetOrientation()))return false; + ride.To=destination->second; + ride.Status.Recovering=false; + ride.ElevatorFallback=true; + SetPhase(player,ride,ClientlessTransportPhase::Riding,"five-minute elevator fallback to destination node"); + TC_LOG_WARN("server.adventurebot.navigation","ELEVATOR_STUCK_TELEPORT guid=%u from='%s' to='%s' passengerMs=%u", + player->GetGUID().GetCounter(),ride.From.Node.c_str(),ride.To.Node.c_str(),ride.ElevatorPassengerMs); + return true; +} + void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff) { using Phase = ClientlessTransportPhase; @@ -559,10 +600,19 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff) player->GetSession()->CompleteClientlessTeleport(); return; } + if(ride.ElevatorFallback) + { + if(!player->GetTransport() && player->GetMapId()==ride.To.Map && + Distance(*player,ride.To.Points.front().Pos)<2.5f) + SetPhase(player,ride,ride.Abort?Phase::Failed:Phase::Arrived,"elevator fallback reached destination node"); + return; + } + GameObject* object = FindObject(player, ride.From); + if(RecoverStuckElevator(player,object,ride,diff))return; if(ride.Status.Recovering){RecoverPassenger(player,ride,diff);return;} if(ride.RetryDelay){ride.RetryDelay=diff>=ride.RetryDelay?0:ride.RetryDelay-diff;return;} - GameObject* object = FindObject(player, ride.From); uint32 limit = uint32(std::min(1800000, std::max(180000, uint64(ride.From.Period) * 3 + 30000))); + if(object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT)limit=std::max(limit,330000u); auto fail = [&](char const* detail) { player->StopMoving(); @@ -607,7 +657,8 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff) return; } if (ride.Abort) { fail("transport cancelled on land"); return; } - if (!AtDock(player, object, ride.From, ride.RequiredPause)) + uint32 boardingPause=object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ? 500u : ride.RequiredPause; + if (!AtDock(player, object, ride.From, boardingPause)) { if(!ride.WaitTried && !player->GetTransport() && !player->IsInCombat()) { @@ -671,7 +722,8 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff) if (permittedArrival && AtDock(player, object, ride.To, 0)) arrival = &ride.To; else if (!ride.Resumed && 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())*std::max(100u,std::min(diff,2000u)*2)+2500; + uint32 exitBudget=object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ? 0u : + uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*std::max(100u,std::min(diff,2000u)*2)+2500; 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; @@ -681,11 +733,16 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff) if (ride.Status.Phase != Phase::Boarding && ride.Status.Phase != Phase::Leaving) return; bool boarding = ride.Status.Phase == Phase::Boarding; auto const& stop = boarding ? ride.From : ride.To; - if (!object || !AtDock(player, object, stop, 0)) + bool landedElevatorExit = !boarding && object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT && + !player->GetTransport() && ride.PointGetTransport()) { - player->StopMoving(); ride.Abort = true; + player->StopMoving(); + // A short elevator stop can end after a legitimate boarding/exit + // began. Retain the passenger and retry, without cancelling its trip. + if(!object || object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT)ride.Abort = true; SetPhase(player, ride, Phase::Riding, "departure during transition; retaining passenger until safe dock"); } else fail("transport departed during land transition"); diff --git a/src/server/game/World/ClientlessTransportMgr.h b/src/server/game/World/ClientlessTransportMgr.h index 281ab028d3..52d2c15dbc 100644 --- a/src/server/game/World/ClientlessTransportMgr.h +++ b/src/server/game/World/ClientlessTransportMgr.h @@ -83,6 +83,9 @@ private: uint32 RequiredPause = 0; uint32 PointTimeout = 0; uint32 RetryDelay = 0; + uint32 ElevatorPassengerMs = 0; + std::string ElevatorDestination; + bool ElevatorFallback = false; bool Moving = false; bool Abort = false; bool Resumed = false; @@ -96,6 +99,7 @@ private: bool Sample(Player* player, Recording& recording, bool force); bool Save(Player* player, std::string const& stand, std::string& error); void UpdateRide(Player* player, Ride& ride, uint32 diff); + bool RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff); void RecoverPassenger(Player* player, Ride& ride, uint32 diff); bool SelectResumedDock(Player* player, GameObject* object, Ride& ride); void SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail); diff --git a/tests/clientless/test_transport.py b/tests/clientless/test_transport.py index 75a43b10ed..f93b90e571 100644 --- a/tests/clientless/test_transport.py +++ b/tests/clientless/test_transport.py @@ -60,6 +60,7 @@ struct Position { float GetExactDist(Position const*p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));} }; constexpr int MOVE_RUN=0,SPELL_AURA_MOUNTED=1; +constexpr int GAMEOBJECT_TYPE_TRANSPORT=11; struct TransportDockState {uint32 Progress=0,Period=0,RemainingPause=0;bool Stopped=false;}; class Player; class GameObject; class TransportBase { @@ -73,7 +74,8 @@ world = r''' }; class GameObject:public Position { public: - TransportBase base;uint32 map=0,entry=1,spawn=1;TransportDockState state{0,30000,10000,true}; + TransportBase base;uint32 map=0,entry=1,spawn=1;int type=15;TransportDockState state{0,30000,10000,true}; + int GetGoType()const{return type;} GameObject(){base.go=this;} bool IsInWorld()const{return true;}uint32 GetMapId()const{return map;} ObjectGuid GetGUID()const{return {spawn};} @@ -87,9 +89,9 @@ class Player:public Position { public: TransportBase* transport=nullptr;uint32 map=0;GameObject* object=nullptr;Motion motion{this};bool pending=false; struct {struct {Position pos;ObjectGuid guid;uint32 time=0;void Reset(){pos={};guid={};time=0;}}transport;}m_movementInfo; - int teleports=0;bool falling=false; + int teleports=0;bool falling=false,teleportAllowed=true; bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){} - bool TeleportTo(uint32 m,float a,float b,float c,float d){++teleports;map=m;Relocate(a,b,c,d);falling=false;return true;} + bool TeleportTo(uint32 m,float a,float b,float c,float d){if(!teleportAllowed)return false;++teleports;if(transport)transport->RemovePassenger(this);map=m;Relocate(a,b,c,d);falling=false;return true;} bool IsInCombat(){return false;}bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;} bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;} bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;} @@ -135,7 +137,8 @@ methods = '\n'.join(function(source, marker) for marker in [ 'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(', 'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(', 'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(', - 'void ClientlessTransportMgr::RecoverPassenger(', 'void ClientlessTransportMgr::UpdateRide(' + 'void ClientlessTransportMgr::RecoverPassenger(', 'bool ClientlessTransportMgr::RecoverStuckElevator(', + 'void ClientlessTransportMgr::UpdateRide(' ]) stubs = r''' void PublishPassengerState(Player*){} @@ -295,7 +298,56 @@ int main(){ resumed.UpdateRide(&fallen,ride,100); assert(ride.Status.Recovering&&fallen.teleports==1&&fallen.x==0); } - std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n"; + // Elevators need 500ms to begin boarding; exiting needs only the stopped dock. + for(bool reverse:{false,true}){ + ClientlessTransportMgr lift;lift._stops["A"]=stop("A",5,0);lift._stops["B"]=stop("B",100,104); + Player rider;GameObject platform;platform.type=11;rider.object=&platform; + auto from=reverse?"B":"A",to=reverse?"A":"B"; + platform.x=reverse?100:5;rider.x=reverse?104:0;platform.state.RemainingPause=499;platform.state.Stopped=false; + assert(lift.Start(&rider,from,to,error));auto& ride=lift._rides.at(26); + run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport); + platform.state.Stopped=true;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport); + platform.state.RemainingPause=500;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport); + platform.x=50;rider.x=51;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport); + platform.x=reverse?5:100;rider.x=platform.x+1;platform.state.Stopped=false; + run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport); + platform.state.Stopped=true;platform.state.RemainingPause=1;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Arrived&&!rider.transport&&rider.x==(reverse?0:104)); + // Actual passenger time, not waiting on land, gates the five-minute rescue. + rider.x=reverse?104:0;platform.x=reverse?100:5;platform.state.RemainingPause=500;assert(lift.Start(&rider,from,to,error));run(lift,rider,lift._rides.at(26)); + auto& stuck=lift._rides.at(26);platform.x=50;rider.x=51;platform.state.Stopped=false; + stuck.ElevatorPassengerMs=0;stuck.Elapsed=0;lift.UpdateRide(&rider,stuck,299999); + assert(rider.teleports==0&&rider.transport&&stuck.ElevatorDestination==to&&!stuck.Abort); + lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==1&&!rider.transport&&rider.x==(reverse?0:104)); + lift.UpdateRide(&rider,stuck,1);assert(stuck.Status.Phase==Phase::Arrived); + lift.UpdateRide(&rider,stuck,300000);assert(rider.teleports==1); + // A cancelled/retried exit cannot change the original recovery destination. + stuck.Status.Phase=Phase::Riding;stuck.ElevatorFallback=false;stuck.Abort=true; + stuck.To=stuck.From;stuck.RetryDelay=3000;platform.base.AddPassenger(&rider); + lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==2&&rider.x==(reverse?0:104)); + } + // A ship, a missing/foreign attachment or a forbidden endpoint cannot use elevator rescue. + ClientlessTransportMgr rescue;rescue._stops["A"]=stop("A",5,0);rescue._stops["B"]=stop("B",100,104); + Player passenger;GameObject vehicle;passenger.object=&vehicle;vehicle.base.AddPassenger(&passenger); + ClientlessTransportMgr::Ride stranded;stranded.From=rescue._stops["A"];stranded.To=rescue._stops["B"]; + assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports); + vehicle.type=11;vehicle.base.RemovePassenger(&passenger); + assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!stranded.ElevatorPassengerMs); + vehicle.base.AddPassenger(&passenger);areas.denied="B"; + assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports);areas.denied.clear(); + passenger.teleportAllowed=false;assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1)&&!stranded.ElevatorFallback); + passenger.teleportAllowed=true;assert(rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1)); + // Departure during an elevator transition retains the live journey, whereas + // walking the final land segment no longer depends on the departed platform. + ClientlessTransportMgr quick;quick._stops["A"]=stop("A",5,0);quick._stops["B"]=stop("B",100,104); + Player commuter;GameObject cabin;cabin.type=11;cabin.state.Stopped=false;commuter.object=&cabin;cabin.base.AddPassenger(&commuter); + ClientlessTransportMgr::Ride interrupted;interrupted.From=quick._stops["A"];interrupted.To=quick._stops["B"]; + interrupted.Status.Phase=Phase::Boarding;interrupted.Point=2; + quick.UpdateRide(&commuter,interrupted,100); + assert(interrupted.Status.Phase==Phase::Riding&&!interrupted.Abort&&commuter.transport); + cabin.base.RemovePassenger(&commuter);commuter.x=103; + interrupted.Status.Phase=Phase::Leaving;interrupted.Point=1;interrupted.Moving=false; + run(quick,commuter,interrupted);assert(interrupted.Status.Phase==Phase::Arrived&&commuter.x==104); + std::cout<<"Transport dock, elevator pause/five-minute rescue, frame, recorder, cross-map copy and ride regressions passed\n"; } ''' with tempfile.TemporaryDirectory(prefix='vcore-transport-test-') as directory: diff --git a/tests/clientless/test_transport_deck_return.py b/tests/clientless/test_transport_deck_return.py index b16a7924b9..365856ec6e 100644 --- a/tests/clientless/test_transport_deck_return.py +++ b/tests/clientless/test_transport_deck_return.py @@ -9,6 +9,7 @@ code=r''' using uint32=unsigned;using uint64=unsigned long long; #define TC_LOG_WARN(...) ((void)0) constexpr int SMSG_MOVE_UPDATE=1; +constexpr int GAMEOBJECT_TYPE_TRANSPORT=11; struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}void Relocate(Position const&p){*this=p;}}; struct Guid{unsigned GetCounter(){return 120;}}; struct WorldPacket{explicit WorldPacket(int){}}; @@ -29,7 +30,7 @@ struct Player:Position{bool alive=true,combat=false,teleport=false;int*world=&ma }; void Transport::UpdatePassengerPosition(int*,Player*p,float x,float y,float z,float o,bool){++relocations;p->x=x;p->y=y;p->z=z;p->o=o;} struct TransportDockState{bool Stopped=true;unsigned RemainingPause=10000;}; -struct GameObject{TransportDockState dock;bool valid=true;Transport*ToTransportBase(){return valid?&transport:nullptr;}int*GetMap(){return ↦}bool GetTransportDockState(TransportDockState&d){d=dock;return true;}unsigned GetEntry(){return 176081;}}object; +struct GameObject{TransportDockState dock;bool valid=true;int type=15;int GetGoType(){return type;}Transport*ToTransportBase(){return valid?&transport:nullptr;}int*GetMap(){return ↦}bool GetTransportDockState(TransportDockState&d){d=dock;return true;}unsigned GetEntry(){return 176081;}}object; '''+s[a:b]+r''' int main(){ Player p;Position anchor{2,-3,-10,0}; @@ -45,6 +46,11 @@ int main(){ p.attached=nullptr;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));p.attached=&transport; anchor.x=101;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));anchor.x=NAN;assert(!ReturnToRecordedStand(&p,&object,anchor,5000)); assert(transport.relocations==1&&p.updates==1); // rejected operations never move or detach + // Elevators need a real stop, but no time reserve even for stand recovery. + anchor.x=2;object.type=11;object.dock.RemainingPause=1; + assert(ReturnToRecordedStand(&p,&object,anchor,5000)); + object.dock.Stopped=false;assert(!ReturnToRecordedStand(&p,&object,anchor,5000)); + assert(transport.relocations==2&&p.updates==2); } ''' with tempfile.TemporaryDirectory()as d: