Fix short-stop elevator travel and recover stranded passengers
Require 500ms remaining dwell to begin elevator boarding, allow exits at stopped docks without a dwell budget, and preserve journeys across interrupted transitions. Recover elevator passengers to their original destination after five minutes. Add regression coverage and elevator recording documentation. Verified production build and live round trips with a dedicated testbot; Mokrak also completed a natural elevator trip.
This commit is contained in:
1 parent
ae0b7513d9
commit
6f8f59ec6d
7 files changed
+353
-18
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)<uint64(exitBudget)+1000)return false;
|
||||
!object->GetTransportDockState(dock) || !dock.Stopped ||
|
||||
(object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT && uint64(dock.RemainingPause)<uint64(exitBudget)+1000))return false;
|
||||
float x=anchor.GetPositionX(),y=anchor.GetPositionY(),z=anchor.GetPositionZ(),o=anchor.GetOrientation();
|
||||
if(!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) ||
|
||||
std::fabs(x)>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];
|
||||
|
||||
@@ -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.Node<source->Node)source=&stop;
|
||||
if(!source || (object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ?
|
||||
Distance(*player,stop.Dock)<Distance(*player,source->Dock) : stop.Node<source->Node))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<uint64>(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<uint64>(1800000, std::max<uint64>(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.Point<stop.Points.size() && !stop.Points[ride.Point].OnTransport;
|
||||
if (!landedElevatorExit && (!object || !AtDock(player, object, stop, 0)))
|
||||
{
|
||||
if (player->GetTransport())
|
||||
{
|
||||
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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user