Keep clientless flight moving across spline handoffs
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:
1 parent
8df9591c99
commit
ca656403bb
5 files changed
+149
-12
No files matched your search
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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.')
|
||||
Reference in new issue
Block a user