diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index d46a7101a3..dc9f43181e 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -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: diff --git a/src/server/game/World/AdventureGroupTravelMgr.cpp b/src/server/game/World/AdventureGroupTravelMgr.cpp index c8e99afa60..51bb11a12a 100644 --- a/src/server/game/World/AdventureGroupTravelMgr.cpp +++ b/src/server/game/World/AdventureGroupTravelMgr.cpp @@ -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()) diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index 77d9419262..b55e176c03 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -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(); + 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)", diff --git a/src/server/game/World/ClientlessFlightMgr.h b/src/server/game/World/ClientlessFlightMgr.h index c7242e6446..634816d1e3 100644 --- a/src/server/game/World/ClientlessFlightMgr.h +++ b/src/server/game/World/ClientlessFlightMgr.h @@ -44,6 +44,8 @@ class TC_GAME_API ClientlessFlightMgr std::shared_ptr Published=std::make_shared(); std::shared_ptr 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); diff --git a/src/server/game/World/ClientlessFlightPath.h b/src/server/game/World/ClientlessFlightPath.h index 55a67a0768..4223538e35 100644 --- a/src/server/game/World/ClientlessFlightPath.h +++ b/src/server/game/World/ClientlessFlightPath.h @@ -5,6 +5,7 @@ #include #include #include +#include #include // 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 UnderOverhang; Point Landing; bool LandingReady=false; - bool Aborted=false; + bool Aborted=false, Complete=false; + std::shared_ptr Replacement; + std::shared_ptr Replace(Point const& landing) + { + auto next=std::make_shared(); + 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::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::max()) + { + // Join a nearby remaining leg instead of replaying the leader's + // entire history after a taxi, recovery or delayed departure. + std::vector candidates; + for(std::size_t i=index;i(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::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_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; diff --git a/tests/clientless/test_flight_handoff.py b/tests/clientless/test_flight_handoff.py index cb691924a6..792ea386cf 100644 --- a/tests/clientless/test_flight_handoff.py +++ b/tests/clientless/test_flight_handoff.py @@ -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 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 Published;std::vector Breadcrumbs; + bool Moving=true,Returning=false,ApproachingDeparture=false,ActionOwned=false,SegmentUnderOverhang=false;uint32 DetachedRecoveryCount=0;Position Departure;Memory Memory;std::shared_ptr Published=std::make_shared();std::vector 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); } diff --git a/tests/clientless/test_group_flight_follow.py b/tests/clientless/test_group_flight_follow.py index a74159d040..4ba9269cd6 100644 --- a/tests/clientless/test_group_flight_follow.py +++ b/tests/clientless/test_group_flight_follow.py @@ -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_ptrFollowing;std::vectorBreadcrumbs; + std::shared_ptrFollowing;std::shared_ptrPublished=std::make_shared();std::vectorBreadcrumbs; }; 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(); + 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); } diff --git a/tests/clientless/test_shared_flight_landing.py b/tests/clientless/test_shared_flight_landing.py index f3808683f5..094c73322c 100644 --- a/tests/clientless/test_shared_flight_landing.py +++ b/tests/clientless/test_shared_flight_landing.py @@ -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 #include #include #include +#include +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 Following;std::shared_ptr Published=std::make_shared();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 Following;std::shared_ptr Published=std::make_shared();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 _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();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(); + 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. diff --git a/tests/clientless/test_shared_flight_rejoin.py b/tests/clientless/test_shared_flight_rejoin.py new file mode 100644 index 0000000000..b34162e687 --- /dev/null +++ b/tests/clientless/test_shared_flight_rejoin.py @@ -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 +#include +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();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)