Fix shared flight trail replacement and safe follower rejoining

Publish replacement landing targets, bound orphaned trail waits, and plan safe connections from the follower position. Support shared-route regroup landings and preserve catch-up flights after the leader lands. Add geometry, lifecycle and group travel regressions.
This commit is contained in:
Nico committed 2026-11-03 19:59:56 +01:00
1 parent 768810a82d
commit 65e4abb0b9
11 files changed
+423 -24

No files matched your search

+199
View File
@@ -1,3 +1,202 @@
# Navigationskorrektur zum lokalen Commit freigegeben
Der Nutzer hat den lokalen Commit der installierten Shared-flight-Korrektur
und ihrer Regressionstests beauftragt. Kein Push und kein weiterer Build oder
Serverneustart für diesen Commit. Gesicherte Binärdateien und Laufzeitlogs
bleiben außerhalb des Commits.
---
# 2026-11-03 – Shared-flight-/Wiedereinstiegsfix installiert
Nutzer beauftragte die vereinbarten Navigationskorrekturen. Implementierung
und gezielte Tests sind abgeschlossen, Build -j10 und Installation erfolgreich.
SharedTrail veröffentlicht beim Ersatz den Landepunkt und einen Nachfolger;
Store invalidiert auch komplett ersetzte Flugrecords. Folger lösen Ersatzketten
auf und warten auf fehlende Landepunkte/Punkte höchstens120s. SharedJoin plant
von der tatsächlichen Position zu einem nahen verbleibenden Punkt der
Anführerroute, statt alte Historie ab Punkt0 nachzufliegen. Luftige Zielpunkte,
Steigung, vertikales Ausweichen und Plattformvolumen werden dabei geprüft.
LandFollowing unterstützt beide Folgemodi. Shared-Zwischenlandung endet als
Recovery/Failed, nicht als vermeintliche Raidankunft. Der Gruppenmanager bricht
validierte Shared-/action-owned Nachholflüge nicht mehr ab, wenn der Leader
bereits gelandet ist. Makroroutenwahl bleibt beim Anführer.
Gezielte Regressionen PASS: shared_flight_rejoin (synthetischer Nach-Taxi-
Anschluss und Winienwis Originalkoordinaten, blockierter alter Rückweg,
Turmplattform), shared_flight_landing (Recordersatz, Publication, Timeout,
Abort), group_flight_follow (beide Landemodi), flight_handoff (echter
Update-Replan publiziert Ready+Replacement), adventure_group_travel (Leader
landet vor Folger), flight_policy, elevated_flight, free_flight und
flight_takeoff_handoff. Alter test_flight_departure war schon vor diesem Task
mit unordered_map::at defekt; nur sein Store-Testdouble angepasst, kein PASS
für diesen alten Test behaupten.
Builds jeweils mit pausiertem Loader und erneutem SafeLogout eingeleitet.
Vor Installation volle5min seit letzter Anforderung19:36:15 eingehalten.
12 Restbots samt GUID/Fraktion/Position und verfügbaren Native-Flags gesichert;
nicht verfügbare individuelle Combat/Taxi/Teleportwerte explizit null, Status-
Aggregate und Transportausgaben zusätzlich im pre-shutdown.log.
Shutdown erneut bekannter separater GameObject::RemoveFromWorld651-ZoneScript-
Fehler, Core worldserver-20261103-184941.core und passende alte Executable
worldserver-before-fix gesichert. Nicht als behoben darstellen.
Neuer Server PID84679, frische ready-Meldung, Ports8085/8087 und Fortbestand
verifiziert. 118 Bots wieder online, Loader1, safeLogout pending0. Restbots
nach tatsächlichem Online geprüft (frühere Abfragen während Login waren noch
ungültig). Hildda64/Norara91/Volakmar128/Ashaelri176 hatten danach gescheiterte
oder fehlende Flugrecords und hingen über Boden: nativ in Fraktionshauptstadt
teleportiert, Ergebnispositionen bestätigt. Reguläre Flüge/Taxi beibehalten.
Winienwi163 wurde für diese Anreise NICHT teleportiert. Nach sicherem Logout
am Taxiknoten und normalem Relog flog sie 19:44:43 selbstständig zur Bastion,
landete19:46:30 auf validiertem Boden und erreichte19:46:32 den Eingangsknoten
(-4899.67,-4222.36,827.764). Route Completed, Flight Arrived. Dieser Live-Flug
war eigenständig ohne SharedTrail-Bindung; Live-Erfolg deshalb nicht als
vollständigen Live-Nachweis aller Rejoin-/Replacementfälle ausgeben. Diese
sind durch die Produktionscode-Regressionen abgedeckt, Spielabnahme bleibt
beim Nutzer. Originale Raidpläne beibehalten, keinen neuen Testtermin oder
Dauerbeobachter eingerichtet.
Artefakte checkpoints/2026-11-03-shared-flight-rejoin/ (result.json,
winienwi-live-arrival.log, Build/Install/Startup/Recovery und Roster).
Neuer Test tests/clientless/test_shared_flight_rejoin.py ist noch untracked;
Source-WIP bewusst uncommittet, Nutzer hat in diesem Turn keinen Commit/Push
beauftragt. Vorherige Änderungen/Checkpoint-WIP erhalten.
---
# 2026-11-03 – Shared-flight-/Wiedereinstiegsfix in Arbeit
Nutzer beauftragt die vereinbarten Korrekturen. Implementiert:
- SharedTrail.Replace veröffentlicht Landepunkt sofort und verlinkt Nachfolger.
- Store invalidiert/verlinkt auch beim Ersatz eines kompletten Flugrecords.
- Folger lösen Ersatzgenerationen auf, setzen ihren Join-Cursor zurück und
warten auf unvollständige/ausbleibende Punkte maximal120s; Abbruch/Ende greift
früher. Keine unbegrenzt gehaltenen verwaisten SharedPointers mehr.
- SharedJoin sucht unter den verbleibenden Punkten die nächsten räumlichen
Kandidaten (max8 geprüfte Kandidaten). Geometrieplaner erstellt sicheren
Anschluss an den luftigen Wegpunkt, einschließlich begrenzter Steigung,
vertikalem Ausweichen und Plattformprüfung. Keine neue Gruppen-Makroroute.
- LandFollowing unterstützt SharedRoute und MovingTarget. Direkte lokale
Landung nur nach Volumenprüfung; Shared-Regroup endet FAILED statt fälschlich
Raidankunft ARRIVED, damit der Executor neu plant.
- Gruppenmanager lässt Shared-/action-owned Nachholflüge weiterlaufen, wenn
der Anführer schon gelandet ist; direkte Flugformation bleibt unverändert.
Tests bestanden: shared_flight_rejoin (inkl. Winienwis Originalkoordinaten,
blockiertem altem Rückweg, sicherem neuem Anschluss und Turmplattform),
shared_flight_landing (Recordersatz, Publication,120sWait,Abort),
group_flight_follow (beide Landemodi), flight_handoff (echter Update-Replan mit
LandingReady+Replacement), adventure_group_travel (Leader am Boden),
flight_policy/elevated_flight/free_flight/takeoff_handoff.
Bekannter alter test_flight_departure hatte bereits vor diesem Task einen
unordered_map::at-Abbruch. Store-Testdouble für neue Produktionsmethode ergänzt;
nicht als Gameplaynachweis verwenden.
Loader seit19:30:08 pausiert, safeLogoutall vor JEDEM Build angefordert.
Build1 und Build-final PASS, letzterBuild (GroupGuard) läuft/Log beachten.
Letzte SafeLogout-Anforderung19:36:15+01, konservativ volle5min bis19:41:15
vorShutdown bei Restbots. Noch12Bots; nicht einzeln ausloggen/Transport abbrechen.
Checkpoints/2026-11-03-shared-flight-rejoin/ enthältRoster,Operation,Buildlogs.
Installation/Restart/Restbotprüfung/LoaderRestore noch OFFEN. Kein Commit/Push
beauftragt. Bisheriger Raidtest für diesen Build unterbrochen; nachher vorherige
Bots/Pläne wiederherstellen, nicht ungefragt neuenRaidtest/Zeiten anlegen.
Separater ZoneScript-Shutdownfehler bleibt offen; GDB-Dumps weiter sichern.
---
# Winienwis Flugkorridor: falscher Wiedereinstieg konkret belegt
Nutzer fragt warum trotzfreiemHimmel. FollowTrail bindetSharedIndex0 an alte
650PunkteStreckeTaliaarnora164. ErsterPunkt(19:12:26):(-4863.52,-6715.61,24.22),
bei altem Abflugpunkt Hafen. Winienwi nachTaxi nun(-4170.55,-4748.93,126.786).
GeprüfterAnschluss ist2087.72yd rückwärts zumaltenPunkt, NICHTdirektzurBastion.
Corridor nimmt418Samples. NativeSurfaceprobes anSamples1..5:
Ground126.786,126.785,129.250,126.643,126.372. BeimSample3 nachca15yd
(-4175.523469,-4763.044928) istSplineZ126.04988 +0.5Toleranz126.54988,
aberSurface129.250 => Bodenfreiheitprüfung scheitert zwingend vorRayprüfung.
FollowTrail erhöhtIndex nurbeiDistanz<0.7yd,keineSuchegeeigneterEinstiegspunkte.
Somit Anschluss-/SharedTrail-Fehler; kein BelegversperrterDirektverbindungzumRaid.
ErgänztgemerktenFix: Wiedereinstieg auf passende vorausliegendeTeilroute nach
Taxi/Recovery, eigenergeprüfterAnschluss stattimmerIndex0. Artefakt
winienwi-stuck/wrong-shared-entry.json. NurRead-onlyProbes,keineBewegung.
---
# Winienwi-Status 03.11.2026 19:21
Nutzer fragt Status. Winienwi163 hat nativ einen Taxiflug bis taxi_663/Gipfel
des Siegers im Schattenhochland beendet (19:21:13). Anschließender Freiflug
zur Bastion sofort abgelehnt, weil Spieler noch teleport=1 im Taxi-Landeübergang.
Recoverybudget5 war bereits verbraucht, dadurch gesamterAnreiseversuch Failed.
19:21:18 teleport0, alive1, combat0, Position(-4170.55,-4748.93,126.787),
EntfernungzumRaideingang1140.29yd. Behavior wartet mit 'instance entrance travel
failed; retrying the same visit', gleicherRaidintent/Deadline21:00 aktiv.
Kein alterSharedTrail-Wartezustandmehr. Keine neue Bewegung/Teleport/Änderung
vonCodex, nurRead-onlySnapshot. NächsterautomatischerVersuchnochzuprüfen.
---
# Winienwi zum Portal gebracht, 03.11.2026 19:18
Vereinbarten SharedTrail-/LandFollowing-Fix als offenen Folgepunkt gespeichert,
noch nicht implementiert. Native FlightAbort163 plus kurzer BehaviorStop und
Adminteleport an sichere Position6yd vor StormwinderSchattenhochlandportal
(-8186.15,419.729,116.787). Er benutzte das Portal anschließend selbstständig:
19:18:26 PORTAL_USE_START spawn200627/entry207694/spell85545 und EDGE_COMPLETE
portal_use edge1801, Ankunft Schattenhochland area5140/zone4922. KeinTeleport
direkt insSchattenhochland. Raidroute läuft danach weiter; ersterAbflug dort
19:18:34 scheitert Korridor, Recovery beginntBodenweg, späterKampfreaktion.
Schedule wieder1, ursprünglichesRaidziel/Deadline erhalten. Temporären game_tele-
EintragCodexTempSWHighlandsPortal gelöscht und Katalogreload, Löschung geprüft.
KeineSourceänderung/Build/Restart. User beobachtet Testweiter.
---
# Vereinbarter nächster Navigationsfix, 03.11.2026
Nutzer stimmt dem Vorschlag zu und verlangt ihn zu merken: Flug-Neuplanungen
müssen den geprüften Landepunkt vollständig veröffentlichen, alte SharedTrails
explizit ersetzen/invalidieren und Folger kontrolliert auf die Nachfolgestrecke
übernehmen. Anschluss von tatsächlicher Position prüfen. Unvollständige oder
verwaiste Flugstrecken dürfen kein unbegrenztes Warten auslösen. Sichere Landung
muss sowohl moving-target FollowingPlayer als auch shared-route Following
unterstützen und ihr Ergebnis bestätigen. Gruppenroutenwahl bleibt beim Leader.
Regression: Leader replant während Folger noch am Start wartet und beendet
anschließend Flug; Folger muss nachweislich fliegen oder sicher landen.
Noch NICHT implementiert. Jetzt ausdrücklich nur Winienwi163 zum Stormwinder
Portal ins Schattenhochland bringen; Test ansonsten weiterlaufen lassen.
---
# 2026-11-03 19:15 – Winienwi wartet auf verwaiste Flugstrecke
Nutzer meldet Winienwi163 hängt fest. Native Statusprüfung: Sturmwind
(-8779.848,579.868,119.133), support119.152, mounted1/canFly1/flying0/falling0.
Seit19:09:39 keine Bewegung. FlugstateFlying,0Segmente,Detailwaiting for leader
validated landing. Group3, leader164Taliaarnora. Anführerin fliegt inzwischen
weiter RichtungRaid, nativePosition19:14(-4889.496,-4720.076,555.722).
Rootcause: ClientlessFlightMgr::Update AIRBORNE_REPLAN ersetztPublished durch
neuesSharedTrail ohneLanding/LandingReady zu setzen. BeimLeader16419:09:09
trittdiesauf. Winienwi bindet sich19:09:39 an diesesTrail, landingReady0.
Leaderlandet19:09:45, beginnt19:09:48weiterenFlug. GehaltenerSharedPointerdes
Followers bleibt gültig, aber ohneLandung/ohneAborted. Advancekehrtvorjeder
Bewegung mit waiting for leader validated landing zurück;keinWarte-Timeout.
ZweiterFehler: GroupTravelMgr fordertLandFollowing wegenSeparation, aberdiese
Funktion behandelt nur Status.FollowingPlayer (moving-target mode), nicht
record.Following (shared route). Daherreturnfalse ohneRecovery. Statusdetail
safe landing before ground regroup/leave ist nurAbsicht,keineausgeführteLandung.
NurDiagnosebeauftragt,keineSourceänderung/Build/Teleport/Reset. Artefakte unter
checkpoints/2026-11-03-2000-raid-test/winienwi-stuck/. Korrektur mussLanding bei
Replanpublizieren, alteTrails sauber invalidieren/Followers neu binden sowie
zeitlichbegrenzte sichereRecovery beiverlorenemLeader unterstützen. Aktueller
Raidtestläuftweiter,keineDauerbeobachtung.
---
# 2026-11-03 – Änderungen committet, 19/20-Uhr-Raidtest gestartet
Der Nutzer bestätigt die automatische Clientfreigabe auch mit Allitester:
@@ -344,9 +344,11 @@ bool AdventureGroupTravelMgr::Follow(Player* player)
{
auto flight=sClientlessFlightMgr->GetStatus(guid);
if(!flight || flight->State==ClientlessFlightState::Arrived || (!player->IsFlying() && flight->State==ClientlessFlightState::Failed))record.FlightOwned=false;
// Shared/action-owned corridors have their own validated destination.
// A leader landing ahead must not cancel a follower's catch-up flight.
else if(!flight->FollowingPlayer)return true;
else if(!player->IsInMap(leader) || !leader->IsFlying() || leader->IsInFlight() || leader->GetTransport() || Separated(player))
{sClientlessFlightMgr->LandFollowing(player);record.Detail="safe landing before ground regroup/leave";return true;}
else if(!flight->FollowingPlayer)return true;
}
if(player->GetTransport() || player->IsInFlight())return true;
if(!record.Pending.empty())
+61 -14
View File
@@ -291,16 +291,32 @@ bool ClientlessFlightMgr::Follow(Player* player,Position const& target,std::stri
bool ClientlessFlightMgr::LandFollowing(Player* player)
{
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end())return false;
auto& record=found->second;if(!record.Status.FollowingPlayer)return false;
auto& record=found->second;
bool const shared=bool(record.Following);
if(!record.Status.FollowingPlayer && !shared)return false;
record.Following.reset();record.SharedWaitSince=0;
record.Published->Aborted=true;
record.Status.FollowingPlayer=false;
if(record.Status.State==ClientlessFlightState::Mounting)
{Fail(player,record,"group flight cancelled before takeoff");return true;}
auto surface=Sample(player,record,player->GetPositionX(),player->GetPositionY());
float solid=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),MAX_HEIGHT,true,MAX_HEIGHT);
if(surface.Valid && std::fabs(surface.Ground-solid)<0.7f)
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);};
Point landing{player->GetPositionX(),player->GetPositionY(),solid};
if(surface.Valid && std::fabs(surface.Ground-solid)<0.7f &&
solid<=player->GetPositionZ()+0.7f &&
Corridor(PointOf(*player),landing,landing,record.Settings,sample,ray,true))
{
record.Status.Landing.Relocate(player->GetPositionX(),player->GetPositionY(),solid,player->GetOrientation());
record.Status.Target=record.Status.Landing;record.Memory={};record.Following.reset();record.Status.Travelled=0;
player->StopMoving();record.Moving=false;
if(shared)
{
// This is a regroup landing, not arrival at the requested raid.
record.Start=record.Status.Landing;record.Returning=true;
record.AbortReason="shared flight regroup landing";record.Breadcrumbs.clear();
}
record.Status.Detail="landing after group flight";return true;
}
Fail(player,record,"no safe local landing; returning along validated flight trail");return true;
@@ -336,6 +352,19 @@ bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record)
record.Moving=true;record.SegmentElapsed=0;++record.Status.Segments;record.Status.Detail="following moving group flight target";return true;
}
void ClientlessFlightMgr::Store(uint32 guid, Record&& record)
{
auto old=_records.find(guid);
if(old!=_records.end() && old->second.Published!=record.Published)
{
old->second.Published->Aborted=true;
if(old->second.Status.MapId==record.Status.MapId &&
old->second.Status.Target.GetExactDist(record.Status.Target)<1)
old->second.Published->Replacement=record.Published;
}
_records[guid]=std::move(record);
}
bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::string & error, bool following)
{
if (! Trinity::IsValidMapCoord(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
@@ -529,9 +558,9 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
record.Memory.Departed=player->GetPositionZ()-origin.Ground>record.Settings.Safety;
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RESUME_AIRBORNE guid=%u ground=%.3f z=%.3f",player->GetGUID().GetCounter(),origin.Ground,player->GetPositionZ());
_records[player->GetGUID().GetCounter()]=std::move(record);return true;
Store(player->GetGUID().GetCounter(),std::move(record));return true;
}
_records[player->GetGUID().GetCounter()] = std::move(record);
Store(player->GetGUID().GetCounter(),std::move(record));
if (!existingMount && player->CastSpell(player, spell, false) != SPELL_CAST_OK)
{
Fail(player, _records.at(player->GetGUID().GetCounter()), "native mount spell rejected");
@@ -597,7 +626,7 @@ bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool
record.Status.State=ClientlessFlightState::Mounting;record.Status.FollowingPlayer=following;
record.Status.Detail="walking to an open takeoff point";
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_DEPARTURE_WALK guid=%u destination=(%.3f %.3f %.3f) path=%.2f",player->GetGUID().GetCounter(),x,y,surface.Ground,path.PathLength);
_records[player->GetGUID().GetCounter()]=std::move(record);return true;
Store(player->GetGUID().GetCounter(),std::move(record));return true;
}
summary();return false;
}
@@ -635,14 +664,23 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
{
if(record.Following && !record.Returning)
{
Point landing;
if(!record.Following->Aborted && !record.Following->BindLanding(landing))
{record.Status.Detail="waiting for leader validated landing";return true;}
if(record.Following->BindLanding(landing))
while(record.Following->Replacement)
{
record.Status.Landing.Relocate(landing.X,landing.Y,landing.Z);
record.Published->Landing=landing;record.Published->LandingReady=true;
record.Following=record.Following->Replacement;
record.SharedIndex=0;record.Join={};record.SharedWaitSince=0;
}
if(record.Following->Aborted)
{LandFollowing(player);return true;}
Point landing;
if(!record.Following->BindLanding(landing))
{
if(!record.SharedWaitSince)record.SharedWaitSince=record.Elapsed+1;
if(record.Following->Complete || record.Elapsed+1-record.SharedWaitSince>=120000)
{LandFollowing(player);return true;}
record.Status.Detail="waiting for leader validated landing";return true;
}
record.Status.Landing.Relocate(landing.X,landing.Y,landing.Z);
record.Published->Landing=landing;record.Published->LandingReady=true;
}
Point current = PointOf(* player), goal = PointOf(record.Returning ? record.Start : record.Status.Landing);
auto sample =[&](float x, float y)
@@ -669,6 +707,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
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";
record.Published->Complete=true;
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;
}
@@ -685,12 +724,20 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
else if(record.Following)
{
auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray,
handoff?player->GetSpeed(MOVE_FLIGHT)*1.5f:0.f);
if(shared.Waiting)return true;
handoff?player->GetSpeed(MOVE_FLIGHT)*1.5f:0.f,&record.Join);
if(shared.Waiting)
{
if(!record.SharedWaitSince)record.SharedWaitSince=record.Elapsed+1;
if(record.Following->Complete || record.Elapsed+1-record.SharedWaitSince>=120000)
LandFollowing(player);
return true;
}
record.SharedWaitSince=0;
next=shared.Step;
if(!next.Valid)
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_SHARED_REJECT guid=%u index=%u size=%u aborted=%u landing=(%.3f %.3f %.3f)",
player->GetGUID().GetCounter(),uint32(record.SharedIndex),uint32(record.Following->Points.size()),record.Following->Aborted?1u:0u,goal.X,goal.Y,goal.Z);
if(!next.Valid){LandFollowing(player);return true;}
if(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true;
}
else
@@ -814,7 +861,7 @@ void ClientlessFlightMgr::Update(uint32 diff)
player->StopMoving();record.Moving=false;record.SegmentElapsed=0;
record.Start.Relocate(player);record.LastPosition.Relocate(player);
record.Memory={};record.Breadcrumbs.clear();
record.Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
record.Published=record.Published->Replace(PointOf(record.Status.Landing));
record.Status.Detail=ownerLost?"replanning detached free flight":"replanning excessive obstacle avoidance";
ClientlessTravelTelemetry::Note(player,"flight-airborne-replan",record.Status.Detail);
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_AIRBORNE_REPLAN guid=%u ownerLost=%u segments=%u avoidances=%u target=(%.2f %.2f %.2f)",
@@ -44,6 +44,8 @@ class TC_GAME_API ClientlessFlightMgr
std::shared_ptr<ClientlessFlightPath::SharedTrail> Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
std::shared_ptr<ClientlessFlightPath::SharedTrail> Following;
std::size_t SharedIndex=0;
ClientlessFlightPath::SharedJoin Join;
uint32 SharedWaitSince=0;
ClientlessFlightStatus Status;
ClientlessFlightPath::Settings Settings;
ClientlessFlightPath::Memory Memory;
@@ -62,6 +64,7 @@ class TC_GAME_API ClientlessFlightMgr
};
ClientlessFlightPath::Surface Sample(Player * player, Record & record, float x, float y) const;
bool Advance(Player * player, Record & record, bool handoff = false);
void Store(uint32 guid, Record&& record);
bool Begin(Player* player, Position const& target, std::string& error, bool following);
bool ApproachDeparture(Player* player, Record& record, bool following);
bool FindReturnLanding(Player* player, Record& record);
+52 -4
View File
@@ -5,6 +5,7 @@
#include <cmath>
#include <functional>
#include <limits>
#include <memory>
#include <vector>
// Geometry-only receding-horizon navigation. Server adapters supply phased
// terrain/ceiling and static+dynamic collision queries. No Player shortcuts.
@@ -117,11 +118,11 @@ namespace ClientlessFlightPath
}
return true;
}
inline Result Next(Point const & from, Point const & goal, Settings const & cfg, Memory & memory, Sample const & sample, Ray const & ray)
inline Result Next(Point const & from, Point const & goal, Settings const & cfg, Memory & memory, Sample const & sample, Ray const & ray, bool airborneGoal = false)
{
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 (! here.Valid || ! end.Valid || (!airborneGoal && std::fabs(end.Ground - goal.Z) > 2.5f)) return
{
};
// Issuing a takeoff spline is not proof of actual clearance. Handoffs
@@ -263,6 +264,7 @@ namespace ClientlessFlightPath
dz = std::min(0.0f, std::max(dz, here.Ground + cfg.Safety + 4 - from.Z));
}
}
if(airborneGoal)dz=std::clamp(dz,-run*cfg.Slope,run*cfg.Slope);
Point far
{
from.X + dx * run, from.Y + dy * run, from.Z + dz
@@ -308,19 +310,65 @@ namespace ClientlessFlightPath
std::vector<bool> UnderOverhang;
Point Landing;
bool LandingReady=false;
bool Aborted=false;
bool Aborted=false, Complete=false;
std::shared_ptr<SharedTrail> Replacement;
std::shared_ptr<SharedTrail> Replace(Point const& landing)
{
auto next=std::make_shared<SharedTrail>();
next->Landing=landing;next->LandingReady=true;
Replacement=next;Aborted=true;
return next;
}
bool BindLanding(Point& goal) const
{
if(!LandingReady || Aborted)return false;
goal=Landing;return true;
}
};
struct SharedJoin
{
bool Active=true;
std::size_t Target=std::numeric_limits<std::size_t>::max();
Memory Navigation;
};
struct SharedResult {Result Step;bool Waiting=false;};
inline SharedResult FollowTrail(Point const& current,Point const& goal,Settings const& settings,
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray,float lookaheadDistance=0)
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray,float lookaheadDistance=0,SharedJoin* join=nullptr)
{
if(trail.Aborted)return {};
if(!trail.LandingReady)return {{},true};
if(join && join->Active)
{
if(trail.Points.empty())return {{},true};
if(join->Target==std::numeric_limits<std::size_t>::max())
{
// Join a nearby remaining leg instead of replaying the leader's
// entire history after a taxi, recovery or delayed departure.
std::vector<std::size_t> candidates;
for(std::size_t i=index;i<trail.Points.size();++i)candidates.push_back(i);
std::sort(candidates.begin(),candidates.end(),[&](auto a,auto b){
float da=Distance(current,trail.Points[a]),db=Distance(current,trail.Points[b]);
return da==db?a<b:da<db;
});
for(std::size_t n=0;n<std::min<std::size_t>(8,candidates.size());++n)
{
auto candidate=candidates[n];Memory memory;
auto step=Next(current,trail.Points[candidate],settings,memory,sample,ray,true);
if(!step.Valid)continue;
join->Target=candidate;join->Navigation=memory;
if(Distance(current,trail.Points[candidate])>=0.7f)
{step.Landing=false;return {step,false};}
break;
}
if(join->Target==std::numeric_limits<std::size_t>::max())return {};
}
if(Distance(current,trail.Points[join->Target])>=0.7f)
{
auto step=Next(current,trail.Points[join->Target],settings,join->Navigation,sample,ray,true);
step.Landing=false;return {step,false};
}
index=join->Target;join->Active=false;
}
while(index<trail.Points.size() && Distance(current,trail.Points[index])<0.7f)++index;
if(index==trail.Points.size())return {{},true};
// Extend a running spline before its endpoint. A corner may only be
@@ -101,6 +101,11 @@ int main(){
travel.status.State=ClientlessTravelState::Arrived;leader.mounted=true;mgr.Follow(&a);assert(a.mounted);leader.mounted=false;a=Player(26); // retain a clean origin
// A ground-only bot waits safely when the leader gains excessive height.
leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0;
// A shared/action-owned catch-up corridor survives a leader landing ahead.
flight.status.State=ClientlessFlightState::Flying;flight.status.FollowingPlayer=false;
mgr.Observe(&a,1);leader.flying=false;auto previousLands=flight.lands;
assert(mgr.Follow(&a)&&flight.lands==previousLands);
flight.status.State=ClientlessFlightState::Arrived;mgr.Follow(&a);
// A native-capable follower uses flight, and safely lands after a kick.
flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a);
// A stale owned ground catch-up route cannot suppress a visible-phase
@@ -61,6 +61,7 @@ bool ClearRay(Player*,Point const&a,Point const&b){
'''+column+r'''
struct ClientlessFlightMgr{
std::unordered_map<unsigned,Record>_records;Surface surface{0,300,true};int approached=0;bool seek=false;
void Store(unsigned guid,Record&& record){_records[guid]=std::move(record);}
Surface Sample(Player*,Record&,float,float){return surface;}
bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&);
bool Prepare(Player*player,Record&record,std::string&error){auto origin=surface;bool following=false;
+9 -1
View File
@@ -46,7 +46,7 @@ struct Record {
struct {bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0},Target{800,0,0};float Travelled=0;uint32 Segments=0,Avoidances=0;std::string Detail;} Status;
Position LastPosition,Start,Segment{25,0,60};Settings Settings;
std::shared_ptr<int> Following;uint32 FormationDelay=0,TargetUpdated=0,Elapsed=0,SegmentElapsed=0,CheckTimer=0;
bool Moving=true,Returning=false,ApproachingDeparture=false,ActionOwned=false,SegmentUnderOverhang=false;uint32 DetachedRecoveryCount=0;Position Departure;Memory Memory;std::shared_ptr<SharedTrail> Published;std::vector<Position> Breadcrumbs;
bool Moving=true,Returning=false,ApproachingDeparture=false,ActionOwned=false,SegmentUnderOverhang=false;uint32 DetachedRecoveryCount=0;Position Departure;Memory Memory;std::shared_ptr<SharedTrail> Published=std::make_shared<SharedTrail>();std::vector<Position> Breadcrumbs;
};
enum class ClientlessTravelState {Moving,Arrived,Stuck};
struct Travel {struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;
@@ -96,6 +96,14 @@ int main(){
r.Elapsed=0;r.Departure=player;travel.status.State=ClientlessTravelState::Moving;
mgr.Update(50);assert(mgr.begins==0&&mgr.failures==1);
travel.status.State=ClientlessTravelState::Arrived;mgr.Update(50);assert(mgr.begins==1);
// A new airborne generation must publish its verified landing immediately
// and leave a replacement link for followers already holding the old trail.
r.ApproachingDeparture=false;r.Status.State=ClientlessFlightState::Flying;
r.Returning=false;r.Status.Segments=12;r.Status.Avoidances=12;r.DetachedRecoveryCount=0;
player.fly=true;auto old=r.Published;r.Status.Landing={800,0,0};
mgr.Update(50);assert(old->Aborted&&old->Replacement==r.Published);
assert(r.Published->LandingReady&&r.Published->Landing.X==800);
r.ApproachingDeparture=true;r.Status.State=ClientlessFlightState::Mounting;
// A stale Arrived flag cannot permit mounting away from the selected spot.
r.Departure.x=player.x+10;mgr.Update(50);assert(mgr.failures==2&&mgr.begins==1);
}
+5 -2
View File
@@ -26,9 +26,9 @@ namespace Movement{struct MoveSplineInit{Player*p;Point target;MoveSplineInit(Pl
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Record{
struct{ClientlessFlightState State=ClientlessFlightState::Flying;bool FollowingPlayer=true;Position Target,Landing;unsigned Segments=0;float Travelled=5000;std::string Detail;}Status;
bool Returning=false,Moving=false;unsigned Elapsed=1000,TargetUpdated=0,SegmentElapsed=0;Position Segment;
bool Returning=false,Moving=false;unsigned Elapsed=1000,TargetUpdated=0,SegmentElapsed=0;Position Segment,Start;unsigned SharedWaitSince=0;std::string AbortReason;
ClientlessFlightPath::Settings Settings;ClientlessFlightPath::Memory Memory;
std::shared_ptr<int>Following;std::vector<Position>Breadcrumbs;
std::shared_ptr<SharedTrail>Following;std::shared_ptr<SharedTrail>Published=std::make_shared<SharedTrail>();std::vector<Position>Breadcrumbs;
};
bool clear=true,valid=true;float ceiling=300;
bool ClearRay(Player*,Point const&,Point const&){return clear;}
@@ -54,6 +54,9 @@ int main(){
p.Relocate(110,20,30);r.Moving=true;assert(mgr.AdvanceFollowing(&p,r));assert(!r.Moving && p.stops==2); // holds in air, never dismounts at moving goal
assert(mgr.LandFollowing(&p));assert(!r.Status.FollowingPlayer && r.Status.Landing.z==0 && r.Status.Travelled==0);
assert(!mgr.Follow(&p,target,error)); // landing cannot be replaced by a new pursuit
r.Status.FollowingPlayer=false;r.Following=std::make_shared<SharedTrail>();
assert(mgr.LandFollowing(&p));assert(r.Returning&&!r.Following&&r.Start.z==0&&r.Breadcrumbs.empty());
r.Returning=false;
r.Status.FollowingPlayer=true;r.Status.State=ClientlessFlightState::Mounting;assert(mgr.LandFollowing(&p) && mgr.failures==1);
r.Status.State=ClientlessFlightState::Flying;r.Status.FollowingPlayer=true;valid=false;assert(mgr.LandFollowing(&p) && mgr.failures==2);
}
+24 -2
View File
@@ -6,26 +6,48 @@ source=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
a=source.index(' if(record.Following && !record.Returning)',source.index('bool ClientlessFlightMgr::Advance('))
b=source.index(' Point current =',a)
sync=source[a:b]
a=source.index('void ClientlessFlightMgr::Store(')
b=source.index('bool ClientlessFlightMgr::Begin(',a)
store=source[a:b]
code=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
using uint32=unsigned;
using namespace ClientlessFlightPath;
struct Position{float x=0,y=0,z=0;void Relocate(float a,float b,float c){x=a;y=b;z=c;}};
struct Record{std::shared_ptr<SharedTrail> Following;std::shared_ptr<SharedTrail> Published=std::make_shared<SharedTrail>();bool Returning=false;struct{Position Landing;std::string Detail;}Status;};
struct Position{float x=0,y=0,z=0;void Relocate(float a,float b,float c){x=a;y=b;z=c;}float GetExactDist(Position const&p)const{return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);}};
struct Record{std::shared_ptr<SharedTrail> Following;std::shared_ptr<SharedTrail> Published=std::make_shared<SharedTrail>();bool Returning=false;unsigned SharedWaitSince=0,Elapsed=0;std::size_t SharedIndex=0;SharedJoin Join;struct{unsigned MapId=0;Position Landing,Target;std::string Detail;}Status;};
int landings=0;void* player=nullptr;void LandFollowing(void*){++landings;}
bool Sync(Record& record){
''' + sync + r'''
return false;
}
struct ClientlessFlightMgr{std::unordered_map<unsigned,Record> _records;void Store(unsigned,Record&&);};
''' + store + r'''
int main(){
ClientlessFlightMgr manager;Record original;original.Status.Target.x=100;
auto previous=original.Published;manager.Store(1,std::move(original));
Record updated;updated.Status.Target.x=100;updated.Published->LandingReady=true;
auto successor=updated.Published;manager.Store(1,std::move(updated));
assert(previous->Aborted&&previous->Replacement==successor);
Record other;other.Status.Target.x=200;manager.Store(1,std::move(other));
assert(successor->Aborted&&!successor->Replacement);
Record r;r.Following=std::make_shared<SharedTrail>();r.Status.Landing.z=777;
assert(Sync(r)&&r.Status.Landing.z==777); // still preparing; no zero landing copied
r.Following->Landing={12,34,827.765625f};r.Following->LandingReady=true;
assert(!Sync(r)&&r.Status.Landing.x==12&&r.Status.Landing.y==34&&r.Status.Landing.z==827.765625f);
assert(r.Published->LandingReady&&r.Published->Landing.Z==827.765625f);
auto replacement=r.Following->Replace({56,78,100});
assert(!Sync(r)&&r.Following==replacement&&r.Status.Landing.z==100);
replacement->Complete=true;assert(!Sync(r)); // completed valid trail remains usable
Record waiting;waiting.Following=std::make_shared<SharedTrail>();
assert(Sync(waiting));waiting.Elapsed=120000;assert(Sync(waiting)&&landings==1);
waiting.Following->Aborted=true;assert(Sync(waiting)&&landings==2);
Settings cfg;auto sample=[](float,float){return Surface{827.765625f,1000,true,0};};
auto clear=[](Point const&,Point const&){return true;};
// The same shared object first exists while its leader is walking to takeoff.
@@ -0,0 +1,61 @@
"""Join a leader trail from a later taxi stop instead of replaying its origin."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <iostream>
using namespace ClientlessFlightPath;
int main(){Settings cfg;auto flat=[](float,float){return Surface{0,1000,true};};auto clear=[](Point const&,Point const&){return true;};
SharedTrail trail;trail.LandingReady=true;trail.Complete=true;trail.Landing={1200,0,0};
trail.Points={{0,0,20},{1025,0,60},{1050,0,60},{1100,0,60},{1200,0,0}};
SharedJoin join;std::size_t index=0;Point current{1000,0,0};bool connected=false;
for(unsigned i=0;i<100&&Distance(current,trail.Landing)>.1f;++i){
auto result=FollowTrail(current,trail.Landing,cfg,trail,index,flat,clear,0,&join);
assert(!result.Waiting&&result.Step.Valid);assert(result.Step.Next.X>=1000);
assert(Corridor(current,result.Step.Next,trail.Landing,cfg,flat,clear,true,result.Step.UnderOverhang));
current=result.Step.Next;if(!join.Active)connected=true;
}
assert(connected&&Distance(current,trail.Landing)<.1f&&index>=1);
// Original Winienwi geometry: the old first point is 2,088 yards behind
// the taxi stop and its direct segment intersects a raised surface.
Point taxi{-4170.55f,-4748.93f,126.786f};SharedTrail actual;
actual.LandingReady=true;actual.Landing={-4905.67f,-4232.75f,827.766f};
actual.Points={{-4863.52f,-6715.61f,24.22f},{-4850,-4750,540},actual.Landing};
auto hills=[](float x,float y){float ground=y< -6500?9.f:126.786f;
if(std::fabs(x+4175.523f)<2.5f&&std::fabs(y+4763.045f)<2.5f)ground=129.25f;
return Surface{ground,1000,true};};
assert(!Corridor(taxi,actual.Points[0],actual.Landing,cfg,hills,clear,true));
join={};index=0;auto connectedTaxi=FollowTrail(taxi,actual.Landing,cfg,actual,index,hills,clear,0,&join);
assert(connectedTaxi.Step.Valid&&join.Target==1&&connectedTaxi.Step.Next.Z>taxi.Z);
// Joining an airborne leg above a tower must still go around its platform.
SharedTrail tower;tower.LandingReady=true;tower.Landing={12,0,200};
tower.Points={{60,0,210},{12,0,210},tower.Landing};
auto platform=[](float x,float y){return Surface{std::hypot(x,y)<20?200.f:0.f,500,true,0.f};};
auto solid=[](Point const&a,Point const&b){for(unsigned i=0;i<=200;++i){auto p=Mix(a,b,i/200.f);if(std::hypot(p.X,p.Y)<20&&p.Z>=199.8f&&p.Z<=200.2f)return false;}return true;};
current={90,0,80};join={};index=0;
for(unsigned i=0;i<100&&Distance(current,tower.Landing)>.1f;++i){
auto step=FollowTrail(current,tower.Landing,cfg,tower,index,platform,solid,0,&join);
assert(step.Step.Valid&&!step.Waiting);current=step.Step.Next;
}
assert(Distance(current,tower.Landing)<.1f);
// No unvalidated shortcut through a tower or missing terrain.
join={};index=0;auto blocked=[](Point const&,Point const&){return false;};
assert(!FollowTrail({1000,0,0},trail.Landing,cfg,trail,index,flat,blocked,0,&join).Step.Valid);
auto missing=[](float,float){return Surface{};};join={};index=0;
assert(!FollowTrail({1000,0,0},trail.Landing,cfg,trail,index,missing,clear,0,&join).Step.Valid);
// A replacement publishes its verified destination immediately, even before
// any points are flown. Old readers can resolve to this generation.
auto old=std::make_shared<SharedTrail>();auto next=old->Replace({100,0,0});
assert(old->Aborted&&old->Replacement==next&&next->LandingReady&&next->Landing.X==100);
assert(next->Points.empty());next->Points={{25,0,20},{100,0,0}};next->Complete=true;
join={};index=0;auto step=FollowTrail({0,0,0},next->Landing,cfg,*next,index,flat,clear,0,&join);
assert(step.Step.Valid&&!step.Waiting);
std::cout<<"Shared route rejoin after taxi, bounded climb, clear corridor, blocked/missing terrain, replacement publication PASS\n";
}
'''
with tempfile.TemporaryDirectory(prefix="vcore-shared-rejoin-") as directory:
p=Path(directory);(p/"test.cpp").write_text(code)
subprocess.run(["c++","-std=c++17","-I"+str(root/"src/server/game/World"),str(p/"test.cpp"),"-o",str(p/"test")],check=True)
subprocess.run([str(p/"test")],check=True)