Publish validated group flight landings and extend moving trail handoffs

This commit is contained in:
Nico committed 2026-11-02 10:03:52 +01:00
1 parent 3056063f12
commit ae0b7513d9
9 files changed
+301 -9

No files matched your search

+72
View File
@@ -1,3 +1,75 @@
# Aktuell 02.11.2026, 10:00: Shared-Flight-Landepunkt/Flugpausen korrigiert
Nutzer meldete Nachfolger kehren über Plattform um und fliegen stockend.
Rootcause belegt:159/161/166 abonnieren09:34:55 Leaderspur bevor Winienwi163
seine Abflugvorbereitung/Landesuche beendet. Begin kopierte nochleere Landing
Position. Spur später korrekt bis837.77 gefolgt, letzterSinkflug falsch geprüft,
09:37:49..52 Rückflug. Leader landete09:37:33 und tratInstanz2 bei.
Fix: SharedTrail publiziert Landing+LandingReady, Advance wartet/bindet den
bestätigtenLandepunkt statt früher Kopie. Geprüfter Lookahead fürFollower und
1500msSplinehandoff; keine ungeprüften Eckabkürzungen. AchtTests PASS,
Build-j10/Install PASS. Checkpoint2026-11-02-shared-flight/README.md.
Loader0 brauchte explizitenPopulationReload (zunächst nachBuildstart korrigiert).
SafeLogout09:49:23;14Restbots nach5Min dokumentiert, saubererShutdown,
ProzessendevorInstall. Kein Einzellogout erzwungen. NeustartPID258518 ready,
Ports8085/8087. Loader1 und PopulationReload restauriert.14Restbots Login,
sechs74/77/91/93/102/121 nachungültigemSchwebezustand nativgeborgen; übrige
nativeTaxi/Flugbewegungen ungestört. Rohzustände/Recoverybelege aufbewahrt.
FürLive-Nachprüfung alle10 offlineRose, nurderenalteInstanceVisit-/Handoff-
Zustände gesichertundentfernt.09Formation/10Raid6354_d0 wiederaktiviert.
Gruppe1 Leader163 undalle10 bestätigt. TempGM-Teleportgelöscht.
Beobachter vcore-adventure-raid-observer-20261102-1000.service weiteraktiv
bis11MEZ. Live-Retry10Uhr bleibt schoninSW anbekanntemGroundSafety-Fehler hängen:
MMAP taxi_2->stormwind_tradecenter komplett189.55yd, AusführungPathRejected,
wiederholteAbflugannäherungen unterbrochen.10:02 nochkeinneuerTurmanflug.
Keine erfolgreicheFollowerlandung/visuellflüssigeGesamtflügebehaupten.
Alle110Bots/Loader1 wiederaktiv, Gruppevollständig. Nächsteunabhängige
Baustelle ist lokalerGroundSafety/Recovery. Beobachterbis11weiteraktiv.
Flugfix separat lokal versionieren; kein Push. FrühereCommits2778cf9bdd/3056063f12.
AnderebenannteBaustellen und abgelehnteGruppennachbesetzung unverändert.
---
# Neuer Zehnertest am 02.11.2026, Gruppenbildung09 / Bastion10:00
Nutzer wollte Wiederholung nur der bekannten10 Bots. Alle159..167/176 wurden
nativ zur GoldenenRose teleportiert, alte Gruppen verlassen, Aruendas162 für
Teststart wiederbelebt. Slots09 groupformation the_gilded_rose,10 raid
instance_visit_at_6354_d0 für alle10 per nativer editplan gesetzt und20DB-Zeilen
validiert. Temporärer GM-Teleport nach Benutzung gelöscht/Cache reloaded.
Vollständige native Gruppe1 nachgewiesen: Leader Winienwi163, Mitglieder
159,160,161,162,163,164,165,166,167,176;2Tank/2Heal/6Damage, Corric dabei.
Kein globaler erzwungener Managerlauf, keine weiteren Tagespläne verändert.
Bestehende Frühaufbruchlogik unverändert: volle Gruppe kann fünf Minuten nach
Vollständigkeit schon vor10 abziehen. Nicht als Fehler oder geänderten10Slot
missverstehen. Vorzustände zeigen alte Fehlflüge; nachTeleport allebodennahSW,
Gruppenbildungsintent aktiv. Beobachterservice:
`vcore-adventure-raid-observer-20261102-1000.service`, read-only bis11MEZ,
Snapshot alle2Minuten und inkrementelle Logs ohne den alten Tail-Replayfehler.
Daten/Skript unter checkpoints/2026-11-02-1000-raid-test/.
Kein Build/Serverrestart für Einrichtung; Loader normal aktiv.
---
# Abschließende Testauswertung ohne Bastion-Cohort
Nutzer verlangte endgültige Auswertung unter Ausschluss der bekannten10 Bots.
Erstellt: doc/clientless/2026-11-02-instance-test-final.md. Reproduzierbare
Offline-Analyse evaluate_without_raid.py und final-without-raid.json im06Uhr-
Testcheckpoint.82 Bots (38A/44H),67Dungeon/15Raid,38 vor07 am fixen Treffpunkt
belegt (18A/20H, gleichwertige Inn-Aliase anerkannt; exakt gleiche Schlüssel33), kein Instanzeintritt. Schluss62 gruppiert in22 Gruppen,
20solo, nur eine vollständige Fünfergruppe. Rollen7Tank/11Heal/64Damage;
Population erlaubt keine vollständige Aufteilung aller82 in strikte Gruppen.
35 Bots RecoveryFailed;60 WaitingGroup,16 nochGruppenbildung und1Leisure.
Fünfergruppe26/41/43/45/72 ZielVersunkenerTempel scheiterte anReise; drei
ab07:56 im Timeout-Rest, keinabsolvierterDungeon. Logsammlerduplikate:
Rohereignishäufigkeiten nicht als unabhängige Versuche zählen. Bericht erläutert
Zeitlücke bis06:13, künstlichenManagerlauf und dass Flugfix nach08 außerhalb
Test liegt. Keine weiteren Source-/Serveränderungen für diese Auswertung.
---
# Aktuell 02.11.2026, 08:33 MEZ: erhöhter Landeanflug installiert und live bestätigt
Kostenmodell zuvor lokal committed: 2778cf9bdd, kein Push. Neuer Auftrag:
@@ -0,0 +1,42 @@
"""Read-only observation of the ten-bot retry until 11:00 CET."""
from pathlib import Path
import sys,os,re,json,time,datetime
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'2026-11-02-0600-group-test'))
import test_driver as d
BASE=Path(__file__).resolve().parent
END=datetime.datetime.fromisoformat('2026-11-02T11:00:00+01:00')
IDS=set(d.RAID)
def append(name,v):
with (BASE/name).open('a') as f:f.write(json.dumps(v,ensure_ascii=False)+'\n')
class Tail:
def __init__(self,path):
self.path=path;self.f=path.open('rb');self.inode=os.fstat(self.f.fileno()).st_ino;self.f.seek(0,2);self.pending=b''
def poll(self):
st=self.path.stat()
if st.st_ino==self.inode and st.st_size<self.f.tell():self.f.seek(0);self.pending=b''
data=self.f.read()
if st.st_ino!=self.inode:
self.f.close();self.f=self.path.open('rb');self.inode=st.st_ino;data+=self.f.read()
parts=(self.pending+data).split(b'\n');self.pending=parts.pop()
for line in parts:
s=line.decode(errors='replace')
if any(int(g) in IDS for g in re.findall(r'\b(?:guid|leader|member|target)=(\d+)\b',s)):
append('events.jsonl',{'time':d.now().isoformat(),'source':self.path.name,'line':s})
def snapshot():
for g in d.RAID:
for c in [f'clientless adventure status {g}',f'clientless actioninfo {g}',f'clientless flight status {g}',f'clientless interaction {g}']:d.command(c)
time.sleep(.6)
stamp=d.now();file='snapshot-'+stamp.strftime('%H%M%S')+'.log';(BASE/file).write_text(d.console(1000))
groups=d.query('Character','SELECT g.guid,g.leaderGuid,m.memberGuid,c.name FROM groups g JOIN group_member m ON m.guid=g.guid JOIN characters c ON c.guid=m.memberGuid WHERE g.guid IN(SELECT guid FROM group_member WHERE memberGuid IN(159,160,161,162,163,164,165,166,167,176)) ORDER BY g.guid,m.memberGuid')
append('snapshots.jsonl',{'time':stamp.isoformat(),'native_file':file,'groups':groups})
if __name__=='__main__':
logs=Path('/home/wow_admin/wow_cata_server/server/debug/bin');tails=[Tail(logs/'Server.log'),Tail(logs/'adventurebot-navigation.log')];next_snapshot=0
append('lifecycle.jsonl',{'time':d.now().isoformat(),'event':'start','end':END.isoformat()})
while d.now()<END:
try:
for t in tails:t.poll()
if time.monotonic()>=next_snapshot:snapshot();next_snapshot=time.monotonic()+120
except Exception as e:append('errors.jsonl',{'time':d.now().isoformat(),'error':str(e)})
time.sleep(2)
for t in tails:t.poll()
append('lifecycle.jsonl',{'time':d.now().isoformat(),'event':'finished'})
@@ -0,0 +1,75 @@
# Shared-flight landing and continuity fix — 02.11.2026
User observed followers turning away above the Bastion platform and repeated
pauses in flight. No group-refill policy changes are part of this fix.
## Root cause
09:34:53: leader Winienwi163 walks8yd to open takeoff column. His prepared
Flight Record already has Target but does not have a validated Landing yet.
09:34:55: followers166/159/161 attach to the leader trail BEFORE the leader's
landing-search result is published. Old Begin copied Status.Landing immediately,
which at this phase is the default zero Position. The shared pointer to the
trail survives preparation, but the copied landing never updated.
09:37:33: leader lands correctly at827.765625 and subsequently enters instance2.
09:37:49..52: followers reach the hover point837.765625. The zero-goal corridor
retains the8yd safety floor margin at the true touchdown and rejects descent.
Fail() therefore starts a checked breadcrumb return; this is not a successful
arrival or a deliberate new destination. Evidence root-cause-evidence.jsonl.
## Implementation
SharedTrail now publishes Landing and LandingReady. Begin does not copy the
leader's potentially uninitialised Record Position. Advance binds the validated
landing from the shared object, waiting for publication if preparation continues.
Followers retain a shared object even after the leader leaves for the instance.
Native attachment logs now include landingReady.
For continuity, handoff begins while up to1500ms remain rather than600ms.
Followers may advance to the next published endpoint before stopping at the old
endpoint, but only if the complete shortcut corridor is clear. Escape-leg corners
are retained. Aborted trails still fail closed. Explicit shared-reject diagnostics
include index, point count, abort flag and effective landing.
Eight targeted tests PASS. New regression reproduces the zero-goal terminal
failure, exercises the real Advance landing-binding prefix with delayed
publication, tests terminal descent, early running handoff, blocked corners,
escape legs and aborted trails. Existing ordinary/elevated flight, native handoff,
group follow, policy, landing search and terrain tests retained.
## Build and recovery
Build-j10 and install PASS. Safe logout requested09:49:23.822668; configuration
loader0 initially did not stop the cached loader. Explicit population reload then
confirmed enabled0 (loader-paused.log). This pause correction happened after the
build had begun and is documented rather than treating the earlier pause as
verified. Before installation, the full five-minute drain elapsed. Remaining14
bots, positions, native available flags and diagnostics were preserved in
final-remaining.json / final-remaining-native.log / last-available-remaining-flags.json.
Five-minute exception allowed clean server shutdown; no individual forced logout
or flight interruption was used to install. Process/tmux absence verified before
install. Fresh startup ready PID258518, ports8085/8087 verified.
Original ten-bot test evidence preserved. Only ten test bots' previous handoff and
instance-visit records were backed up and cleared to start a fresh run; all ten
natively teleported offline to the Golden Rose, reloaded, slots09formation and
10raid6354_d0 applied. Complete group1, leader163, all ten confirmed. Normal loader
restored (including explicit population reload); all14 recovery bots requested
login. Moving native taxi/flight actors left intact. Six unsupported stationary
actors74/77/91/93/102/121 received native capital recovery after login checks;
post-restart-native.log and recovery-capitals.log preserve findings/results.
Temporary GM teleport destination removed and cache reloaded. No production use.
Live result of the new10:00 run is recorded separately below when available.
## Live follow-up limitation
10:00 retry remains a complete ten-bot group led by163 with unchanged raid goal.
The initial route now stalls on the separately known ground safety issue: native
MMAP taxi_2 -> stormwind_tradecenter is complete (path189.55yd) but execution
returns PathRejected. Repeated departure approaches are interrupted. The group
has not reached the tower again as of10:02, so this run does NOT prove the final
follower descent or visually smooth flight. New landingReady=1 attachment and
short native flights were logged, but they are not a substitute for that proof.
All110 bots online, loader enabled1, original group still intact. Existing
observer remains active until11:00; no automatic chat report is promised.
The next independent blocker is the local ground safety/recovery behavior.
+24 -4
View File
@@ -499,17 +499,22 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
record.Status.State = ClientlessFlightState::Mounting;
record.Status.MountSpell = existingMount ? existingMount : spell;
record.Status.Detail = "waiting for native flying mount";
record.Published->Landing=PointOf(record.Status.Landing);
record.Published->LandingReady=true;
if(!following)if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID())
{
auto source=_records.find(group->GetLeaderGUID().GetCounter());
if(source!=_records.end() && source->second.Status.MapId==player->GetMapId() &&
source->second.Status.Target.GetExactDist(target)<1 && !source->second.Published->Aborted)
{
record.Following=source->second.Published;record.Status.Landing=source->second.Status.Landing;
// The leader may still be walking to takeoff and have no landing
// yet. Its published trail survives that preparation. Bind the
// verified landing lazily rather than copying an unset Position.
record.Following=source->second.Published;
std::vector<ObjectGuid> members;for(auto const& member:group->GetMemberSlots())if(member.guid!=group->GetLeaderGUID())members.push_back(member.guid);
std::sort(members.begin(),members.end());auto slot=std::find(members.begin(),members.end(),player->GetGUID())-members.begin();
record.FormationDelay=3500+uint32(slot)*600; // native shared corridor, staggered single-file departure
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter());
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u landingReady=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter(),record.Following->LandingReady?1u:0u);
}
}
if(airborne)
@@ -625,6 +630,17 @@ void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string con
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))
{
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)
{
@@ -665,9 +681,13 @@ 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);
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;
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(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true;
}
else
@@ -832,7 +852,7 @@ void ClientlessFlightMgr::Update(uint32 diff)
record.Moving = false;
Advance(player, record);
}
else if (!record.Returning && player->movespline->timeRemaining() <= 600 &&
else if (!record.Returning && player->movespline->timeRemaining() <= 1500 &&
Distance(PointOf(record.Segment), PointOf(record.Status.Landing)) > 0.1f)
{
// Replace a still-running spline from its native current position.
+18 -1
View File
@@ -305,15 +305,32 @@ namespace ClientlessFlightPath
{
std::vector<Point> Points;
std::vector<bool> UnderOverhang;
Point Landing;
bool LandingReady=false;
bool Aborted=false;
bool BindLanding(Point& goal) const
{
if(!LandingReady || Aborted)return false;
goal=Landing;return true;
}
};
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)
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray,float lookaheadDistance=0)
{
if(trail.Aborted)return {};
if(!trail.LandingReady)return {{},true};
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
// skipped if the NEW complete flight volume is verified; otherwise let
// the original checked corner finish. Never skip across an escape leg.
if(index+1<trail.Points.size() && Distance(current,trail.Points[index])<=lookaheadDistance)
{
bool escape=(index<trail.UnderOverhang.size() && trail.UnderOverhang[index]) ||
(index+1<trail.UnderOverhang.size() && trail.UnderOverhang[index+1]);
if(!escape && Corridor(current,trail.Points[index+1],goal,settings,sample,ray,true))++index;
}
auto next=trail.Points[index];
bool escape=index<trail.UnderOverhang.size() && trail.UnderOverhang[index];
return {{next,Corridor(current,next,goal,settings,sample,ray,true,escape),false,Distance(next,goal)<0.7f,escape},false};
+1 -1
View File
@@ -18,7 +18,7 @@ auto rays=[](Point const&a,Point const&b){
void run(Point p,bool escapeExpected){
Settings cfg;Memory m;m.Departed=true;Point goal{12,0,200};
bool climbed=false,escaped=false,crossed=false,descended=false;unsigned steps=0;
SharedTrail trail;
SharedTrail trail;trail.LandingReady=true;trail.Landing=goal;
for(;steps<100 && Distance(p,goal)>.1f;++steps){
auto n=Next(p,goal,cfg,m,terrain,rays);assert(n.Valid);
assert(Corridor(p,n.Next,goal,cfg,terrain,rays,false,n.UnderOverhang));
+2 -2
View File
@@ -24,7 +24,7 @@ struct Position {
float GetExactDist(Position const&p){return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);}
};
Point PointOf(Position const& p){return {p.x,p.y,p.z};}
struct Spline {int remaining=500; int timeRemaining()const{return remaining;}};
struct Spline {int remaining=1100; int timeRemaining()const{return remaining;}};
struct Player:Position {
Spline spline; Spline* movespline=&spline;
bool alive=true,fly=true,running=true;int stops=0;
@@ -75,7 +75,7 @@ int main(){
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._records[26].CheckTimer=0;player.spline.remaining=1900;
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;
+1 -1
View File
@@ -73,7 +73,7 @@ int main(){
auto highApproach=simulate({0,0,70},{1,0,0},flat,clear);
assert(highApproach.Arrived&&highApproach.Steps<20&&highApproach.Peak<=70);
// Convoy members replay exactly the published segments, retaining their own index.
SharedTrail trail;trail.Points={{20,0,30},{40,0,40}};std::size_t index=0;Settings settings;
SharedTrail trail;trail.LandingReady=true;trail.Landing={800,0,0};trail.Points={{20,0,30},{40,0,40}};std::size_t index=0;Settings settings;
auto shared=FollowTrail({0,0,0},{800,0,0},settings,trail,index,flat,clear);
assert(shared.Step.Valid&&shared.Step.Next.X==20&&index==0);
shared=FollowTrail({20,0,30},{800,0,0},settings,trail,index,flat,clear);
@@ -0,0 +1,66 @@
"""Regression: join while leader walks to takeoff; continuous safe trail handoff."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
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]
code=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <iostream>
#include <memory>
#include <string>
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;};
bool Sync(Record& record){
''' + sync + r'''
return false;
}
int main(){
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);
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.
SharedTrail trail;Point landing{0,0,0};std::size_t index=0;
assert(!trail.BindLanding(landing));
auto pending=FollowTrail({0,0,0},landing,cfg,trail,index,sample,clear);
assert(pending.Waiting&&!pending.Step.Valid);
// Leader completes admission later. Follower must bind this published landing,
// including after the leader record is gone/has entered the instance.
trail.Landing={-4905.67f,-4232.75f,827.765625f};trail.LandingReady=true;
Point above{trail.Landing.X,trail.Landing.Y,trail.Landing.Z+10};trail.Points={above,trail.Landing};
// Old bug: stale zero goal retains the 8-yard floor margin during touchdown.
assert(!Corridor(above,trail.Landing,landing,cfg,sample,clear,true));
assert(trail.BindLanding(landing));index=1;
auto final=FollowTrail(above,landing,cfg,trail,index,sample,clear);
assert(final.Step.Valid&&final.Step.Landing&&Distance(final.Step.Next,landing)<.01f);
// In open air the next segment is ready before reaching the prior endpoint.
SharedTrail cruise;cruise.LandingReady=true;cruise.Landing={100,0,0};
cruise.Points={{25,0,60},{50,0,60}};
auto flat=[](float,float){return Surface{0,300,true};};
index=0;auto moving=FollowTrail({15,0,60},cruise.Landing,cfg,cruise,index,flat,clear,15);
assert(moving.Step.Valid&&moving.Step.Next.X==50&&index==1);
// No shortcut through a blocked corner, and no early escape-leg shortcut.
auto obstacle=[](Point const&a,Point const&b){return !(a.X<20&&b.X>30);};
index=0;auto corner=FollowTrail({15,0,60},cruise.Landing,cfg,cruise,index,flat,obstacle,15);
assert(corner.Step.Valid&&corner.Step.Next.X==25&&index==0);
cruise.UnderOverhang={true,false};index=0;
auto escape=FollowTrail({15,0,60},cruise.Landing,cfg,cruise,index,flat,clear,15);
assert(escape.Step.Valid&&index==0);
trail.Aborted=true;assert(!trail.BindLanding(landing));
assert(!FollowTrail(above,landing,cfg,trail,index,sample,clear).Step.Valid);
std::cout<<"Shared flight: late landing publication, terminal descent, early handoff, blocked/escape corners and abort PASS\n";
}
'''
with tempfile.TemporaryDirectory() 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)