Compare commits
2
Commits
4474714c00
...
ca656403bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca656403bb | ||
|
|
8df9591c99 |
No files matched your search
@@ -0,0 +1,162 @@
|
||||
# Freie Flugreisen
|
||||
|
||||
Die Umsetzung ergänzt die Planungsgrundlage in [free-flying.md](free-flying.md).
|
||||
Freies Fliegen ist eine vorübergehend berechnete Alternative im Fernreisesystem.
|
||||
Es werden dafür weder Nodes noch Kanten in der Datenbank aufgezeichnet.
|
||||
|
||||
## Reiseplanung
|
||||
|
||||
Ein Flug kann an einem regulären Graphnode oder einem virtuellen Straßenausstieg
|
||||
beginnen und an einem regulären Node oder einer Location enden. Virtuelle
|
||||
Straßeneinstiege sind keine Flugziele. Der Abflugpunkt muss tatsächlich erreicht
|
||||
sein; ein Bot außerhalb des Graphen erhält keinen beliebigen direkten Luftstart.
|
||||
|
||||
Normale Straßenreisen haben einen Kostenvorteil. Flug wird bei genügend großem
|
||||
Vorteil berücksichtigt; bevorzugte Flugzonen senken Mindestdistanz und Kosten.
|
||||
Damit können dort auch die Anschlussstrecken zwischen Nodes und Locations fliegen.
|
||||
Die Vorgabe meint insbesondere Gebiete, deren Aufbau und gefährliche Gegner
|
||||
Bodenreisen ungeeignet machen. Die Kosten sind Planungsgewichte, keine Sekunden.
|
||||
Scheitert eine Flugoption am Boden, wird nur diese Option für die aktuelle Reise
|
||||
gesperrt und mit den übrigen Möglichkeiten erneut geplant.
|
||||
|
||||
Die Planung berücksichtigt Kartentyp, bekannte Gebietsregeln, Reitfähigkeit und
|
||||
Lizenzen. Instanzen sowie bekannte Flugverbote wie Immersangwald und Geisterlande
|
||||
werden ausgeschlossen. Dynamische Schlachtfeldregeln bleiben zur Laufzeit maßgeblich.
|
||||
Unbekannte Gebietsmetadaten erlauben lediglich einen Planungsversuch: Beim Start
|
||||
entscheiden die normalen Mountfähigkeiten und Zauberprüfungen des Servers.
|
||||
Vashj'irs besondere Unterwasserreisen sind nicht enthalten.
|
||||
|
||||
## Profil und Konfiguration
|
||||
|
||||
Im `movement`-Abschnitt des Charakterprofils:
|
||||
|
||||
```lua
|
||||
can_free_fly = true,
|
||||
flight_mounts = { [32235] = 100 }, -- Zauber-ID: Goldener Greif; Gewicht 100
|
||||
```
|
||||
|
||||
Flugmounts verwenden **gelernte Mountzauber**, im Unterschied zu den bisherigen
|
||||
Display-IDs der Bodenmounts. Die Auswahl erfolgt gewichtet nach der Routenwahl.
|
||||
Benötigt werden mindestens Expertenreiten (225), ein geeignetes gelerntes Flugtier
|
||||
und die native Flugberechtigung. Für Karten 0/1 wird die Flugmeisterlizenz (90267),
|
||||
für Nordend Kaltwetterflug (54197) berücksichtigt. Ein Profil allein erteilt keine
|
||||
Fähigkeiten. Arthorans Profil enthält den bereits bereitgestellten Greifen.
|
||||
|
||||
```ini
|
||||
Clientless.Flight.Enabled = 1
|
||||
Clientless.Flight.Clearance = 60
|
||||
Clientless.Flight.MinDistance = 600
|
||||
Clientless.Flight.PreferredZones = "210 4197"
|
||||
```
|
||||
|
||||
`Clearance` ist der nominelle Geländeabstand in Yards (begrenzt auf 20–200).
|
||||
`MinDistance` gilt außerhalb bevorzugter Zonen, dort mindestens 100 Yards;
|
||||
in bevorzugten Zonen sind Flüge ab 60 Yards planbar. Die voreingestellten
|
||||
Zonen sind Eiskrone und Tausendwinter; aktuelle Flugverbote gelten weiterhin.
|
||||
Konfigurationsänderungen können über `reload config` übernommen werden.
|
||||
|
||||
## Flugausführung
|
||||
|
||||
Nach dem normalen Mountzauber wartet die Steuerung auf die tatsächliche native
|
||||
Flugfähigkeit. Es gibt keine künstliche Flugfreigabe und keinen Zielteleport.
|
||||
Die Bewegung erfolgt in kurzen nativen Flugsplines mit Flugmountgeschwindigkeit.
|
||||
Im normalen Flug wird die nächste Spline bis zu 600 ms vor dem Ende der laufenden
|
||||
Spline ab der aktuellen Position gestartet. Dadurch entsteht zwischen den
|
||||
Abschnitten kein regulärer Halt. Der neue Korridor wird vollständig geprüft;
|
||||
Landungen und die konservative Rückkehr über die Flugspur erreichen weiterhin
|
||||
ihre Endpunkte. Die Wegstrecke zählt tatsächlich geflogene Bewegung, nicht die
|
||||
überlappenden Längen der erteilten Bewegungsaufträge. `status` zeigt mit
|
||||
`handoffs` die Anzahl der Übergaben während einer noch laufenden Spline.
|
||||
|
||||
Die Navigation prüft Gelände und Wasseroberfläche, native maximale Flughöhe,
|
||||
VMAP-Geometrie und kollidierbare dynamische GameObjects in der jeweiligen Phase.
|
||||
Mehrere versetzte Kollisionsstrahlen approximieren die Körperbreite und -höhe.
|
||||
Vorausschau und geglättete Geländehöhe verhindern das Nachfahren jeder kleinen
|
||||
Spitze; längere Absenkungen erhalten eine Verzögerung. Höhenänderungen erfolgen
|
||||
diagonal. Hindernisse können moderate seitliche Umwege oder zusätzliche Steigung
|
||||
auslösen; die maximale Flughöhe bleibt eine harte Grenze.
|
||||
|
||||
Für Landungen wird fester Boden mit freiem Luftraum gesucht. Ersatzpunkte liegen
|
||||
höchstens 20 Yards neben dem Ziel und benötigen einen kurzen vollständigen
|
||||
MMAP-Anschluss zum eigentlichen Ziel. Auf Wasser wird nicht gelandet. Nach
|
||||
bestätigtem Bodenkontakt endet die Flugsteuerung, gegebenenfalls folgt die lokale
|
||||
Bodenstrecke; danach setzt die normale Graph- beziehungsweise Intentlogik fort.
|
||||
|
||||
## Diagnosebefehle
|
||||
|
||||
In der Serverkonsole ohne führenden Punkt, ingame mit Punkt:
|
||||
|
||||
```text
|
||||
clientless flight status 26
|
||||
clientless flight probe 26 -10100 -600
|
||||
clientless flight test_node 26 northshire
|
||||
clientless flight abort 26
|
||||
```
|
||||
|
||||
`status` zeigt Zustand, Fortschritt und Fehler; `probe` prüft Gelände und
|
||||
Flughöhenbegrenzung auf der aktuellen Karte ohne Bewegung. `test_node` ist ein
|
||||
isolierter Ausführungstest zwischen einem tatsächlich erreichten regulären
|
||||
Abflugnode und einem regulären Zielnode derselben Karte. Er verlangt einen
|
||||
untätigen Bot und ersetzt keinen Test der normalen Reiseplanung.
|
||||
|
||||
`abort` bricht einen ausstehenden Mountzauber ab oder versucht in der Luft eine
|
||||
Rückkehr über die aufgezeichneten, erneut kollisionsgeprüften Flugabschnitte.
|
||||
Erst nach der Landung wird abgestiegen. Ist auch die Rückstrecke blockiert,
|
||||
stoppt der Bot gemountet und meldet den Fehler; dieser Sonderfall benötigt
|
||||
weitere Betreuung. Neue Bodenaktionen werden in der Luft abgelehnt.
|
||||
|
||||
Das Log `adventurebot-navigation.log` enthält `FLIGHT_START`, `FLIGHT_SEGMENT`,
|
||||
`FLIGHT_RETURN`, `FLIGHT_FAILED` und `FLIGHT_LANDED`.
|
||||
|
||||
## Grenzen und Prüfungen
|
||||
|
||||
Die erste Umsetzung verwendet begrenzte lokale Vorausschau und einen begrenzten
|
||||
Umweg, keinen globalen dreidimensionalen Pfadsolver. Fehlende Geometriedaten
|
||||
werden abgelehnt. Komplexe Gebäude, Schluchten und vollständig umschlossene Ziele
|
||||
können eine Flugoption scheitern lassen. Spieler und NPC-Körper bilden keine
|
||||
zusätzlichen Hindernisse über die native Weltgeometrie hinaus. Schauflüge und
|
||||
Tagesroutinen gehören weiterhin in die spätere Verhaltenssteuerung.
|
||||
|
||||
Automatisierte Prüfungen:
|
||||
|
||||
```bash
|
||||
python3 tests/clientless/test_free_flight.py
|
||||
python3 tests/clientless/test_free_flight_planner.py
|
||||
python3 tests/clientless/test_flight_policy.py
|
||||
python3 tests/clientless/test_flight_handoff.py
|
||||
```
|
||||
|
||||
Sie prüfen die produktive Geometrie und Planungslogik mit kontrollierten Daten:
|
||||
Start/Landung, geglättete Spitzen, Flughöhenlimit, seitliches/vertikales Ausweichen,
|
||||
fehlende Geometrie, hohen Zielanflug, dynamische Flugoptionen, Straßenpräferenz,
|
||||
virtuelle Ausstiege, Flugverbote und Fehlerbehandlung. Build und Servertests
|
||||
ersetzen keine abschließende visuelle Abnahme im Spiel.
|
||||
|
||||
Serverseitige Testbeobachtungen am 26.10.2026 mit Arthoran (GUID 26):
|
||||
|
||||
- Goldhain → Nordhain und zurück: natives Aufmounten, Flugsegmente und bestätigte Landung.
|
||||
- Manueller Abbruch in der Luft: Rückflug entlang der geprüften Spur und Landung am Ausgangspunkt.
|
||||
- Reguläres `travel_to_location`: Kombination bestehender Wege mit Flug ab virtuellem Straßenausstieg, Landung und `travel-location-complete`.
|
||||
- Ein nicht sicher fortsetzbarer Abflug am Node `taxi_12` führte zu Rückkehr und Neuplanung statt einer erfundenen Ankunft.
|
||||
|
||||
Das Testziel wurde nur temporär als Worldpoint angelegt. Dafür wurden keine
|
||||
Flugnodes oder Flugkanten gespeichert.
|
||||
|
||||
Der abschließende reguläre Location-Test mit dem verbesserten Anflug endete um
|
||||
16:44:19 mit `FLIGHT_LANDED` (32 Segmente, Zielabweichung 0,00) und anschließendem
|
||||
`travel-location-complete`. Der Test lief über die normale Reiseaktion und den
|
||||
virtuellen Straßenausstieg, nicht über `flight test_node`.
|
||||
|
||||
## Sichtprüfung und Abschluss
|
||||
|
||||
Beim beobachteten Flug Goldhain → Nordhain fiel zunächst ein kurzes Anhalten
|
||||
zwischen den Abschnitten auf. Die vorzeitige Spline-Übergabe behebt diese Lücke.
|
||||
Der anschließende Rückflug Nordhain → Goldhain landete am 26.10.2026 um 18:09:14
|
||||
nach 36 Abschnitten mit Zielabweichung 0,00. Das Log bestätigt Übergaben während
|
||||
noch laufender Splines. Der Nutzer hat den Flug anschließend ausdrücklich als
|
||||
sauber und flüssig bestätigt.
|
||||
|
||||
Damit ist der Entwicklungsmeilenstein Navigation und Fernreisen einschließlich
|
||||
unabhängiger Flugreisen abgenommen. Die oben genannten Grenzen der begrenzten
|
||||
Flugnavigation bleiben bestehen; weitere Strecken können schrittweise ergänzt
|
||||
und geprüft werden. Mikroaktionen folgen als eigener Entwicklungsschritt.
|
||||
@@ -0,0 +1,48 @@
|
||||
1. Freies Fliegen als eigenständige Reiseart
|
||||
|
||||
Freies Fliegen soll eine reguläre Reisemöglichkeit der AdventureBots werden, neben Laufen, Reiten und Greifenflügen. Es ist ausdrücklich von den festen Flugrouten zwischen Greifenmeistern zu unterscheiden.
|
||||
|
||||
2. Wann AdventureBots fliegen sollen
|
||||
- Grundsätzlich sollen Bots aus RP-Gründen Straßen und Reittiere bevorzugen.
|
||||
- Freies Fliegen soll bei einem deutlichen Reisevorteil infrage kommen.
|
||||
- Besonders sinnvoll ist es in Gebieten, in denen Bodenreisen ungeeignet oder extrem umständlich sind, beispielsweise Eiskrone oder Tausendwinter.
|
||||
- Zonenregeln dürfen ausdrücklich festlegen, dass Fliegen in bestimmten Gebieten bevorzugt werden soll.
|
||||
- Auch kürzere Reisen können perspektivisch durch Laufen, Reiten oder freies Fliegen zurückgelegt werden.
|
||||
3. Technische Flugnavigation
|
||||
|
||||
Freies Fliegen benötigt ein eigenes Navigationssystem und darf nicht einfach als gewöhnliche TravelGraph-Kante umgesetzt werden.
|
||||
|
||||
Wir hatten folgenden Ablauf festgelegt:
|
||||
Start / Takeoff
|
||||
|
||||
3D-Luftnavigation
|
||||
|
||||
Zielanflug / Approach
|
||||
|
||||
Landung / Landing
|
||||
|
||||
Dabei müssen Flughöhe, Hindernisse, geeignete Landepunkte und die Übergänge zwischen Boden- und Flugbewegung berücksichtigt werden.
|
||||
|
||||
4. Auswahl des Flugmounts
|
||||
|
||||
Die Entscheidung, ob geflogen wird, ist von der Auswahl des konkreten Flugmounts getrennt.
|
||||
|
||||
Wählt die Reiseplanung freies Fliegen, darf das Charakterprofil bestimmen, welches passende Flugtier verwendet wird.
|
||||
|
||||
5. RP-Flüge und Tagesroutinen
|
||||
|
||||
Wir hatten auch gelegentliche Schauflüge hochstufiger Abenteurer über Sturmwind besprochen, beispielsweise mit einer passenden Landung vor dem Auktionshaus.
|
||||
|
||||
Dabei wurde ausdrücklich festgelegt: Solche RP-Schauflüge gehören in die spätere Tagesroutine beziehungsweise Verhaltenssteuerung, nicht in das Navigationssystem selbst.
|
||||
|
||||
Das Navigationssystem übernimmt lediglich die tatsächliche Durchführung des Fluges.
|
||||
|
||||
6. Entwicklungsreihenfolge
|
||||
|
||||
Wir hatten folgende Reihenfolge akzeptiert:
|
||||
|
||||
Portale und Instanzen → Schiffe und Luftschiffe → Tiefenbahn → freie Flugreisen.
|
||||
|
||||
Freies Fliegen war damit als späterer eigenständiger Navigationsschritt vorgesehen. Eine erfolgreiche Implementierung oder ein abgeschlossener Funktionstest wurde bislang nicht bestätigt.
|
||||
|
||||
Das ist der bisher besprochene Stand, ohne zusätzliche Umsetzungsvorschläge.
|
||||
@@ -41,6 +41,7 @@ GridMap::GridMap()
|
||||
m_V9 = nullptr;
|
||||
m_V8 = nullptr;
|
||||
_minHeightPlanes = nullptr;
|
||||
_maxHeightPlanes = nullptr;
|
||||
// Liquid data
|
||||
_liquidGlobalEntry = 0;
|
||||
_liquidGlobalFlags = map_liquidHeaderTypeFlags::NoWater;
|
||||
@@ -123,6 +124,7 @@ void GridMap::unloadData()
|
||||
delete[] m_V9;
|
||||
delete[] m_V8;
|
||||
delete[] _minHeightPlanes;
|
||||
delete[] _maxHeightPlanes;
|
||||
delete[] _liquidEntry;
|
||||
delete[] _liquidFlags;
|
||||
delete[] _liquidMap;
|
||||
@@ -131,6 +133,7 @@ void GridMap::unloadData()
|
||||
m_V9 = nullptr;
|
||||
m_V8 = nullptr;
|
||||
_minHeightPlanes = nullptr;
|
||||
_maxHeightPlanes = nullptr;
|
||||
_liquidEntry = nullptr;
|
||||
_liquidFlags = nullptr;
|
||||
_liquidMap = nullptr;
|
||||
@@ -233,6 +236,13 @@ bool GridMap::loadHeightData(FILE* in, uint32 offset, uint32 /*size*/)
|
||||
{ -533.33331f, -533.33331f }
|
||||
};
|
||||
|
||||
_maxHeightPlanes = new G3D::Plane[8];
|
||||
for (uint32 quarterIndex = 0; quarterIndex < 8; ++quarterIndex)
|
||||
_maxHeightPlanes[quarterIndex] = G3D::Plane(
|
||||
G3D::Vector3(boundGridCoords[indices[quarterIndex][0]][0], boundGridCoords[indices[quarterIndex][0]][1], maxHeights[indices[quarterIndex][0]]),
|
||||
G3D::Vector3(boundGridCoords[indices[quarterIndex][1]][0], boundGridCoords[indices[quarterIndex][1]][1], maxHeights[indices[quarterIndex][1]]),
|
||||
G3D::Vector3(boundGridCoords[indices[quarterIndex][2]][0], boundGridCoords[indices[quarterIndex][2]][1], maxHeights[indices[quarterIndex][2]])
|
||||
);
|
||||
_minHeightPlanes = new G3D::Plane[8];
|
||||
for (uint32 quarterIndex = 0; quarterIndex < 8; ++quarterIndex)
|
||||
_minHeightPlanes[quarterIndex] = G3D::Plane(
|
||||
@@ -565,6 +575,36 @@ float GridMap::getMinHeight(float x, float y) const
|
||||
return ray.intersection(_minHeightPlanes[quarterIndex]).z;
|
||||
}
|
||||
|
||||
float GridMap::getMaxHeight(float x, float y) const
|
||||
{
|
||||
if (!_maxHeightPlanes)
|
||||
return INVALID_HEIGHT;
|
||||
|
||||
GridCoord gridCoord = Trinity::ComputeGridCoordSimple(x, y);
|
||||
|
||||
int32 doubleGridX = int32(std::floor(-(x - MAP_HALFSIZE) / CENTER_GRID_OFFSET));
|
||||
int32 doubleGridY = int32(std::floor(-(y - MAP_HALFSIZE) / CENTER_GRID_OFFSET));
|
||||
|
||||
float gx = x - (int32(gridCoord.x_coord) - CENTER_GRID_ID + 1) * SIZE_OF_GRIDS;
|
||||
float gy = y - (int32(gridCoord.y_coord) - CENTER_GRID_ID + 1) * SIZE_OF_GRIDS;
|
||||
|
||||
uint32 quarterIndex = 0;
|
||||
if (doubleGridY & 1)
|
||||
{
|
||||
if (doubleGridX & 1)
|
||||
quarterIndex = 4 + (gx <= gy);
|
||||
else
|
||||
quarterIndex = 2 + ((-SIZE_OF_GRIDS - gx) > gy);
|
||||
}
|
||||
else if (doubleGridX & 1)
|
||||
quarterIndex = 6 + ((-SIZE_OF_GRIDS - gx) <= gy);
|
||||
else
|
||||
quarterIndex = gx > gy;
|
||||
|
||||
G3D::Ray ray = G3D::Ray::fromOriginAndDirection(G3D::Vector3(gx, gy, 0.0f), G3D::Vector3::unitZ());
|
||||
return ray.intersection(_maxHeightPlanes[quarterIndex]).z;
|
||||
}
|
||||
|
||||
bool GridMap::isHole(int row, int col) const
|
||||
{
|
||||
if (!_holes)
|
||||
|
||||
@@ -42,6 +42,7 @@ class TC_GAME_API GridMap
|
||||
uint8* m_uint8_V8;
|
||||
};
|
||||
G3D::Plane* _minHeightPlanes;
|
||||
G3D::Plane* _maxHeightPlanes;
|
||||
// Height level data
|
||||
float _gridHeight;
|
||||
float _gridIntHeightMultiplier;
|
||||
@@ -95,6 +96,7 @@ public:
|
||||
uint16 getArea(float x, float y) const;
|
||||
inline float getHeight(float x, float y) const {return (this->*_gridGetHeight)(x, y);}
|
||||
float getMinHeight(float x, float y) const;
|
||||
float getMaxHeight(float x, float y) const;
|
||||
float getLiquidLevel(float x, float y) const;
|
||||
ZLiquidStatus GetLiquidStatus(float x, float y, float z, Optional<map_liquidHeaderTypeFlags> ReqLiquidType, LiquidData* data = 0, float collisionHeight = 2.03128f) const; // DEFAULT_COLLISION_HEIGHT in Object.h
|
||||
};
|
||||
|
||||
@@ -657,6 +657,14 @@ float TerrainInfo::GetMinHeight(PhaseShift const& phaseShift, uint32 mapId, floa
|
||||
return -500.0f;
|
||||
}
|
||||
|
||||
float TerrainInfo::GetMaxHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y)
|
||||
{
|
||||
if (GridMap const* grid = GetGrid(PhasingHandler::GetTerrainMapId(phaseShift, mapId, this, x, y), x, y))
|
||||
return grid->getMaxHeight(x, y);
|
||||
|
||||
return VMAP_INVALID_HEIGHT_VALUE;
|
||||
}
|
||||
|
||||
float TerrainInfo::GetGridHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y)
|
||||
{
|
||||
if (GridMap* gmap = GetGrid(PhasingHandler::GetTerrainMapId(phaseShift, mapId, this, x, y), x, y))
|
||||
|
||||
@@ -92,6 +92,7 @@ public:
|
||||
void GetZoneAndAreaId(PhaseShift const& phaseShift, uint32 mapId, uint32& zoneid, uint32& areaid, Position const& pos) { GetZoneAndAreaId(phaseShift, mapId, zoneid, areaid, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ()); }
|
||||
|
||||
float GetMinHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y);
|
||||
float GetMaxHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y);
|
||||
float GetGridHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y);
|
||||
float GetStaticHeight(PhaseShift const& phaseShift, uint32 mapId, float x, float y, float z, bool checkVMap = true, float maxSearchDist = DEFAULT_HEIGHT_SEARCH);
|
||||
float GetStaticHeight(PhaseShift const& phaseShift, uint32 mapId, Position const& pos, bool checkVMap = true, float maxSearchDist = DEFAULT_HEIGHT_SEARCH) { return GetStaticHeight(phaseShift, mapId, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), checkVMap, maxSearchDist); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
@@ -181,9 +182,20 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
record.TransportActive = false;
|
||||
record.FlightActive = false;
|
||||
record.FlightApproachActive = false;
|
||||
record.PortalUseActive = false;
|
||||
record.Status = {};
|
||||
record.Status.ActionName = action.Name;
|
||||
auto const* existingFlight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter());
|
||||
if (player->IsFlying() || (existingFlight &&
|
||||
(existingFlight->State==ClientlessFlightState::Mounting || existingFlight->State==ClientlessFlightState::Flying)))
|
||||
{
|
||||
record.Status.State=ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail="airborne player requires safe flight recovery before ground actions";
|
||||
record.Active=false;
|
||||
return false;
|
||||
}
|
||||
if (player->GetTransport())
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
@@ -710,7 +722,11 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
// Only genuinely local destinations may bypass the macro graph. The
|
||||
// destination anchor may intentionally be much farther from the exact
|
||||
// Location, so directRadius must stay independent from anchorRadius.
|
||||
if (directDistance <= directRadius)
|
||||
sClientlessFlightMgr->BeginPlanning();
|
||||
Position goalPosition;goalPosition.Relocate(point->X,point->Y,point->Z,point->O);
|
||||
bool const preferredFlight=sClientlessFlightMgr->Enabled(player) &&
|
||||
sClientlessFlightMgr->Preferred(player,point->MapId,goalPosition);
|
||||
if (directDistance <= directRadius && !preferredFlight)
|
||||
{
|
||||
// C8.20: do not bypass the macro graph when the apparent direct
|
||||
// distance is short but the actual local MMAP route is blocked.
|
||||
@@ -893,6 +909,44 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
step.NodeKey.c_str(), step.NodeType.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, step.EdgeReverse ? 1u : 0u, record.RecoveryCount);
|
||||
|
||||
if (step.EdgeType == "free_flight")
|
||||
{
|
||||
std::string error;
|
||||
bool validSource=record.RouteStep>0 &&
|
||||
record.Route[record.RouteStep-1].NodeKey!="virtual_ground_entry";
|
||||
if (validSource)
|
||||
{
|
||||
auto const& source=record.Route[record.RouteStep-1];
|
||||
float const distance = player->GetExactDist(source.X, source.Y, source.Z);
|
||||
if (source.MapId == player->GetMapId() && distance > 1.5f && distance <= 120.0f)
|
||||
{
|
||||
// Native taxis may finish beside their graph coordinates.
|
||||
// Physically reach the departure before asking the mount to fly.
|
||||
Position departure;
|
||||
departure.Relocate(source.X, source.Y, source.Z, source.O);
|
||||
if (!sClientlessTravelMgr->StartTravel(player, departure, true))
|
||||
return RecoverFlightLeg(player, characterGuid, record, "flight departure ground approach failed");
|
||||
record.FlightApproachActive = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.Detail = "ground approach to free-flight departure";
|
||||
return true;
|
||||
}
|
||||
validSource=source.MapId==player->GetMapId() &&
|
||||
player->GetExactDist(source.X,source.Y,source.Z)<=3.0f;
|
||||
}
|
||||
Position target;target.Relocate(step.X,step.Y,step.Z,step.O);
|
||||
if (!validSource || step.MapId!=player->GetMapId() ||
|
||||
!sClientlessFlightMgr->Start(player,target,error))
|
||||
{
|
||||
return RecoverFlightLeg(player,characterGuid,record,error.empty()?"free flight source not physically reached":error);
|
||||
}
|
||||
record.FlightActive=true;record.Active=true;
|
||||
record.Status.State=ClientlessActionExecutionState::Moving;
|
||||
record.Status.TargetName="node:"+step.NodeKey;
|
||||
record.Status.Detail="free flight mounting";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (step.EdgeType == "portal_use")
|
||||
return StartPortalUse(player, characterGuid, record);
|
||||
|
||||
@@ -1440,6 +1494,26 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::RecoverFlightLeg(Player* player,uint32 characterGuid,Record& record,std::string const& reason)
|
||||
{
|
||||
record.FlightActive=false;
|
||||
if (record.RouteStep>0 && record.RouteStep<record.Route.size())
|
||||
{
|
||||
auto const& source=record.Route[record.RouteStep-1];
|
||||
auto const& target=record.Route[record.RouteStep];
|
||||
if(source.NodeKey=="virtual_ground_exit")
|
||||
record.FailedBridgeKeys.insert("flight_exit:exit:"+std::to_string(source.EdgeDatabaseId)+":"+std::to_string(source.GroundEndIndex)+(source.EdgeReverse?"R":"F"));
|
||||
else if(target.NodeKey=="flight_location")record.FailedBridgeKeys.insert("flight_finish:"+source.NodeKey);
|
||||
else record.FailedBridgeKeys.insert("flight:"+source.NodeKey+"->"+target.NodeKey);
|
||||
}
|
||||
if(player->IsFlying())
|
||||
{
|
||||
record.Status.State=ClientlessActionExecutionState::Failed;record.Status.Detail=reason;
|
||||
record.Active=false;return false;
|
||||
}
|
||||
return RecoverTravel(player,characterGuid,record,0,"free-flight-unavailable: "+reason);
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
uint32 characterGuid, Record& record)
|
||||
{
|
||||
@@ -1881,6 +1955,46 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
uint32 characterGuid,
|
||||
Record& record)
|
||||
{
|
||||
if (record.FlightApproachActive)
|
||||
{
|
||||
auto const* approach = sClientlessTravelMgr->GetStatus(characterGuid);
|
||||
if (approach && approach->State == ClientlessTravelState::Moving)
|
||||
return;
|
||||
record.FlightApproachActive = false;
|
||||
if (!approach || approach->State != ClientlessTravelState::Arrived)
|
||||
RecoverFlightLeg(player, characterGuid, record, "flight departure ground approach did not arrive");
|
||||
else
|
||||
StartRouteStep(player, characterGuid, record);
|
||||
return;
|
||||
}
|
||||
if (record.FlightActive)
|
||||
{
|
||||
auto const* flight=sClientlessFlightMgr->GetStatus(characterGuid);
|
||||
if (flight && (flight->State==ClientlessFlightState::Mounting || flight->State==ClientlessFlightState::Flying))
|
||||
{record.Status.Detail=flight->Detail;return;}
|
||||
record.FlightActive=false;
|
||||
if (!flight || flight->State!=ClientlessFlightState::Arrived)
|
||||
{
|
||||
RecoverFlightLeg(player,characterGuid,record,flight?flight->Detail:"free flight state disappeared");
|
||||
return;
|
||||
}
|
||||
auto const& step=record.Route[record.RouteStep];
|
||||
if (player->GetExactDist(step.X,step.Y,step.Z)>1.5f)
|
||||
{
|
||||
Position target;target.Relocate(step.X,step.Y,step.Z,step.O);
|
||||
if (!sClientlessTravelMgr->StartTravel(player,target,true))
|
||||
{
|
||||
record.Status.State=ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail="safe landing reached but local target approach failed";
|
||||
record.Active=false;return;
|
||||
}
|
||||
record.Status.Detail="ground approach after confirmed landing";
|
||||
return;
|
||||
}
|
||||
++record.RouteStep;
|
||||
StartRouteStep(player,characterGuid,record);
|
||||
return;
|
||||
}
|
||||
if (record.TransportActive)
|
||||
{
|
||||
ClientlessTransportStatus const* transport = sClientlessTransportMgr->GetStatus(characterGuid);
|
||||
|
||||
@@ -60,6 +60,8 @@ private:
|
||||
|
||||
// C8.10b: active server-side taxi edge.
|
||||
bool TransportActive = false;
|
||||
bool FlightActive = false;
|
||||
bool FlightApproachActive = false;
|
||||
bool PortalUseActive = false;
|
||||
uint64 PortalUseTeleportSequence = 0;
|
||||
uint32 PortalUseElapsedMs = 0;
|
||||
@@ -106,6 +108,7 @@ private:
|
||||
bool StartFinalLocalLeg(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverFinalLocalLeg(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
bool StartRouteStep(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverFlightLeg(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
bool StartPortalUse(Player* player, uint32 characterGuid, Record& record);
|
||||
void UpdatePortalUse(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessRouteRecorderMgr.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "BattlefieldMgr.h"
|
||||
#include "Battlefield.h"
|
||||
#include "Chat.h"
|
||||
#include "Config.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "MoveSplineInit.h"
|
||||
#include "MoveSpline.h"
|
||||
#include "PhasingHandler.h"
|
||||
#include "Player.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Random.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "TerrainMgr.h"
|
||||
#include "VMapFactory.h"
|
||||
#include "IVMapManager.h"
|
||||
#include "VMapManager2.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
namespace
|
||||
{
|
||||
using namespace ClientlessFlightPath;
|
||||
Point PointOf(Position const & p)
|
||||
{
|
||||
return
|
||||
{
|
||||
p.GetPositionX(), p.GetPositionY(), p.GetPositionZ()
|
||||
};
|
||||
}
|
||||
float Infinity()
|
||||
{
|
||||
return std::numeric_limits < float >::infinity();
|
||||
}
|
||||
bool ClearRay(Player * player, Point const & a, Point const & b)
|
||||
{
|
||||
Map * map = player->GetMap();
|
||||
float x, y, z;
|
||||
return map->isInLineOfSight(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags(0)) && ! map->getObjectHitPos(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, x, y, z, 0);
|
||||
}
|
||||
char const * StateName(ClientlessFlightState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ClientlessFlightState::Mounting : return "Mounting";
|
||||
case ClientlessFlightState::Flying : return "Flying";
|
||||
case ClientlessFlightState::Arrived : return "Arrived";
|
||||
case ClientlessFlightState::Failed : return "Failed";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
uint32 FlightType(SpellInfo const * spell)
|
||||
{
|
||||
if (spell) for (auto const & effect : spell->Effects) if (effect.ApplyAuraName == SPELL_AURA_MOUNTED) return uint32(effect.MiscValueB);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ClientlessFlightMgr * ClientlessFlightMgr::instance()
|
||||
{
|
||||
static ClientlessFlightMgr mgr;
|
||||
return & mgr;
|
||||
}
|
||||
|
||||
void ClientlessFlightMgr::BeginPlanning()
|
||||
{
|
||||
_planningAreas.clear();
|
||||
_planningTerrain.clear();
|
||||
}
|
||||
|
||||
std::pair < uint32, uint32 > ClientlessFlightMgr::AreaZone(Player const * player, uint32 map, Position const & point) const
|
||||
{
|
||||
std::string key = std::to_string(map) + ":" + std::to_string(point.GetPositionX()) + ":" + std::to_string(point.GetPositionY()) + ":" + std::to_string(point.GetPositionZ());
|
||||
auto found = _planningAreas.find(key);
|
||||
if (found != _planningAreas.end()) return found->second;
|
||||
auto & terrain = _planningTerrain[map];
|
||||
if (! terrain) terrain = sTerrainMgr.LoadTerrain(map);
|
||||
uint32 zone = 0, area = 0;
|
||||
if (terrain) terrain->GetZoneAndAreaId(player->GetPhaseShift(), zone, area, map, point);
|
||||
return _planningAreas.emplace(key, std::make_pair(area, zone)).first->second;
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Enabled(Player const * player) const
|
||||
{
|
||||
auto const * profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if (! sConfigMgr->GetBoolDefault("Clientless.Flight.Enabled", true) || ! profile || ! profile->CanFreeFly || player->GetSkillValue(SKILL_RIDING) < 225) return false;
|
||||
for (auto const & mount : profile->FlightMounts) if (player->HasSpell(mount.DisplayId) && FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Preferred(Player const * player, uint32 map, Position const & position) const
|
||||
{
|
||||
std::istringstream list(sConfigMgr->GetStringDefault("Clientless.Flight.PreferredZones", "210 4197"));
|
||||
uint32 zone = AreaZone(player, map, position).second, candidate;
|
||||
while (list >> candidate) if (zone == candidate) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
float ClientlessFlightMgr::Estimate(Player const * player, uint32 map, Position const & from, Position const & to) const
|
||||
{
|
||||
auto const * entry = sMapStore.LookupEntry(map);
|
||||
if (! Enabled(player) || ! entry || entry->MapType != MAP_COMMON) return Infinity();
|
||||
if ((map == 0 || map == 1) && ! player->HasSpell(90267)) return Infinity();
|
||||
if (map == 571 && ! player->HasSpell(54197)) return Infinity();
|
||||
uint32 const area = AreaZone(player, map, from).first;
|
||||
if (auto const * data = sAreaTableStore.LookupEntry(area))
|
||||
{
|
||||
bool const dynamicRules = data->World_pvp_ID != 0 ||(map == player->GetMapId() && area == player->GetAreaId() && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS));
|
||||
if (! dynamicRules && ! data->GetMountFlags().HasFlag(AreaMountFlags::AllowFlyingMounts)) return Infinity();
|
||||
}
|
||||
// Unknown terrain permission is an estimate only; native mount capability
|
||||
// and spell checks at the actual departure are mandatory.
|
||||
bool const preferred = Preferred(player, map, from) || Preferred(player, map, to);
|
||||
float const distance = Distance(PointOf(from), PointOf(to));
|
||||
float minimum = preferred ? 60.0f : std::max(100.0f, sConfigMgr->GetFloatDefault("Clientless.Flight.MinDistance", 600));
|
||||
if (! std::isfinite(distance) || distance < minimum || distance > 20000) return Infinity();
|
||||
return preferred ? 40 + distance * 0.4f : 180 + distance * 1.5f;
|
||||
}
|
||||
|
||||
ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const
|
||||
{
|
||||
auto i = _records.find(guid);
|
||||
return i == _records.end() ? nullptr : & i->second.Status;
|
||||
}
|
||||
|
||||
void ClientlessFlightMgr::Clear(uint32 guid)
|
||||
{
|
||||
_records.erase(guid);
|
||||
}
|
||||
|
||||
Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, float y) const
|
||||
{
|
||||
Map * map = player->GetMap();
|
||||
uint32 terrain = PhasingHandler::GetTerrainMapId(player->GetPhaseShift(), map->GetId(), map->GetTerrain(), x, y);
|
||||
std::string key = std::to_string(terrain) + ":" + std::to_string(int(std::floor(x / 533.33333f))) + ":" + std::to_string(int(std::floor(y / 533.33333f)));
|
||||
auto found = record.Tiles.find(key);
|
||||
if (found == record.Tiles.end()) found = record.Tiles.emplace(key, TerrainMgr::ExistMapAndVMap(terrain, x, y)).first;
|
||||
if (! found->second) return
|
||||
{
|
||||
};
|
||||
map->LoadGrid(x, y);
|
||||
// terrain/VMAP and dynamic objects before ray queries
|
||||
float ground = map->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
|
||||
float water = map->GetWaterLevel(player->GetPhaseShift(), x, y);
|
||||
if (std::isfinite(water) && water > ground) ground = water;
|
||||
float ceiling = map->GetTerrain()->GetMaxHeight(player->GetPhaseShift(), map->GetId(), x, y);
|
||||
return
|
||||
{
|
||||
ground, ceiling, std::isfinite(ground) && std::isfinite(ceiling) && ground > INVALID_HEIGHT + 1 && ceiling > ground + 5
|
||||
};
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::string & error)
|
||||
{
|
||||
if (! Trinity::IsValidMapCoord(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
|
||||
{
|
||||
error = "invalid free-flight destination coordinates";
|
||||
return false;
|
||||
}
|
||||
if (! player || ! player->IsInWorld() || ! Enabled(player) || ! player->IsAlive() || player->GetMap()->GetEntry()->MapType != MAP_COMMON || player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported() || player->IsInCombat() || ! player->IsOutdoors() || player->IsInWater() || player->IsFalling())
|
||||
{
|
||||
error = "free flight unavailable in current player/location state";
|
||||
return false;
|
||||
}
|
||||
auto * vmaps = VMAP::VMapFactory::createOrGetVMapManager();
|
||||
if (! vmaps->isLineOfSightCalcEnabled() || ! vmaps->isHeightCalcEnabled())
|
||||
{
|
||||
error = "free flight requires native VMAP height and collision checks";
|
||||
return false;
|
||||
}
|
||||
if (auto * battlefield = sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(), player->GetZoneId())) if (! battlefield->CanFlyIn())
|
||||
{
|
||||
error = "battlefield currently forbids flying";
|
||||
return false;
|
||||
}
|
||||
auto const * profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
std::vector < std::pair < uint32, uint32 >> candidates;
|
||||
uint32 total = 0;
|
||||
for (auto const & mount : profile->FlightMounts)
|
||||
{
|
||||
if (! player->HasSpell(mount.DisplayId)) continue;
|
||||
uint32 type = FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId));
|
||||
auto const * capability = player->GetMountCapability(type);
|
||||
if (! capability || !(capability->Flags & MOUNT_CAPABILITY_FLAG_FLYING)) continue;
|
||||
uint32 weight = std::min(mount.Weight, uint32(10000));
|
||||
total += weight;
|
||||
candidates.emplace_back(mount.DisplayId, weight);
|
||||
}
|
||||
if (! total)
|
||||
{
|
||||
error = "no learned profile mount with native flying capability here";
|
||||
return false;
|
||||
}
|
||||
uint32 roll = urand(1, total), spell = 0;
|
||||
for (auto const & candidate : candidates)
|
||||
{
|
||||
spell = candidate.first;
|
||||
if (roll <= candidate.second) break;
|
||||
roll -= candidate.second;
|
||||
}
|
||||
Record record;
|
||||
record.Status.MapId = player->GetMapId();
|
||||
record.Status.Target = target;
|
||||
record.Start.Relocate(player);
|
||||
record.LastPosition.Relocate(player);
|
||||
record.Settings.Clearance = std::clamp(sConfigMgr->GetFloatDefault("Clientless.Flight.Clearance", 60), 20.0f, 200.0f);
|
||||
auto origin = Sample(player, record, player->GetPositionX(), player->GetPositionY());
|
||||
if (! origin.Valid || std::fabs(origin.Ground - player->GetPositionZ()) > 3)
|
||||
{
|
||||
error = "unknown flight bounds or no safe ground departure";
|
||||
return false;
|
||||
}
|
||||
bool foundLanding = false;
|
||||
for (float radius :
|
||||
{
|
||||
0.0f, 6.0f, 12.0f, 20.0f
|
||||
})
|
||||
{
|
||||
for (unsigned i = 0; i <(radius == 0 ? 1u : 12u); ++ i)
|
||||
{
|
||||
float angle = float(i) * 6.283185307f / 12;
|
||||
float x = target.GetPositionX() + radius * std::cos(angle), y = target.GetPositionY() + radius * std::sin(angle);
|
||||
auto landing = Sample(player, record, x, y);
|
||||
if (! landing.Valid || std::fabs(landing.Ground - target.GetPositionZ()) > 5) continue;
|
||||
float solid = player->GetMap()->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
|
||||
if (landing.Ground > solid + 0.7f) continue;
|
||||
// water is no solid touchdown
|
||||
Point bottom
|
||||
{
|
||||
x, y, landing.Ground + 1
|
||||
}, top = bottom;
|
||||
top.Z += record.Settings.Clearance;
|
||||
if (! ClearRay(player, bottom, top)) continue;
|
||||
Position candidate;
|
||||
candidate.Relocate(x, y, landing.Ground, target.GetOrientation());
|
||||
if (radius > 0 || std::fabs(landing.Ground - target.GetPositionZ()) > 0.7f)
|
||||
{
|
||||
auto path = PathGenerator::AnalyzePositions(player->GetMapId(), player->GetMapId(), player->GetInstanceId(), candidate, target, NAV_GROUND);
|
||||
if (! path.IsComplete() || path.FinalDistance > 1.5f || path.PathLength > 120) continue;
|
||||
}
|
||||
record.Status.Landing = candidate;
|
||||
foundLanding = true;
|
||||
break;
|
||||
}
|
||||
if (foundLanding) break;
|
||||
}
|
||||
if (! foundLanding)
|
||||
{
|
||||
error = "no free solid landing with a validated short target approach";
|
||||
return false;
|
||||
}
|
||||
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
|
||||
player->StopMoving();
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "free-flight-takeoff");
|
||||
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
|
||||
record.Status.State = ClientlessFlightState::Mounting;
|
||||
record.Status.MountSpell = spell;
|
||||
record.Status.Detail = "waiting for native flying mount";
|
||||
_records[player->GetGUID().GetCounter()] = std::move(record);
|
||||
if (player->CastSpell(player, spell, false) != SPELL_CAST_OK)
|
||||
{
|
||||
Fail(player, _records.at(player->GetGUID().GetCounter()), "native mount spell rejected");
|
||||
error = "native mount spell rejected";
|
||||
return false;
|
||||
}
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_START guid=%u map=%u spell=%u target=(%.2f %.2f %.2f)", player->GetGUID().GetCounter(), player->GetMapId(), spell, target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
|
||||
{
|
||||
player->StopMoving();
|
||||
record.Moving = false;
|
||||
if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
|
||||
if (! record.Returning && record.Status.State == ClientlessFlightState::Flying && player->CanFly() && player->IsAlive() && player->GetMapId() == record.Status.MapId && ! record.Breadcrumbs.empty())
|
||||
{
|
||||
record.Returning = true;
|
||||
record.AbortReason = reason;
|
||||
record.Status.Detail = "aborting: returning along checked flight trail";
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "FLIGHT_RETURN guid=%u reason='%s'", player->GetGUID().GetCounter(), reason.c_str());
|
||||
return;
|
||||
}
|
||||
record.Status.State = ClientlessFlightState::Failed;
|
||||
record.Status.Detail = reason;
|
||||
// If even the reverse corridor is blocked, remain mounted and stopped;
|
||||
// never fabricate a landing or drop the player from an unknown altitude.
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "FLIGHT_FAILED guid=%u map=%u pos=(%.2f %.2f %.2f) reason='%s'", player->GetGUID().GetCounter(), player->GetMapId(), player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), reason.c_str());
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff)
|
||||
{
|
||||
Point current = PointOf(* player), goal = PointOf(record.Returning ? record.Start : record.Status.Landing);
|
||||
auto sample =[&](float x, float y)
|
||||
{
|
||||
return Sample(player, record, x, y);
|
||||
};
|
||||
auto ray =[&](Point const & a, Point const & b)
|
||||
{
|
||||
return ClearRay(player, a, b);
|
||||
};
|
||||
if (Horizontal(current, goal) < 1.5f && std::fabs(current.Z - goal.Z) < 0.7f)
|
||||
{
|
||||
auto floor = sample(current.X, current.Y);
|
||||
if (! floor.Valid || std::fabs(current.Z - floor.Ground) > 0.7f)
|
||||
{
|
||||
Fail(player, record, "touchdown ground not confirmed");
|
||||
return false;
|
||||
}
|
||||
player->StopMoving();
|
||||
player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_ASCENDING | MOVEMENTFLAG_DESCENDING);
|
||||
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
|
||||
record.Status.State = record.Returning ? ClientlessFlightState::Failed : ClientlessFlightState::Arrived;
|
||||
record.Status.Detail = record.Returning ? "safely returned after abort: " + record.AbortReason : "landed on validated destination ground";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_LANDED guid=%u segments=%u avoidances=%u travelled=%.2f error=%.2f", player->GetGUID().GetCounter(), record.Status.Segments, record.Status.Avoidances, record.Status.Travelled, Distance(current, goal));
|
||||
return true;
|
||||
}
|
||||
ClientlessFlightPath::Result next;
|
||||
if (record.Returning)
|
||||
{
|
||||
while (! record.Breadcrumbs.empty() && Distance(current, PointOf(record.Breadcrumbs.back())) < 0.7f) record.Breadcrumbs.pop_back();
|
||||
Point back = record.Breadcrumbs.empty() ? goal : PointOf(record.Breadcrumbs.back());
|
||||
next =
|
||||
{
|
||||
back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const memory = record.Memory;
|
||||
next = Next(current, goal, record.Settings, record.Memory, sample, ray);
|
||||
// Reissuing the same endpoint would only restart its final milliseconds.
|
||||
// Let that spline finish instead; keep planning memory unchanged.
|
||||
if (handoff && next.Valid && Distance(next.Next, PointOf(record.Segment)) < 1)
|
||||
{
|
||||
record.Memory = memory;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (! next.Valid ||(! record.Returning && record.Status.Travelled > Distance(PointOf(record.Start), goal) * 2 + 500))
|
||||
{
|
||||
Fail(player, record, "no safe bounded flight corridor; holding position mounted");
|
||||
return false;
|
||||
}
|
||||
if (! record.Returning) record.Breadcrumbs.push_back(* player);
|
||||
record.Segment.Relocate(next.Next.X, next.Next.Y, next.Next.Z, player->GetOrientation());
|
||||
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
|
||||
Movement::MoveSplineInit init(player);
|
||||
init.MoveTo(next.Next.X, next.Next.Y, next.Next.Z, false);
|
||||
init.SetFly();
|
||||
init.SetWalk(false);
|
||||
init.SetVelocity(player->GetSpeed(MOVE_FLIGHT));
|
||||
if (init.Launch() <= 0)
|
||||
{
|
||||
Fail(player, record, "flight spline rejected");
|
||||
return false;
|
||||
}
|
||||
record.Moving = true;
|
||||
record.SegmentElapsed = 0;
|
||||
++ record.Status.Segments;
|
||||
record.Status.Avoidances += next.Avoiding ? 1 : 0;
|
||||
if (handoff) ++record.Status.Handoffs;
|
||||
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing ? "landing approach" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" : next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_SEGMENT guid=%u n=%u avoid=%u handoff=%u target=(%.2f %.2f %.2f) detail='%s'", player->GetGUID().GetCounter(), record.Status.Segments, next.Avoiding ? 1 : 0, handoff ? 1 : 0, next.Next.X, next.Next.Y, next.Next.Z, record.Status.Detail.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessFlightMgr::Update(uint32 diff)
|
||||
{
|
||||
for (auto &[guid, record] : _records)
|
||||
{
|
||||
if (record.Status.State == ClientlessFlightState::Arrived || record.Status.State == ClientlessFlightState::Failed) continue;
|
||||
auto * session = sWorld->FindClientlessSession(guid);
|
||||
auto * player = session ? session->GetPlayer() : nullptr;
|
||||
if (! player || ! player->IsInWorld() || sWorld->GetClientlessPlayerState(guid) != ClientlessPlayerState::Online) continue;
|
||||
record.Elapsed += std::min(diff, uint32(30000));
|
||||
record.SegmentElapsed += std::min(diff, uint32(30000));
|
||||
if (player->GetMapId() != record.Status.MapId || ! player->IsAlive() || player->IsBeingTeleported())
|
||||
{
|
||||
Fail(player, record, "flight player/map state changed");
|
||||
continue;
|
||||
}
|
||||
// Count actual motion, not overlapping issued spline lengths.
|
||||
record.Status.Travelled += Distance(PointOf(record.LastPosition), PointOf(*player));
|
||||
record.LastPosition.Relocate(player);
|
||||
if (record.Status.State == ClientlessFlightState::Mounting)
|
||||
{
|
||||
if (player->IsMounted() && player->CanFly()) record.Status.State = ClientlessFlightState::Flying;
|
||||
else
|
||||
{
|
||||
if (record.Elapsed > 8000) Fail(player, record, "native mount did not grant flight");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (! player->CanFly() || record.Elapsed >(record.Returning ? 2100000u : 1800000u))
|
||||
{
|
||||
Fail(player, record, "flight permission lost or total timeout");
|
||||
continue;
|
||||
}
|
||||
if (record.CheckTimer > diff)
|
||||
{
|
||||
record.CheckTimer -= diff;
|
||||
continue;
|
||||
}
|
||||
record.CheckTimer = 200;
|
||||
if (record.Moving && player->IsSplineEnabled())
|
||||
{
|
||||
if (record.SegmentElapsed > 10000)
|
||||
{
|
||||
Fail(player, record, "flight segment made no confirmed progress");
|
||||
continue;
|
||||
}
|
||||
auto ray =[&](Point const & a, Point const & b)
|
||||
{
|
||||
return ClearRay(player, a, b);
|
||||
};
|
||||
auto sample =[&](float x, float y)
|
||||
{
|
||||
return Sample(player, record, x, y);
|
||||
};
|
||||
// Recheck the remaining corridor as moving GO geometry changes.
|
||||
if (! Corridor(PointOf(* player), PointOf(record.Segment), PointOf(record.Returning ? record.Start : record.Status.Landing), record.Settings, sample, ray, true))
|
||||
{
|
||||
player->StopMoving();
|
||||
record.Moving = false;
|
||||
Advance(player, record);
|
||||
}
|
||||
else if (!record.Returning && player->movespline->timeRemaining() <= 600 &&
|
||||
Distance(PointOf(record.Segment), PointOf(record.Status.Landing)) > 0.1f)
|
||||
{
|
||||
// Replace a still-running spline from its native current position.
|
||||
// No StopMoving/arrival packet between ordinary flight segments.
|
||||
// Landing and conservative breadcrumb return finish their endpoint.
|
||||
Advance(player, record, true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
record.Moving = false;
|
||||
Advance(player, record);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Command(ChatHandler * handler, char const * args)
|
||||
{
|
||||
std::istringstream in(args ? args : "");
|
||||
std::string op;
|
||||
uint32 guid = 0;
|
||||
in >> op >> guid;
|
||||
auto const * status = GetStatus(guid);
|
||||
if (op == "abort")
|
||||
{
|
||||
auto * session = sWorld->FindClientlessSession(guid);
|
||||
auto * player = session ? session->GetPlayer() : nullptr;
|
||||
auto found = _records.find(guid);
|
||||
if (player && found != _records.end() &&(found->second.Status.State == ClientlessFlightState::Mounting || found->second.Status.State == ClientlessFlightState::Flying))
|
||||
{
|
||||
if (found->second.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
|
||||
Fail(player, found->second, "operator requested safe return");
|
||||
handler->SendSysMessage("Flight abort requested. An airborne player attempts the checked reverse trail before dismounting.");
|
||||
}
|
||||
else handler->SendSysMessage("No active free flight to abort.");
|
||||
return true;
|
||||
}
|
||||
if (op == "probe")
|
||||
{
|
||||
float x, y;
|
||||
auto * session = sWorld->FindClientlessSession(guid);
|
||||
auto * player = session ? session->GetPlayer() : nullptr;
|
||||
if (! player || !(in >> x >> y) || ! std::isfinite(x) || ! std::isfinite(y) || std::fabs(x) > 17000 || std::fabs(y) > 17000)
|
||||
{
|
||||
handler->SendSysMessage("Usage: .clientless flight probe <onlineGuid> <x> <y>");
|
||||
return true;
|
||||
}
|
||||
Record probe;
|
||||
auto surface = Sample(player, probe, x, y);
|
||||
handler->PSendSysMessage("Flight surface map=%u x=%.3f y=%.3f valid=%u ground=%.3f ceiling=%.3f", player->GetMapId(), x, y, surface.Valid ? 1u : 0u, surface.Ground, surface.Ceiling);
|
||||
return true;
|
||||
}
|
||||
if (op == "test_node")
|
||||
{
|
||||
std::string node;
|
||||
in >> node;
|
||||
auto * session = sWorld->FindClientlessSession(guid);
|
||||
auto * player = session ? session->GetPlayer() : nullptr;
|
||||
auto const * action = sClientlessActionExecutorMgr->GetStatus(guid);
|
||||
if (! player || ! sClientlessPlayerMgr->IsManaged(guid) || ! ClientlessRouteRecorderMgr::IsSafeKey(node) ||(action &&(action->State == ClientlessActionExecutionState::Moving || action->State == ClientlessActionExecutionState::SelectingTarget)) ||(status &&(status->State == ClientlessFlightState::Mounting || status->State == ClientlessFlightState::Flying)))
|
||||
{
|
||||
handler->SendSysMessage("Use an idle online CPC player and a valid destination node.");
|
||||
return true;
|
||||
}
|
||||
QueryResult target = WorldDatabase.PQuery("SELECT w.map,w.position_x,w.position_y,w.position_z FROM vcore_clientless_travel_node n JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key WHERE n.node_key='%s' AND n.enabled=1 AND w.enabled=1", node.c_str());
|
||||
QueryResult origin = WorldDatabase.PQuery("SELECT n.node_key FROM vcore_clientless_travel_node n JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key WHERE n.enabled=1 AND w.enabled=1 AND w.map=%u AND POW(w.position_x-%.6f,2)+POW(w.position_y-%.6f,2)+POW(w.position_z-%.6f,2)<=9 LIMIT 1", player->GetMapId(), player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
if (! target || ! origin || target->Fetch()[0].GetUInt32() != player->GetMapId())
|
||||
{
|
||||
handler->SendSysMessage("Flight test requires a real departure node within 3 yards and a destination node on the same map.");
|
||||
return true;
|
||||
}
|
||||
auto * fields = target->Fetch();
|
||||
Position point;
|
||||
point.Relocate(fields[1].GetFloat(), fields[2].GetFloat(), fields[3].GetFloat());
|
||||
std::string error;
|
||||
bool ok = Start(player, point, error);
|
||||
handler->PSendSysMessage("Isolated free-flight test: %s %s", ok ? "started" : "rejected", error.c_str());
|
||||
return true;
|
||||
}
|
||||
if (op != "status" || ! guid)
|
||||
{
|
||||
handler->SendSysMessage("Usage: .clientless flight status <guid> OR test_node <guid> <node>. test_node isolates flight execution; normal trips use the planner.");
|
||||
return false;
|
||||
}
|
||||
if (! status) handler->SendSysMessage("No free-flight state for this character.");
|
||||
else handler->PSendSysMessage("Flight GUID %u: state=%s map=%u mountSpell=%u segments=%u avoidances=%u handoffs=%u travelled=%.1f detail='%s'", guid, StateName(status->State), status->MapId, status->MountSpell, status->Segments, status->Avoidances, status->Handoffs, status->Travelled, status->Detail.c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef VCORE_CLIENTLESS_FLIGHT_MGR_H
|
||||
#define VCORE_CLIENTLESS_FLIGHT_MGR_H
|
||||
#include "Common.h"
|
||||
#include "Position.h"
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
class Player;
|
||||
class ChatHandler;
|
||||
class TerrainInfo;
|
||||
enum class ClientlessFlightState : uint8
|
||||
{
|
||||
Mounting, Flying, Arrived, Failed
|
||||
};
|
||||
struct ClientlessFlightStatus
|
||||
{
|
||||
ClientlessFlightState State = ClientlessFlightState::Failed;
|
||||
std::string Detail;
|
||||
uint32 MapId = 0, MountSpell = 0, Segments = 0, Avoidances = 0, Handoffs = 0;
|
||||
Position Target, Landing;
|
||||
float Travelled = 0;
|
||||
};
|
||||
class TC_GAME_API ClientlessFlightMgr
|
||||
{
|
||||
public : static ClientlessFlightMgr * instance();
|
||||
// Estimates are transient alternatives in macro planning, not DB edges.
|
||||
float Estimate(Player const * player, uint32 mapId, Position const & from, Position const & to) const;
|
||||
bool Preferred(Player const * player, uint32 mapId, Position const & position) const;
|
||||
bool Enabled(Player const * player) const;
|
||||
void BeginPlanning();
|
||||
bool Start(Player * player, Position const & target, std::string & error);
|
||||
void Update(uint32 diff);
|
||||
void Clear(uint32 guid);
|
||||
ClientlessFlightStatus const * GetStatus(uint32 guid) const;
|
||||
bool Command(ChatHandler * handler, char const * args);
|
||||
private : struct Record
|
||||
{
|
||||
ClientlessFlightStatus Status;
|
||||
ClientlessFlightPath::Settings Settings;
|
||||
ClientlessFlightPath::Memory Memory;
|
||||
Position Start, Segment, LastPosition;
|
||||
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
|
||||
bool Moving = false, Returning = false;
|
||||
std::string AbortReason;
|
||||
std::vector < Position > Breadcrumbs;
|
||||
std::unordered_map < std::string, bool > Tiles;
|
||||
};
|
||||
ClientlessFlightPath::Surface Sample(Player * player, Record & record, float x, float y) const;
|
||||
bool Advance(Player * player, Record & record, bool handoff = false);
|
||||
void Fail(Player * player, Record & record, std::string const & reason);
|
||||
std::pair < uint32, uint32 > AreaZone(Player const * player, uint32 map, Position const & point) const;
|
||||
mutable std::unordered_map < std::string, std::pair < uint32, uint32 >> _planningAreas;
|
||||
mutable std::unordered_map < uint32, std::shared_ptr < TerrainInfo >> _planningTerrain;
|
||||
std::unordered_map < uint32, Record > _records;
|
||||
};
|
||||
#define sClientlessFlightMgr ClientlessFlightMgr::instance()
|
||||
#endif
|
||||
@@ -0,0 +1,214 @@
|
||||
#ifndef VCORE_CLIENTLESS_FLIGHT_PATH_H
|
||||
#define VCORE_CLIENTLESS_FLIGHT_PATH_H
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
// Geometry-only receding-horizon navigation. Server adapters supply phased
|
||||
// terrain/ceiling and static+dynamic collision queries. No Player shortcuts.
|
||||
namespace ClientlessFlightPath
|
||||
{
|
||||
struct Point
|
||||
{
|
||||
float X = 0, Y = 0, Z = 0;
|
||||
};
|
||||
struct Surface
|
||||
{
|
||||
float Ground = 0, Ceiling = 0;
|
||||
bool Valid = false;
|
||||
};
|
||||
struct Settings
|
||||
{
|
||||
float Clearance = 60, Safety = 8, Radius = 2;
|
||||
float Step = 25, Lookahead = 100, Slope = 0.6f;
|
||||
};
|
||||
struct Memory
|
||||
{
|
||||
float LowerTerrainDistance = 0;
|
||||
float HeadingX = 1, HeadingY = 0;
|
||||
bool Departed = false;
|
||||
};
|
||||
struct Result
|
||||
{
|
||||
Point Next;
|
||||
bool Valid = false, Avoiding = false, Landing = false;
|
||||
};
|
||||
using Sample = std::function < Surface(float, float) >;
|
||||
using Ray = std::function < bool(Point const &, Point const &) >;
|
||||
inline float Horizontal(Point const & a, Point const & b)
|
||||
{
|
||||
return std::hypot(b.X - a.X, b.Y - a.Y);
|
||||
}
|
||||
inline float Distance(Point const & a, Point const & b)
|
||||
{
|
||||
return std::hypot(Horizontal(a, b), b.Z - a.Z);
|
||||
}
|
||||
inline Point Mix(Point const & a, Point const & b, float t)
|
||||
{
|
||||
return
|
||||
{
|
||||
a.X +(b.X - a.X) * t, a.Y +(b.Y - a.Y) * t, a.Z +(b.Z - a.Z) * t
|
||||
};
|
||||
}
|
||||
inline bool Corridor(Point const & from, Point const & to, Point const & goal, Settings const & cfg, Sample const & sample, Ray const & ray, bool departure = false)
|
||||
{
|
||||
int const count = std::max(1, int(std::ceil(Distance(from, to) / 5)));
|
||||
Surface const origin = sample(from.X, from.Y);
|
||||
if (! origin.Valid) return false;
|
||||
for (int i = 0; i <= count; ++ i)
|
||||
{
|
||||
Point p = Mix(from, to, float(i) / count);
|
||||
// Only takeoff/actual target touchdown may approach the surface. This
|
||||
// allowance follows the real rising/descending slope, not arbitrary terrain.
|
||||
float margin = cfg.Safety;
|
||||
if (departure) margin = std::min(margin, std::max(0.0f, from.Z - origin.Ground) + Horizontal(from, p) * std::min(0.3f, std::max(0.0f, to.Z - from.Z) / std::max(1.0f, Horizontal(from, to)) * 0.75f));
|
||||
margin = std::min(margin, std::max(0.0f, Horizontal(p, goal) * 0.2f));
|
||||
Surface s = sample(p.X, p.Y);
|
||||
if (! s.Valid || ! std::isfinite(s.Ground) || ! std::isfinite(s.Ceiling) || p.Z + 0.5f < s.Ground + margin || p.Z + 5 > s.Ceiling) return false;
|
||||
}
|
||||
// A volume, not one center ray. Keep the lower rays above foot level so
|
||||
// a valid ground departure/touchdown does not intersect the ground plane.
|
||||
for (auto offset : std::array < Point, 5 >
|
||||
{
|
||||
{
|
||||
{
|
||||
0, 0, 0
|
||||
},
|
||||
{
|
||||
cfg.Radius, 0, 0
|
||||
},
|
||||
{
|
||||
- cfg.Radius, 0, 0
|
||||
},
|
||||
{
|
||||
0, cfg.Radius, 0
|
||||
},
|
||||
{
|
||||
0, - cfg.Radius, 0
|
||||
}
|
||||
}
|
||||
}) for (float height :
|
||||
{
|
||||
0.8f, 4.5f
|
||||
})
|
||||
{
|
||||
Point a
|
||||
{
|
||||
from.X + offset.X, from.Y + offset.Y, from.Z + height
|
||||
};
|
||||
Point b
|
||||
{
|
||||
to.X + offset.X, to.Y + offset.Y, to.Z + height
|
||||
};
|
||||
if (! ray(a, b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
inline Result Next(Point const & from, Point const & goal, Settings const & cfg, Memory & memory, Sample const & sample, Ray const & ray)
|
||||
{
|
||||
float const distance = Horizontal(from, goal);
|
||||
Surface const here = sample(from.X, from.Y), end = sample(goal.X, goal.Y);
|
||||
if (! here.Valid || ! end.Valid || std::fabs(end.Ground - goal.Z) > 2.5f) return
|
||||
{
|
||||
};
|
||||
if (distance < 2 && std::fabs(from.Z - goal.Z) < 1.5f) return
|
||||
{
|
||||
goal, Corridor(from, goal, goal, cfg, sample, ray, true), false, true
|
||||
};
|
||||
float ux = distance > 0.1f ?(goal.X - from.X) / distance : memory.HeadingX;
|
||||
float uy = distance > 0.1f ?(goal.Y - from.Y) / distance : memory.HeadingY;
|
||||
if (distance > 0.1f)
|
||||
{
|
||||
memory.HeadingX = ux;
|
||||
memory.HeadingY = uy;
|
||||
}
|
||||
std::array < float, 5 > heights;
|
||||
float ceiling = here.Ceiling;
|
||||
for (unsigned i = 0; i < heights.size(); ++ i)
|
||||
{
|
||||
float d = std::min(distance, float(i) * cfg.Lookahead / 4);
|
||||
Surface s = sample(from.X + ux * d, from.Y + uy * d);
|
||||
if (! s.Valid) return
|
||||
{
|
||||
};
|
||||
heights[i] = s.Ground;
|
||||
ceiling = std::min(ceiling, s.Ceiling);
|
||||
}
|
||||
std::sort(heights.begin(), heights.end());
|
||||
float desired = heights[2] + cfg.Clearance;
|
||||
// ignore short radical peaks in nominal height
|
||||
bool const approach = distance < std::max(150.0f,(from.Z - goal.Z) / cfg.Slope + cfg.Step);
|
||||
if (approach) desired = goal.Z + distance * cfg.Slope * 0.65f;
|
||||
if (! approach && desired < from.Z - 10)
|
||||
{
|
||||
memory.LowerTerrainDistance += cfg.Step;
|
||||
if (memory.LowerTerrainDistance < 150) desired = from.Z;
|
||||
}
|
||||
else memory.LowerTerrainDistance = 0;
|
||||
float const horizon = std::min(cfg.Lookahead, std::max(cfg.Step, distance));
|
||||
bool const departure = ! memory.Departed;
|
||||
if (departure) desired = std::max(desired, here.Ground + 20);
|
||||
desired = std::min(desired, ceiling - 6);
|
||||
Result best;
|
||||
float bestScore = std::numeric_limits < float >::infinity();
|
||||
// Gentle nominal height tracking is separate from obstacle-climbing
|
||||
// candidates. Upward avoidance can exceed nominal clearance, never ceiling.
|
||||
for (float angle :
|
||||
{
|
||||
0.0f, 0.45f, - 0.45f, 0.9f, - 0.9f, 1.35f, - 1.35f
|
||||
}) for (float extraRise :
|
||||
{
|
||||
0.0f, - 1.0f, 25.0f, 55.0f
|
||||
})
|
||||
{
|
||||
float dx = ux * std::cos(angle) - uy * std::sin(angle);
|
||||
float dy = uy * std::cos(angle) + ux * std::sin(angle);
|
||||
float dz = std::clamp(desired - from.Z, - horizon * cfg.Slope, horizon * cfg.Slope);
|
||||
if (extraRise < 0) dz = 0;
|
||||
if (extraRise > 0) dz = std::min(horizon * cfg.Slope, std::max(0.0f, dz) + extraRise);
|
||||
float run = std::min(horizon, distance);
|
||||
// If an avoidance climb left us high near the destination, first
|
||||
// open the approach instead of descending vertically onto the goal.
|
||||
if (approach && distance < 2 *(from.Z - goal.Z) / cfg.Slope && extraRise == 0)
|
||||
{
|
||||
dz = - std::min(horizon * cfg.Slope * 0.7f, std::max(0.0f, from.Z - goal.Z));
|
||||
if (distance <(from.Z - goal.Z) / cfg.Slope * 0.7f)
|
||||
{
|
||||
dx = - dx;
|
||||
dy = - dy;
|
||||
run = horizon;
|
||||
dz = std::min(0.0f, std::max(dz, here.Ground + cfg.Safety + 4 - from.Z));
|
||||
}
|
||||
}
|
||||
Point far
|
||||
{
|
||||
from.X + dx * run, from.Y + dy * run, from.Z + dz
|
||||
};
|
||||
if (! departure && approach && angle == 0 && extraRise == 0 && distance <= horizon && std::fabs(from.Z - goal.Z) <= distance * cfg.Slope) far = goal;
|
||||
if (departure && far.Z <= from.Z + 0.5f) continue;
|
||||
if (Distance(from, far) < 0.5f ||(Horizontal(from, far) < 1 && std::fabs(far.Z - from.Z) > 2)) continue;
|
||||
if (! Corridor(from, far, goal, cfg, sample, ray, departure)) continue;
|
||||
float score = Distance(far, goal) + run + std::fabs(angle) * 20 + std::max(0.0f, extraRise) * 0.4f + std::fabs(far.Z - desired) * 0.5f;
|
||||
bool const openApproach = approach && angle == 0 && extraRise == 0;
|
||||
if (!openApproach && score >= bestScore) continue;
|
||||
float t = std::min(1.0f, cfg.Step / std::max(1.0f, Horizontal(from, far)));
|
||||
best =
|
||||
{
|
||||
Mix(from, far, t), true, angle != 0 || extraRise > 0, Distance(far, goal) < 0.1f && t == 1
|
||||
};
|
||||
bestScore = score;
|
||||
// Follow a clear nominal approach immediately. Level-flight scoring
|
||||
// must not postpone descent until directly above the destination.
|
||||
if (openApproach)
|
||||
{
|
||||
memory.Departed = true;
|
||||
return best;
|
||||
}
|
||||
}
|
||||
if (best.Valid) memory.Departed = true;
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -174,11 +174,11 @@ std::unordered_map<std::string, float> ReadFloatDictionary(lua_State* L, int tab
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movementTable)
|
||||
std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movementTable, char const* field = "ground_mounts")
|
||||
{
|
||||
std::vector<ClientlessWeightedMount> mounts;
|
||||
movementTable = lua_absindex(L, movementTable);
|
||||
lua_getfield(L, movementTable, "ground_mounts");
|
||||
lua_getfield(L, movementTable, field);
|
||||
if (!lua_istable(L, -1))
|
||||
{
|
||||
lua_pop(L, 1);
|
||||
@@ -371,6 +371,8 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
|
||||
profile.GroundMountSpeedRate = std::max(1.0f,
|
||||
ReadFloatField(_state, -1, "ground_mount_speed_rate", 1.6f));
|
||||
profile.GroundMounts = ReadMountDictionary(_state, -1);
|
||||
profile.CanFreeFly = ReadBoolField(_state, -1, "can_free_fly", false);
|
||||
profile.FlightMounts = ReadMountDictionary(_state, -1, "flight_mounts");
|
||||
}
|
||||
lua_pop(_state, 1);
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ struct TC_GAME_API ClientlessAdventureProfile
|
||||
float GroundMountMinDistance = 60.0f;
|
||||
float GroundMountSpeedRate = 1.6f;
|
||||
std::vector<ClientlessWeightedMount> GroundMounts;
|
||||
bool CanFreeFly = false;
|
||||
// Flight dictionaries use learned mount SPELL IDs, never display IDs.
|
||||
std::vector<ClientlessWeightedMount> FlightMounts;
|
||||
|
||||
// Parsed now; consumed later by the weighted intent/day planner.
|
||||
std::unordered_map<std::string, float> Personality;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include "World.h"
|
||||
@@ -149,6 +150,7 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
sClientlessPerceptionMgr->Update(diff);
|
||||
// CLIENTLESS_PLAYER_CORE_C8_4_ACTION_EXECUTOR
|
||||
sClientlessTransportMgr->Update(diff);
|
||||
sClientlessFlightMgr->Update(diff);
|
||||
sClientlessActionExecutorMgr->Update(diff);
|
||||
|
||||
for (auto itr = _records.begin(); itr != _records.end(); )
|
||||
@@ -160,6 +162,7 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
if (state == ClientlessPlayerState::Offline)
|
||||
{
|
||||
sClientlessActionExecutorMgr->Clear(characterGuid);
|
||||
sClientlessFlightMgr->Clear(characterGuid);
|
||||
itr = _records.erase(itr);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Log.h"
|
||||
@@ -509,6 +510,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
std::unordered_set<std::string> const* blockedBridgeKeys,
|
||||
ClientlessTravelLocationGoal const* locationGoal)
|
||||
{
|
||||
sClientlessFlightMgr->BeginPlanning();
|
||||
struct FlightPlanningScope
|
||||
{
|
||||
~FlightPlanningScope() { sClientlessFlightMgr->BeginPlanning(); }
|
||||
} flightPlanningScope;
|
||||
// VCORE_C819_DEFERRED_START_NODES: run the unchanged C8.18 search in two tiers.
|
||||
// A partial ordinary route must not suppress a complete route via a deferred root.
|
||||
auto runSearch = [&](bool allowDeferred) -> bool
|
||||
@@ -533,6 +539,14 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z)
|
||||
{
|
||||
if (!flightEnabled || from.MapId != map)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
|
||||
return sClientlessFlightMgr->Estimate(player,map,a,b);
|
||||
};
|
||||
if (startRadius <= 0.0f)
|
||||
startRadius = 500.0f;
|
||||
// C8.18: this cap applies ONLY to virtual node-to-node bridges.
|
||||
@@ -853,14 +867,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float LocalCost = 0.0f;
|
||||
float DistanceSq = 0.0f;
|
||||
ClientlessTravelEdgePoint Point;
|
||||
bool Flight = false;
|
||||
};
|
||||
float const locationAccessRadius = locationGoal
|
||||
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
|
||||
std::vector<ExitCandidate> rawExitCandidates;
|
||||
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
|
||||
if (locationGoal)
|
||||
if (locationGoal || flightEnabled)
|
||||
{
|
||||
float const radius = locationAccessRadius;
|
||||
auto const& targetNode = destinationItr->second;
|
||||
uint32 const goalMap = locationGoal ? locationGoal->MapId : targetNode.MapId;
|
||||
float const goalX = locationGoal ? locationGoal->X : targetNode.X;
|
||||
float const goalY = locationGoal ? locationGoal->Y : targetNode.Y;
|
||||
float const goalZ = locationGoal ? locationGoal->Z : targetNode.Z;
|
||||
float const radius = flightEnabled ? 20000.0f : locationAccessRadius;
|
||||
std::unordered_set<std::string> scannedDirected;
|
||||
for (auto const& edgePair : _edges)
|
||||
{
|
||||
@@ -899,13 +919,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float dz = ordered[i].Z - ordered[i - 1].Z;
|
||||
travelled += std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
ClientlessTravelEdgePoint const& point = ordered[i];
|
||||
if (point.MapId != locationGoal->MapId)
|
||||
if (point.MapId != goalMap)
|
||||
continue;
|
||||
dx = point.X - locationGoal->X;
|
||||
dy = point.Y - locationGoal->Y;
|
||||
dz = point.Z - locationGoal->Z;
|
||||
dx = point.X - goalX;
|
||||
dy = point.Y - goalY;
|
||||
dz = point.Z - goalZ;
|
||||
float const dsq = dx * dx + dy * dy + dz * dz;
|
||||
if (dsq > radius * radius || dsq >= nearest.DistanceSq)
|
||||
if (dsq > radius * radius)
|
||||
continue;
|
||||
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
|
||||
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
|
||||
@@ -919,7 +939,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
candidate.RoadCost = edge.Cost * (travelled / totalLength);
|
||||
candidate.DistanceSq = dsq;
|
||||
candidate.Point = point;
|
||||
candidate.LocalCost = std::sqrt(dsq);
|
||||
candidate.LocalCost = locationGoal && dsq <= locationAccessRadius*locationAccessRadius
|
||||
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
|
||||
ClientlessTravelNode exitNode;
|
||||
exitNode.MapId=point.MapId;exitNode.X=point.X;exitNode.Y=point.Y;exitNode.Z=point.Z;
|
||||
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:"+exitKey)
|
||||
? std::numeric_limits<float>::infinity() : flightCost(exitNode,goalMap,goalX,goalY,goalZ);
|
||||
if (air < candidate.LocalCost) { candidate.LocalCost=air;candidate.Flight=true; }
|
||||
if (!std::isfinite(candidate.LocalCost)) continue;
|
||||
// Never replace a node goal with an unvalidated ground exit.
|
||||
if (!locationGoal && !candidate.Flight) continue;
|
||||
if (std::isfinite(nearest.DistanceSq) &&
|
||||
candidate.RoadCost+candidate.LocalCost >= nearest.RoadCost+nearest.LocalCost) continue;
|
||||
nearest = std::move(candidate);
|
||||
}
|
||||
if (std::isfinite(nearest.DistanceSq))
|
||||
@@ -946,10 +977,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
} bestExit;
|
||||
float bestGoalCost = std::numeric_limits<float>::infinity();
|
||||
std::string bestGoalState;
|
||||
bool bestFinalFlight = false;
|
||||
// C8.20: a virtual entry is represented in the queue by its FAR
|
||||
// endpoint. Check exits BEFORE that endpoint independently: otherwise
|
||||
// an exit halfway along the road would pay for the entire remaining edge.
|
||||
if (locationGoal)
|
||||
if (locationGoal || flightEnabled)
|
||||
{
|
||||
for (auto const& root : virtualRoots)
|
||||
{
|
||||
@@ -1001,9 +1033,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (distanceItr == distance.end() || current.Cost != distanceItr->second)
|
||||
continue;
|
||||
|
||||
if (current.Cost >= bestGoalCost) break;
|
||||
if (!locationGoal && current.Value.Node == destinationNode)
|
||||
{
|
||||
bestGoalState = currentKey;
|
||||
bestGoalCost = current.Cost;
|
||||
bestExit = {};
|
||||
bestFinalFlight = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1025,18 +1061,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
// Use the same local access limit for nodes and road exits.
|
||||
// Otherwise a distant node can bypass a closer usable exit.
|
||||
if (direct <= locationAccessRadius)
|
||||
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_finish:"+candidateNode.Key)
|
||||
? std::numeric_limits<float>::infinity() : flightCost(candidateNode,locationGoal->MapId,locationGoal->X,locationGoal->Y,locationGoal->Z);
|
||||
if (direct <= locationAccessRadius || std::isfinite(air))
|
||||
{
|
||||
// Node anchors and virtual exits use the same estimate.
|
||||
// Neither is rejected because a remote tile is unloaded.
|
||||
std::string const finishKey = "finish:" + candidateNode.Key;
|
||||
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
||||
? std::numeric_limits<float>::infinity() : direct;
|
||||
? std::numeric_limits<float>::infinity() :
|
||||
std::min(direct<=locationAccessRadius ? direct : std::numeric_limits<float>::infinity(),air);
|
||||
if (current.Cost + finalCost < bestGoalCost)
|
||||
{
|
||||
bestGoalCost = current.Cost + finalCost;
|
||||
bestGoalState = currentKey;
|
||||
bestExit = {};
|
||||
bestFinalFlight = air <= finalCost;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1058,6 +1098,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
}
|
||||
|
||||
if (!locationGoal && flightEnabled)
|
||||
{
|
||||
auto exits=exitsByFrom.find(current.Value.Node);
|
||||
if (exits!=exitsByFrom.end())
|
||||
for (auto const& exit:exits->second)
|
||||
if (current.Cost+exit.RoadCost+exit.LocalCost<bestGoalCost)
|
||||
{
|
||||
bestGoalCost=current.Cost+exit.RoadCost+exit.LocalCost;
|
||||
bestGoalState=currentKey;bestExit={true,false,exit};
|
||||
}
|
||||
}
|
||||
|
||||
// VCORE_C816_PORTAL_MOUNT_GUARD: a source node is not ordinary
|
||||
// ground transit. Once reached during a planned route, its only legal
|
||||
// continuation is the portal edge itself. Physical AreaTriggers may
|
||||
@@ -1098,6 +1150,28 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic flights connect real nodes; never the synthetic entry waypoints.
|
||||
// Include them before the local-bridge guard: a flight is a separate
|
||||
// macro travel mode with its own native departure and landing checks.
|
||||
if (flightEnabled && !portalArrival)
|
||||
{
|
||||
auto const& origin=_nodes.at(current.Value.Node);
|
||||
for (auto const& pair:_nodes)
|
||||
{
|
||||
auto const& target=pair.second;
|
||||
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
||||
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
|
||||
if (!std::isfinite(cost)) continue;
|
||||
State next{target.Key,false};std::string key=stateKey(next);
|
||||
float score=current.Cost+cost;
|
||||
auto old=distance.find(key);if(old!=distance.end()&&old->second<=score)continue;
|
||||
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[currentKey]+1;
|
||||
previous[key]={currentKey,current.Value.Node,"free_flight",0,false};
|
||||
open.push({score,next});
|
||||
}
|
||||
}
|
||||
|
||||
// A local bridge is deliberately impossible immediately after another
|
||||
// local bridge. This prevents MMAP from silently becoming a long-range
|
||||
// planner while still allowing unlimited bridges over a long journey
|
||||
@@ -1319,7 +1393,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
}
|
||||
|
||||
if (locationGoal && bestExit.Active && bestExit.RootPartial &&
|
||||
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
|
||||
!bestGoalState.empty())
|
||||
previous.erase(bestGoalState);
|
||||
std::string selectedState = bestGoalState;
|
||||
@@ -1515,7 +1589,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
route.push_back(std::move(step));
|
||||
}
|
||||
|
||||
if (locationGoal && complete && bestExit.Active)
|
||||
if ((locationGoal || flightEnabled) && complete && bestExit.Active)
|
||||
{
|
||||
ExitCandidate const& exit = bestExit.Candidate;
|
||||
if (bestExit.RootPartial)
|
||||
@@ -1581,6 +1655,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
"C820_NODE_FINISH node='%s' fullCost=%.1f",
|
||||
reachedNode.c_str(), bestGoalCost);
|
||||
|
||||
if (complete && (bestExit.Active ? bestExit.Candidate.Flight : bestFinalFlight))
|
||||
{
|
||||
ClientlessTravelRouteStep flight;
|
||||
auto const& target=destinationItr->second;
|
||||
flight.NodeKey=locationGoal ? "flight_location" : target.Key;
|
||||
flight.NodeType=locationGoal ? "location" : target.Type;
|
||||
flight.EdgeType="free_flight";
|
||||
flight.MapId=locationGoal ? locationGoal->MapId : target.MapId;
|
||||
flight.X=locationGoal ? locationGoal->X : target.X;
|
||||
flight.Y=locationGoal ? locationGoal->Y : target.Y;
|
||||
flight.Z=locationGoal ? locationGoal->Z : target.Z;
|
||||
flight.O=locationGoal ? 0 : target.O;
|
||||
route.push_back(std::move(flight));
|
||||
reachedNode=locationGoal ? "flight_location" : target.Key;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"C818_PREFLIGHT complete=%u bridgeTests=%u bridgeAccepted=%u bridgeRejected=%u "
|
||||
"distanceRejected=%u forwardTests=%u forwardAccepted=%u steps=%u",
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
-- Native sea-route test: Stormwind -> Ruttheran, no onward action.
|
||||
log("SHIP_TEST_SW_RUTTHERAN loaded")
|
||||
queue_action("travel_to_node", {node="ship_ruttheran_br", hard_destination=true})
|
||||
-- Travel acceptance completed. Wait for explicit actions until daily behavior is configured.
|
||||
log("Arthoran ready; no automatic test journey queued")
|
||||
@@ -8,6 +8,8 @@ return {
|
||||
|
||||
movement = {
|
||||
can_ground_mount = true,
|
||||
can_free_fly = true,
|
||||
flight_mounts = { [32235] = 100 }, -- Golden Gryphon, learned mount spell
|
||||
ground_mount_min_distance = 45,
|
||||
ground_mount_speed_rate = 1.60,
|
||||
-- [mount display ID] = random weight
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "Chat.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
@@ -2202,6 +2203,9 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessFlight(ChatHandler* handler, char const* args)
|
||||
{ return sClientlessFlightMgr->Command(handler,args); }
|
||||
|
||||
static bool HandleClientlessTransport(ChatHandler* handler, char const* args)
|
||||
{
|
||||
return sClientlessTransportMgr->Command(handler, args);
|
||||
@@ -2274,6 +2278,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiImport, "" },
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
|
||||
} },
|
||||
{ "flight", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessFlight, "" },
|
||||
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalObjectImport, "" },
|
||||
|
||||
@@ -4331,3 +4331,12 @@ Metric.ConnectionInfo = "127.0.0.1;8086;worldserver"
|
||||
Metric.OverallStatusInterval = 1
|
||||
|
||||
###################################################################################################
|
||||
|
||||
# Dynamic free flight is profile opt-in. No free-flight nodes/edges are stored.
|
||||
Clientless.Flight.Enabled = 1
|
||||
# Desired clearance, not strict terrain following; hard safety/ceiling checks remain.
|
||||
Clientless.Flight.Clearance = 60
|
||||
# RP penalty normally favors roads. Zone IDs below prefer free flight, including
|
||||
# the final node/virtual-exit -> location leg (Icecrown, Wintergrasp).
|
||||
Clientless.Flight.MinDistance = 600
|
||||
Clientless.Flight.PreferredZones = "210 4197"
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Exercise production Update with a running native spline, not a string check."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
source = (root / 'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
|
||||
a = source.index('void ClientlessFlightMgr::Update(uint32 diff)')
|
||||
b = source.index('bool ClientlessFlightMgr::Command(', a)
|
||||
update = source[a:b]
|
||||
harness = r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
using uint32 = uint32_t;
|
||||
using namespace ClientlessFlightPath;
|
||||
struct Position {
|
||||
float x=0,y=0,z=60;
|
||||
float GetPositionX() const{return x;} float GetPositionY() const{return y;} float GetPositionZ() const{return z;}
|
||||
void Relocate(Position const* p){*this=*p;}
|
||||
};
|
||||
Point PointOf(Position const& p){return {p.x,p.y,p.z};}
|
||||
struct Spline {int remaining=500; int timeRemaining()const{return remaining;}};
|
||||
struct Player:Position {
|
||||
Spline spline; Spline* movespline=&spline;
|
||||
bool alive=true,fly=true,running=true;int stops=0;
|
||||
bool IsInWorld(){return true;} uint32 GetMapId(){return 0;}
|
||||
bool IsAlive(){return alive;} bool IsBeingTeleported(){return false;}
|
||||
bool IsMounted(){return true;} bool CanFly(){return fly;}
|
||||
bool IsSplineEnabled(){return running;}
|
||||
void StopMoving(){++stops;running=false;}
|
||||
} player;
|
||||
struct Session {Player* GetPlayer(){return &player;}} session;
|
||||
enum class ClientlessPlayerState {Online};
|
||||
struct World {
|
||||
Session* FindClientlessSession(uint32){return &session;}
|
||||
ClientlessPlayerState GetClientlessPlayerState(uint32){return ClientlessPlayerState::Online;}
|
||||
} world;
|
||||
auto sWorld=&world;
|
||||
enum class ClientlessFlightState {Mounting,Flying,Arrived,Failed};
|
||||
struct Record {
|
||||
struct {ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0};float Travelled=0;} Status;
|
||||
Position LastPosition,Start,Segment{25,0,60};Settings Settings;
|
||||
uint32 Elapsed=0,SegmentElapsed=0,CheckTimer=0;
|
||||
bool Moving=true,Returning=false;
|
||||
};
|
||||
bool clear=true;
|
||||
bool ClearRay(Player*,Point const&,Point const&){return clear;}
|
||||
class ClientlessFlightMgr {
|
||||
public:
|
||||
std::unordered_map<uint32,Record> _records;
|
||||
int advances=0,handoffs=0,failures=0;
|
||||
Surface Sample(Player*,Record&,float,float){return {0,300,true};}
|
||||
void Fail(Player*,Record& r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;}
|
||||
bool Advance(Player*,Record&,bool handoff=false){++advances;handoffs+=handoff;return true;}
|
||||
void Update(uint32 diff);
|
||||
};
|
||||
'''.replace('Settings Settings;', 'ClientlessFlightPath::Settings Settings;')
|
||||
harness += update + r'''
|
||||
int main(){
|
||||
ClientlessFlightMgr mgr;mgr._records[26]={};
|
||||
// Begin replacement while the existing spline is still active: no stop.
|
||||
mgr.Update(50);assert(mgr.handoffs==1&&mgr.advances==1&&player.stops==0);
|
||||
// The 200ms collision cadence must not launch a replacement each tick.
|
||||
mgr.Update(50);assert(mgr.advances==1);
|
||||
mgr._records[26].CheckTimer=0;player.spline.remaining=900;
|
||||
mgr.Update(50);assert(mgr.advances==1);
|
||||
// Exact landing endpoint must finish normally, never repeatedly extend.
|
||||
auto& r=mgr._records[26];r.CheckTimer=0;r.Status.Landing=r.Segment;
|
||||
player.spline.remaining=400;mgr.Update(50);assert(mgr.advances==1);
|
||||
// Reverse trail keeps its original checked corners.
|
||||
r.Segment={25,0,60};r.Returning=true;r.CheckTimer=0;
|
||||
mgr.Update(50);assert(mgr.advances==1);
|
||||
// Distance means measured motion, not unflown overlapping destinations.
|
||||
player.x=7;r.CheckTimer=0;mgr.Update(50);assert(std::fabs(r.Status.Travelled-7)<0.01);
|
||||
// A newly blocked corridor still stops/replans immediately.
|
||||
clear=false;r.CheckTimer=0;mgr.Update(50);
|
||||
assert(player.stops==1&&mgr.advances==2&&mgr.handoffs==1);
|
||||
// A completed spline continues via the normal non-handoff path.
|
||||
clear=true;r.CheckTimer=0;mgr.Update(50);assert(mgr.advances==3);
|
||||
player.fly=false;mgr.Update(50);assert(mgr.failures==1);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-flight-handoff-') as folder:
|
||||
cpp = Path(folder) / 'test.cpp'
|
||||
binary = Path(folder) / 'test'
|
||||
cpp.write_text(harness)
|
||||
subprocess.run(['c++', '-std=c++17', '-I', str(root/'src/server/game/World'), str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print('Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.')
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Compile production flight eligibility/cost and fail-safe abort decisions with Core doubles."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
source=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
|
||||
def function(signature):
|
||||
a=source.index(signature);opening=source.index('{',a);depth=1;b=opening+1
|
||||
while depth:
|
||||
depth+=(source[b]=='{')-(source[b]=='}');b+=1
|
||||
return source[a:b]
|
||||
harness=r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <limits>
|
||||
using uint32=uint32_t;
|
||||
using namespace ClientlessFlightPath;
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
constexpr int SKILL_RIDING=762,MAP_COMMON=0,SPELL_AURA_MOUNT_RESTRICTIONS=1;
|
||||
struct Position{float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}};
|
||||
Point PointOf(Position const&p){return {p.x,p.y,p.z};}float Infinity(){return std::numeric_limits<float>::infinity();}
|
||||
struct Guid{uint32 GetCounter()const{return 26;}};
|
||||
struct Player{
|
||||
bool license=true,cold=true,learned=true,alive=true,canFly=true,restrictions=false;
|
||||
uint32 skill=225,map=0,area=87;int stops=0,interrupts=0;
|
||||
Guid GetGUID()const{return {};}
|
||||
uint32 GetSkillValue(int)const{return skill;}
|
||||
bool HasSpell(uint32 s)const{return s==90267?license:s==54197?cold:learned;}
|
||||
uint32 GetMapId()const{return map;}uint32 GetAreaId()const{return area;}
|
||||
bool HasAuraType(int)const{return restrictions;}
|
||||
bool CanFly(){return canFly;}bool IsAlive(){return alive;}
|
||||
void StopMoving(){++stops;}void InterruptNonMeleeSpells(bool){++interrupts;}
|
||||
float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 50;}
|
||||
};
|
||||
struct Mount{uint32 DisplayId=32235,Weight=100;};
|
||||
struct Profile{bool CanFreeFly=true;std::vector<Mount> FlightMounts{{}};} profile;
|
||||
struct Lua{auto GetAdventureProfile(uint32){return &profile;}} lua;
|
||||
struct Config{bool enabled=true;bool GetBoolDefault(const char*,bool){return enabled;}float GetFloatDefault(const char*,float v){return v;}std::string GetStringDefault(const char*,const char*v){return v;}} config;
|
||||
auto sClientlessLuaEngine=&lua;auto sConfigMgr=&config;
|
||||
struct SpellMgr{void* GetSpellInfo(uint32){return this;}} spells;auto sSpellMgr=&spells;
|
||||
uint32 FlightType(void*){return 1;}
|
||||
struct MapEntry{uint32 MapType=0;} entry;struct Maps{auto LookupEntry(uint32){return &entry;}} maps;auto& sMapStore=maps;
|
||||
enum class AreaMountFlags{AllowFlyingMounts};
|
||||
struct Flags{bool fly=true;bool HasFlag(AreaMountFlags){return fly;}};
|
||||
struct Area{bool fly=true;uint32 World_pvp_ID=0;Flags GetMountFlags()const{return {fly};}} area;
|
||||
struct Areas{bool known=true;auto LookupEntry(uint32){return known?&area:nullptr;}} areas;auto& sAreaTableStore=areas;
|
||||
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
|
||||
struct Record{
|
||||
struct{ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status;
|
||||
bool Moving=true,Returning=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
|
||||
};
|
||||
class ClientlessFlightMgr{public:
|
||||
uint32 areaCode=87,zoneCode=12;
|
||||
std::pair<uint32,uint32> AreaZone(Player const*,uint32,Position const&)const{return {areaCode,zoneCode};}
|
||||
bool Enabled(Player const*)const;bool Preferred(Player const*,uint32,Position const&)const;
|
||||
float Estimate(Player const*,uint32,Position const&,Position const&)const;
|
||||
void Fail(Player*,Record&,std::string const&);
|
||||
};
|
||||
'''
|
||||
for signature in ('bool ClientlessFlightMgr::Enabled(', 'bool ClientlessFlightMgr::Preferred(', 'float ClientlessFlightMgr::Estimate(', 'void ClientlessFlightMgr::Fail('):
|
||||
harness+=function(signature)+'\n'
|
||||
harness+=r'''
|
||||
int main(){
|
||||
ClientlessFlightMgr mgr;Player p;Position a,b;b.x=1000;
|
||||
float roadBias=mgr.Estimate(&p,0,a,b);assert(roadBias>1000&&std::isfinite(roadBias));
|
||||
p.license=false;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));p.license=true;
|
||||
p.cold=false;assert(!std::isfinite(mgr.Estimate(&p,571,a,b)));p.cold=true;
|
||||
p.skill=150;assert(!mgr.Enabled(&p));p.skill=225;
|
||||
p.learned=false;assert(!mgr.Enabled(&p));p.learned=true;
|
||||
profile.CanFreeFly=false;assert(!mgr.Enabled(&p));profile.CanFreeFly=true;
|
||||
entry.MapType=1;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));entry.MapType=0;
|
||||
// Eversong/Ghostlands and Deeprun use native mount flags 13, without bit 2.
|
||||
area.fly=false;assert(!std::isfinite(mgr.Estimate(&p,530,a,b)));
|
||||
assert(!std::isfinite(mgr.Estimate(&p,369,a,b)));
|
||||
area.fly=true;mgr.zoneCode=210;
|
||||
assert(mgr.Estimate(&p,571,a,b)<roadBias); // explicit zone preference
|
||||
b.x=90;assert(std::isfinite(mgr.Estimate(&p,571,a,b)));
|
||||
mgr.zoneCode=12;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));
|
||||
// Unknown metadata is not an invented blanket prohibition; actual native
|
||||
// mount capability/cast at departure still decides in Start().
|
||||
areas.known=false;b.x=1000;assert(std::isfinite(mgr.Estimate(&p,0,a,b)));areas.known=true;
|
||||
area.fly=false;area.World_pvp_ID=1;assert(std::isfinite(mgr.Estimate(&p,571,a,b)));area.World_pvp_ID=0;
|
||||
// An airborne abort first returns. A blocked return must not dismount/drop.
|
||||
Record r;mgr.Fail(&p,r,"blocked");assert(r.Returning&&r.Status.State==ClientlessFlightState::Flying&&p.stops==1);
|
||||
mgr.Fail(&p,r,"reverse blocked");assert(r.Status.State==ClientlessFlightState::Failed);
|
||||
Record mounting;mounting.Status.State=ClientlessFlightState::Mounting;mgr.Fail(&p,mounting,"cast rejected");
|
||||
assert(mounting.Status.State==ClientlessFlightState::Failed&&p.interrupts==1&&!mounting.Returning);
|
||||
Record revoked;p.canFly=false;mgr.Fail(&p,revoked,"permission lost");assert(revoked.Status.State==ClientlessFlightState::Failed&&!revoked.Returning);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-flight-policy-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
|
||||
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
print('Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.')
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Run the production 3D flight geometry against deterministic terrain/collisions."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
harness=r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
using namespace ClientlessFlightPath;
|
||||
struct Run {Point End;float Peak=0,Side=0;unsigned Steps=0,Avoidances=0;bool Arrived=false;};
|
||||
Run simulate(Point start,Point target,Sample sample,Ray ray,int limit=500){
|
||||
Settings cfg;Memory mem;mem.Departed=start.Z-sample(start.X,start.Y).Ground>cfg.Safety;Run r;Point p=start;
|
||||
for(int i=0;i<limit;++i){
|
||||
auto next=Next(p,target,cfg,mem,sample,ray);
|
||||
if(!next.Valid)break;
|
||||
assert(Corridor(p,next.Next,target,cfg,sample,ray,p.Z-sample(p.X,p.Y).Ground<cfg.Safety));
|
||||
assert(next.Next.Z+5<=sample(next.Next.X,next.Next.Y).Ceiling);
|
||||
p=next.Next;++r.Steps;r.Peak=std::max(r.Peak,p.Z);r.Side=std::max(r.Side,std::fabs(p.Y));r.Avoidances+=next.Avoiding;
|
||||
if(Distance(p,target)<0.8f){r.Arrived=true;break;}
|
||||
}
|
||||
r.End=p;return r;
|
||||
}
|
||||
int main(){
|
||||
auto flat=[](float,float){return Surface{0,300,true};};
|
||||
auto clear=[](Point const&,Point const&){return true;};
|
||||
auto normal=simulate({0,0,0},{800,0,0},flat,clear);
|
||||
assert(normal.Arrived&&normal.Peak>30&&normal.Peak<=60.1f);
|
||||
auto low=[](float,float){return Surface{0,42,true};};
|
||||
auto ceiling=simulate({0,0,0},{800,0,0},low,clear);
|
||||
assert(ceiling.Arrived&&ceiling.Peak<=36.1f);
|
||||
auto peak=[](float x,float){return Surface{x>390&&x<400?40.0f:0.0f,300,true};};
|
||||
auto smooth=simulate({0,0,0},{800,0,0},peak,clear);
|
||||
assert(smooth.Arrived&&smooth.Peak<75); // no nominal clearance chase over short peak
|
||||
auto wall=[](Point const&a,Point const&b){
|
||||
for(int i=0;i<=80;++i){auto p=Mix(a,b,i/80.0f);if(p.X>=240&&p.X<=260&&std::fabs(p.Y)<16&&p.Z<110)return false;}return true;
|
||||
};
|
||||
auto side=simulate({0,0,0},{800,0,0},flat,wall);
|
||||
assert(side.Arrived&&side.Avoidances>0&&side.Side>10);
|
||||
auto wide=[](Point const&a,Point const&b){
|
||||
for(int i=0;i<=80;++i){auto p=Mix(a,b,i/80.0f);if(p.X>=240&&p.X<=260&&std::fabs(p.Y)<500&&p.Z<95)return false;}return true;
|
||||
};
|
||||
auto climb=simulate({0,0,0},{800,0,0},flat,wide);
|
||||
assert(climb.Arrived&&climb.Peak>95&&climb.Avoidances>0);
|
||||
auto unknown=[](float x,float){return Surface{0,300,x<120};};
|
||||
assert(!simulate({0,0,0},{800,0,0},unknown,clear).Arrived);
|
||||
auto blocked=[](Point const&,Point const&){return false;};
|
||||
assert(!simulate({0,0,0},{800,0,0},flat,blocked).Arrived);
|
||||
// Missing/covered/wrong-floor landing is not an arrival.
|
||||
auto invalidEnd=[](float x,float){return Surface{x>790?20.0f:0.0f,300,true};};
|
||||
assert(!simulate({0,0,0},{800,0,0},invalidEnd,clear).Arrived);
|
||||
auto shortFlight=simulate({0,0,0},{90,0,0},flat,clear);
|
||||
assert(shortFlight.Arrived&&shortFlight.Peak>5);
|
||||
// An avoidance may leave the bot high immediately above the landing site.
|
||||
// Open a descending approach instead of oscillating over the target.
|
||||
Memory landingMemory;landingMemory.Departed=true;
|
||||
Point landingStart{660,0,60};
|
||||
auto firstDescent=Next(landingStart,{800,0,0},Settings{},landingMemory,flat,clear);
|
||||
assert(firstDescent.Valid&&firstDescent.Next.X>660&&firstDescent.Next.Z<60);
|
||||
auto highApproach=simulate({0,0,70},{1,0,0},flat,clear);
|
||||
assert(highApproach.Arrived&&highApproach.Steps<20&&highApproach.Peak<=70);
|
||||
std::cout<<"Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-free-flight-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
|
||||
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
@@ -0,0 +1,112 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
h=(root/'src/server/game/World/ClientlessTravelGraphMgr.h').read_text()
|
||||
s=(root/'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
|
||||
structs=h[h.index('struct TC_GAME_API ClientlessTravelNode'):h.index('class TC_GAME_API ClientlessTravelGraphMgr')].replace('TC_GAME_API ','')
|
||||
method=s[s.index('bool ClientlessTravelGraphMgr::PlanAdaptiveRoute('):s.index('bool ClientlessTravelGraphMgr::PlanRouteToPosition(')]
|
||||
signature=h[h.index(' bool PlanAdaptiveRoute('):h.index(' bool FindNearestNodeToPosition(')]
|
||||
preamble=r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
using uint32=uint32_t; using int32=int32_t;
|
||||
#define TC_LOG_INFO(...) do {} while(0)
|
||||
#define TC_LOG_DEBUG(...) do {} while(0)
|
||||
#define TC_LOG_WARN(...) do {} while(0)
|
||||
constexpr uint32 NAV_GROUND=1, PATHFIND_INCOMPLETE=2, PATHFIND_NOPATH=4;
|
||||
struct Position { float x=0,y=0,z=0; void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
|
||||
struct Player { float x=0,y=0,z=0; bool IsInWorld() const{return true;} uint32 GetMapId()const{return 0;} uint32 GetInstanceId()const{return 0;} float GetPositionX()const{return x;} float GetPositionY()const{return y;} float GetPositionZ()const{return z;} };
|
||||
float dist(Position a,Position b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
|
||||
struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious=false; uint32 PathType=1,PointCount=2; float DirectDistance=0,PathLength=0,MaxSegmentLength=0,MaxZDelta=0,FinalDistance=0; std::string Reason;};
|
||||
struct Travel {void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);}} travel;
|
||||
auto sClientlessTravelMgr=&travel;
|
||||
struct Flight {
|
||||
bool enabled=false;float minimumX=-10000;
|
||||
void BeginPlanning(){}
|
||||
bool Enabled(Player const*){return enabled;}
|
||||
float Estimate(Player const*,uint32 map,Position a,Position b){
|
||||
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&d>=60?180+d*1.5f:std::numeric_limits<float>::infinity();
|
||||
}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
struct VcorePositionPathResult {bool complete=false; uint32 Result=2;float FinalDistance=0,PathLength=0;bool IsComplete()const{return complete;}};
|
||||
struct PathGenerator {static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){float d=dist(a,b);return {available && d<=maxDistance,2,0,d};}};
|
||||
std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;}
|
||||
'''
|
||||
cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+'''
|
||||
std::unordered_map<std::string,ClientlessTravelNode> _nodes;
|
||||
std::unordered_map<std::string,std::vector<ClientlessTravelEdge>> _edges;
|
||||
std::unordered_map<uint32,std::vector<ClientlessTravelEdgePoint>> _edgePoints;
|
||||
};
|
||||
'''
|
||||
tests=r'''
|
||||
void node(ClientlessTravelGraphMgr& g,std::string k,float x,float y=0){ClientlessTravelNode n;n.Key=k;n.X=x;n.Y=y;g._nodes[k]=n;}
|
||||
void edge(ClientlessTravelGraphMgr&g,std::string a,std::string b,uint32 id,float cost,bool rev=false,std::string type="ground"){g._edges[a].push_back({id,a,b,type,cost,rev});}
|
||||
std::vector<ClientlessTravelRouteStep> plan(ClientlessTravelGraphMgr&g,Player p,ClientlessTravelLocationGoal goal,float radius,std::unordered_set<std::string> blocked={}){
|
||||
std::vector<ClientlessTravelRouteStep> r;bool complete=false;std::string reached,error;
|
||||
assert(g.PlanAdaptiveRoute(&p,"B",radius,200,r,complete,reached,error,nullptr,&blocked,&goal));if(!complete){std::cerr<<"incomplete x="<<p.x<<" goal="<<goal.X<<" blocked="<<blocked.size()<<" error="<<error<<"\n";}assert(complete);return r;}
|
||||
int main(){
|
||||
ClientlessTravelGraphMgr g;node(g,"A",0);node(g,"B",1000);edge(g,"A","B",1,1000);
|
||||
for(float x:{0.f,100.f,700.f,900.f,1000.f}){ClientlessTravelEdgePoint p;p.X=x;g._edgePoints[1].push_back(p);}
|
||||
ClientlessTravelLocationGoal goal;goal.X=700;goal.Y=20;goal.AnchorRadius=50;
|
||||
// A stored path exists. Its interior exit must still win without remote MMAP.
|
||||
auto r=plan(g,{},goal,1);assert(r.back().GroundEndIndex==2);assert(r.back().X==700);
|
||||
// Preserve a valid entry even when another entry reaches the endpoint cheaper.
|
||||
Player p;p.x=100;r=plan(g,p,goal,900);assert(r.front().NodeKey=="virtual_ground_entry");assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==2);
|
||||
// Reverse direction uses execution-order indices.
|
||||
ClientlessTravelGraphMgr rev=g;rev._edges.clear();edge(rev,"B","A",1,1000,true);p.x=900;goal.X=100;r=plan(rev,p,goal,1);assert(r.back().EdgeReverse);assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==3);
|
||||
// Blacklist the nearest failed exit: select the next eligible waypoint.
|
||||
goal.X=700;goal.AnchorRadius=250;r=plan(g,{},goal,1,{"exit:1:2F"});assert(r.back().GroundEndIndex==3);
|
||||
// Regular anchors use the same estimated local cost without remote MMAP.
|
||||
goal.X=1000;goal.Y=0;goal.AnchorRadius=10;r=plan(g,{},goal,1);assert(r.back().NodeKey=="B");assert(r.back().GroundEndIndex==-1);
|
||||
// Forward entry after a transport can exit on the same road before its endpoint.
|
||||
ClientlessTravelGraphMgr f=g;node(f,"S",-1000);node(f,"T",100);edge(f,"S","T",2,1,false,"taxi");p.x=-1000;goal.X=700;goal.Y=20;goal.AnchorRadius=50;PathGenerator::available=true;PathGenerator::maxDistance=150;
|
||||
r=plan(f,p,goal,1);assert(r.back().GroundStartIndex==1);assert(r.back().GroundEndIndex==2);assert(r[r.size()-2].NodeKey=="virtual_ground_entry");
|
||||
// A regular node outside the exit radius must not win an optimistic local leg.
|
||||
ClientlessTravelGraphMgr detour=g;node(detour,"N",300,0);edge(detour,"A","N",5,1,false,"taxi");goal.X=700;goal.Y=20;goal.AnchorRadius=1000;p.x=0;PathGenerator::available=false;
|
||||
r=plan(detour,p,goal,1);assert(r.back().NodeKey=="virtual_ground_exit");
|
||||
// Restore the directional test's narrow goal radius.
|
||||
goal.AnchorRadius=50;
|
||||
// A point beyond the exit cannot traverse the directed road backwards.
|
||||
PathGenerator::available=false;p.x=900;r=plan(g,p,goal,1);assert(r.back().GroundStartIndex<0); // uses ordinary B anchor when expanded below
|
||||
// New flight alternatives are transient and preserve the existing road preference.
|
||||
flight.enabled=true;Player start;
|
||||
ClientlessTravelGraphMgr air;node(air,"A",0);node(air,"B",1000);
|
||||
std::vector<ClientlessTravelRouteStep> route;bool done=false;std::string endKey,why;
|
||||
assert(air.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
|
||||
assert(done&&route.size()==2&&route.back().EdgeType=="free_flight"&&route.back().EdgeDatabaseId==0);
|
||||
assert(air._edges.empty()&&air._nodes.size()==2);
|
||||
ClientlessTravelGraphMgr road=air;edge(road,"A","B",9,1000);
|
||||
assert(road.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
|
||||
assert(done&&route.back().EdgeType=="ground");
|
||||
ClientlessTravelLocationGoal far;far.X=2000;far.AnchorRadius=50;
|
||||
assert(air.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&far));
|
||||
assert(done&&route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight");
|
||||
// A permissible takeoff halfway along a real road may beat its far endpoint.
|
||||
ClientlessTravelGraphMgr exits;node(exits,"A",0);node(exits,"B",1000);node(exits,"D",500,800);
|
||||
edge(exits,"A","B",8,1000);for(float x:{0.f,100.f,500.f,900.f,1000.f}){ClientlessTravelEdgePoint pt;pt.X=x;exits._edgePoints[8].push_back(pt);}
|
||||
flight.minimumX=400;
|
||||
assert(exits.PlanAdaptiveRoute(&start,"D",1,200,route,done,endKey,why));
|
||||
assert(done&&route.back().NodeKey=="D"&&route.back().EdgeType=="free_flight");
|
||||
assert(route[route.size()-2].NodeKey=="virtual_ground_exit");
|
||||
for(auto const& step:route)if(step.EdgeType=="free_flight")assert(step.NodeKey!="virtual_ground_entry");
|
||||
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
|
||||
}
|
||||
'''
|
||||
# Last direction case must be unreachable: explicitly inspect a partial result.
|
||||
tests=tests.replace('r=plan(g,p,goal,1);assert(r.back().GroundStartIndex<0); // uses ordinary B anchor when expanded below','std::vector<ClientlessTravelRouteStep> partial;bool complete;std::string reached,error;assert(g.PlanAdaptiveRoute(&p,"B",1,200,partial,complete,reached,error,nullptr,nullptr,&goal));assert(!complete);')
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-flight-planner-") as folder:
|
||||
cpp=Path(folder)/"test.cpp";binary=Path(folder)/"test"
|
||||
cpp.write_text(preamble+structs+cls+method+tests)
|
||||
subprocess.run(["c++","-std=c++17","-O0",str(cpp),"-o",str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
Reference in new issue
Block a user