Keep clientless flight moving across spline handoffs
GCC PCH / build (push) Waiting to run
clang PCH / build (push) Canceled after 0s

Replace ordinary flight splines before they finish while retaining collision checks, terminal landing and conservative return behavior. Measure actual travelled distance, cover handoff timing with a production Update regression test, retire Arthoran's automatic ship test and document visual acceptance of the travel milestone.
This commit is contained in:
Nico committed 2026-10-26 18:21:48 +01:00
1 parent 8df9591c99
commit ca656403bb
5 files changed
+149 -12

No files matched your search

+23 -1
View File
@@ -60,6 +60,13 @@ Konfigurationsänderungen können über `reload config` übernommen werden.
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.
@@ -116,6 +123,7 @@ Automatisierte Prüfungen:
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:
@@ -132,9 +140,23 @@ Serverseitige Testbeobachtungen am 26.10.2026 mit Arthoran (GUID 26):
- 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. Die visuelle Abnahme bleibt offen.
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.
+29 -5
View File
@@ -13,6 +13,7 @@
#include "Log.h"
#include "Map.h"
#include "MoveSplineInit.h"
#include "MoveSpline.h"
#include "PhasingHandler.h"
#include "Player.h"
#include "PathGenerator.h"
@@ -213,6 +214,7 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
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)
@@ -297,7 +299,7 @@ void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string con
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 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)
@@ -334,7 +336,18 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record)
back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false
};
}
else next = Next(current, goal, record.Settings, record.Memory, sample, ray);
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");
@@ -357,9 +370,9 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record)
record.SegmentElapsed = 0;
++ record.Status.Segments;
record.Status.Avoidances += next.Avoiding ? 1 : 0;
record.Status.Travelled += Distance(current, next.Next);
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 target=(%.2f %.2f %.2f) detail='%s'", player->GetGUID().GetCounter(), record.Status.Segments, next.Avoiding ? 1 : 0, next.Next.X, next.Next.Y, next.Next.Z, record.Status.Detail.c_str());
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;
}
@@ -378,6 +391,9 @@ void ClientlessFlightMgr::Update(uint32 diff)
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;
@@ -420,6 +436,14 @@ void ClientlessFlightMgr::Update(uint32 diff)
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;
@@ -496,6 +520,6 @@ bool ClientlessFlightMgr::Command(ChatHandler * handler, char const * args)
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 travelled=%.1f detail='%s'", guid, StateName(status->State), status->MapId, status->MountSpell, status->Segments, status->Avoidances, status->Travelled, status->Detail.c_str());
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;
}
+3 -3
View File
@@ -17,7 +17,7 @@ struct ClientlessFlightStatus
{
ClientlessFlightState State = ClientlessFlightState::Failed;
std::string Detail;
uint32 MapId = 0, MountSpell = 0, Segments = 0, Avoidances = 0;
uint32 MapId = 0, MountSpell = 0, Segments = 0, Avoidances = 0, Handoffs = 0;
Position Target, Landing;
float Travelled = 0;
};
@@ -39,7 +39,7 @@ class TC_GAME_API ClientlessFlightMgr
ClientlessFlightStatus Status;
ClientlessFlightPath::Settings Settings;
ClientlessFlightPath::Memory Memory;
Position Start, Segment;
Position Start, Segment, LastPosition;
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
bool Moving = false, Returning = false;
std::string AbortReason;
@@ -47,7 +47,7 @@ class TC_GAME_API ClientlessFlightMgr
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 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;
@@ -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")
+92
View File
@@ -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.')