Allow validated vertical approaches to elevated flight landings
This commit is contained in:
1 parent
2778cf9bdd
commit
3056063f12
9 files changed
+344
-11
No files matched your search
@@ -0,0 +1,72 @@
|
||||
# Testauswertung: 02.11.2026, Gruppenbildung 06:00 / Instanzen 07:00
|
||||
|
||||
Erstellt: 02.11.2026 08:00:09 CET. Geplanter Auswertungszeitpunkt: 08:00 UTC+01:00.
|
||||
|
||||
- Geprüfte Testbots: 92. Unter Stufe 15 ausgenommen: 18.
|
||||
- Feste Treffpunkte: Allianz Goldene Rose in Sturmwind; Horde Broken Tusk in Orgrimmar.
|
||||
- Status-Snapshots: 22; ausgewertete eindeutige Logereignisse: 16592.
|
||||
- Erste erfasste Statusstichprobe: 2026-11-02T06:13:38.615403+01:00; frühere Ereignisse sind nicht vollständig erfasst.
|
||||
- Bots mit belegter Ankunft am festgelegten Formationstreffpunkt: 40.
|
||||
- Bots mit protokolliertem tatsächlichem Instanzbeitritt: 0.
|
||||
- Snapshots ohne laufenden Testserver: 0.
|
||||
|
||||
## Bekannte Zehnergruppe: Bastion des Zwielichts, normal
|
||||
|
||||
- Maximal gemeinsam in einer nativen Gruppe beobachtet: 9/10.
|
||||
- Alle zehn gemeinsam nachgewiesen: nein; durch die Stichproben nicht nachgewiesen.
|
||||
- Bots mit Instanzplanung: 9/10; mit tatsächlichem Beitritt zu Karte 671 im Modus 0: 0/10.
|
||||
|
||||
| GUID | Name | Rolle | Bastion betreten | Letzter nativer Status |
|
||||
|---:|---|---|---|---|
|
||||
| 159 | Renren | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / instance visit waiting for safe native state |
|
||||
| 160 | Marndra | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 161 | Marnda | healer | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 162 | Aruendas | healer | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 163 | Winienwi | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 164 | Taliaarnora | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 165 | Brynda | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 166 | Corric | damage | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / waiting for a suitable group / native browser queue |
|
||||
| 167 | Ragnorli | tank | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
| 176 | Ashaelri | tank | nicht nachgewiesen | raid / instance_visit_at_6354_d0 / travelling to instance_visit_at_6354_d0 |
|
||||
|
||||
## Übernahme der Anführerroute
|
||||
|
||||
- Protokollierte Routenübernahmen mit Journey-Seed: 78.
|
||||
- Gleicher zuletzt beobachteter Anführer-Seed: 72; abweichend: 4; Anführer-Seed im Beobachtungsfenster nicht bekannt: 2.
|
||||
- Ein abweichender letzter Seed erfordert Prüfung der konkreten Reise und des Zeitpunkts; diese Zählung allein beweist keinen Fehler.
|
||||
|
||||
## Fehler und Reiseereignisse
|
||||
|
||||
- GROUP_MERGE: 28.
|
||||
- GROUP_MANAGER_RUN: 9.
|
||||
- GROUP_EARLY_DEPART: 15.
|
||||
- PORTAL_USE_START: 36.
|
||||
- RECOVERY_FAILED: 117.
|
||||
- INSTANCE_TRAVEL_RETRY: 11.
|
||||
- INSTANCE_ENTRY_DENIED: 0.
|
||||
|
||||
Häufigste protokollierte Fehler:
|
||||
|
||||
- RECOVERY_FAILED: route-local-PathRejected (26 Ereignisse).
|
||||
- RECOVERY_FAILED: free-flight-unavailable: safely returned after abort: no safe bounded flight corridor; holding position mounted (15 Ereignisse).
|
||||
- RECOVERY_FAILED: free-flight-unavailable: departure-unavailable: ground approach interrupted (12 Ereignisse).
|
||||
- RECOVERY_FAILED: free-flight-unavailable: no free solid landing with a validated target approach (12 Ereignisse).
|
||||
- INSTANCE_TRAVEL_RETRY: siehe Rohlog (11 Ereignisse).
|
||||
- RECOVERY_FAILED: deferred-start-movement-failed (9 Ereignisse).
|
||||
- RECOVERY_FAILED: deferred-start-no-progress (7 Ereignisse).
|
||||
- RECOVERY_FAILED: final-local-PathRejected (6 Ereignisse).
|
||||
- RECOVERY_FAILED: deferred-start-endpoint-replan (6 Ereignisse).
|
||||
- RECOVERY_FAILED: wander-ended-Arrived (4 Ereignisse).
|
||||
- RECOVERY_FAILED: deferred-start-attempts-exhausted (4 Ereignisse).
|
||||
- RECOVERY_FAILED: recorded group road rejected by safety adapter (4 Ereignisse).
|
||||
|
||||
## Grenzen der Auswertung
|
||||
|
||||
- Ausgewertet werden technische Zustände und native Logereignisse; keine visuelle Gameplay-Abnahme und keine Bestätigung von Bosskämpfen.
|
||||
- Ein Reiseplan ist kein Ankunftsnachweis. Fehlende Ereignisse werden als nicht nachgewiesen ausgewiesen.
|
||||
- Die Gruppenmitgliedschaft und Zustände werden alle fünf Minuten stichprobenartig gelesen; kurzlebige Zwischenzustände können fehlen.
|
||||
- Charakterpositionen aus DB-Snapshots sind zuletzt gespeicherte Positionen. Live-Reisepositionen stehen in den TEMP_TRAVEL-Ereignissen.
|
||||
- Der Beobachter repariert, teleportiert oder startet den Server nicht. Ausfälle werden dokumentiert.
|
||||
- Letzter Snapshot: 2026-11-02T07:59:25.206244+01:00.
|
||||
|
||||
Rohdaten: doc/clientless/checkpoints/2026-11-02-0600-group-test/ (manifest.json, snapshots.jsonl, events.jsonl, snapshot-*.log).
|
||||
@@ -1,3 +1,38 @@
|
||||
# Aktuell 02.11.2026, 08:33 MEZ: erhöhter Landeanflug installiert und live bestätigt
|
||||
|
||||
Kostenmodell zuvor lokal committed: 2778cf9bdd, kein Push. Neuer Auftrag:
|
||||
Steigungsbudget statt fester30-Yards-Grenze; senkrechter Aufstieg im laufenden
|
||||
Flug, sichere seitliche Flucht unter Plattform, Anflug von oben. Implementiert
|
||||
in ClientlessFlightPath.h und FlightMgr; Details/Belege:
|
||||
`checkpoints/2026-11-02-elevated-flight/README.md`.
|
||||
Sieben gezielte Tests PASS, Build-j10 und Install PASS. Bestehender
|
||||
Abflug-Extraktionstest scheitert bereits auf HEAD; Baselinefehler archiviert.
|
||||
Frischer Server ready PID249564, Ports8085/8087 und Prozess geprüft.
|
||||
Finaler Build mit Loader0 und erfolgreichem SafeLogout bis0 Sessions; keine
|
||||
5-Minuten-Ausnahme/kein erzwungener Einzellogout. Erster Build traf bereits
|
||||
beendeten Server an, Ursache dieses alten Endes nicht geklärt.
|
||||
|
||||
LIVE: Renren159 flog isoliert ab taxi_663 zur Bastion-Plattform. Senkrechter
|
||||
Aufstieg bei ca. (-4817.58,-4266.23) bis837.77, oberhalb der Plattform angeflogen,
|
||||
08:27:42 sicher auf827.77 gelandet; FLIGHT_LANDED error0.00. Keine Bestätigung
|
||||
von Raidbeitritt. Seitliches Ausweichen unter Plattform synthetisch getestet.
|
||||
Temporäres Profil exakt restauriert, LuaReload OK, Scheduleon, temporären
|
||||
GM-Teleport gelöscht. Loader1 restauriert, SafeLogoutresume, ursprüngliche110
|
||||
Bots wieder online. Acht alte schwebende Raidbots vor Login nachSW geborgen;
|
||||
zusätzlich Braneven121 nach nativer Fehlzustandsprüfung nachORG versetzt,
|
||||
Position16.047/floor16.088/flying0/falling0 bestätigt. Recoverylisten und
|
||||
Rohdiagnosen im Checkpoint erhalten. Normale Tagespläne laufen wieder.
|
||||
|
||||
Nutzer hat Nachbesetzungsänderung ausdrücklich ABGELEHNT: Elanora wurde manuell
|
||||
für Test entfernt, dadurch entstandene Neunergruppe wird akzeptiert. Gruppen-
|
||||
manager/Frühaufbruch für diesen Auftrag unverändert lassen.
|
||||
08:00-Bericht erfolgreich um08:00:09 erstellt: 40/92 Treffpunktankünfte,
|
||||
kein Instanzeintritt, maximal9/10 Cohort. Bericht ist vor dem Flugfix entstanden.
|
||||
`doc/clientless/2026-11-02-0800-test-report.md`. Andere bekannte Baustellen
|
||||
(Ground-Safety, Treffpunktfallback, Kolonnenbildung) nicht als behoben darstellen.
|
||||
|
||||
---
|
||||
|
||||
# Aktueller Stand 02.11.2026, 06:21 MEZ
|
||||
|
||||
Das Entfernungskostenmodell ist gebaut und installiert. Acht gezielte Tests sind
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Elevated flight navigation — 2026-11-02
|
||||
|
||||
User correction: no fixed 30-yard height threshold. Near a raised destination,
|
||||
compare required ascent with horizontal distance × configured slope. Preserve
|
||||
ordinary slope-limited flight but explicitly permit a vertical aerial escape.
|
||||
Do not modify group refill behavior; the missing raid member followed a manual
|
||||
removal and is accepted for this performative test.
|
||||
|
||||
Implementation in ClientlessFlightPath.h:
|
||||
- Within 100 horizontal yards, an insufficient slope budget or an obstructed
|
||||
ascending direct approach selects an elevated approach.
|
||||
- Validate full descent from landing+10, a full open climb column, and the full
|
||||
above-platform crossing before committing. At most 65 column candidates at
|
||||
radii 0/25/50/100/150 around the current position.
|
||||
- Stable Escape/Climb/Cross/Descend phases prevent height oscillation. Vertical
|
||||
climb is exempt from ordinary slope; normal cruising remains slope limited.
|
||||
- A blocked column can be reached laterally in clear air below an overhang.
|
||||
The exception still checks the bare terrain/water floor, ceiling and all
|
||||
collision-volume rays. Unknown floor fails closed. It does not tunnel through
|
||||
an ADT hill or bypass platform collision.
|
||||
- Early normal climb reserves ascent distance for raised destinations.
|
||||
- Published group trails carry per-segment overhang permissions; followers and
|
||||
moving-segment rechecks retain the same collision checks.
|
||||
|
||||
Seven targeted tests pass (individual logs here): elevated geometry, ordinary flight,
|
||||
spline handoff, moving group follow, flight policy, landing search and native terrain fallback. Elevated tests
|
||||
cover a solid thin platform, starting directly beneath it, a 10-yard height change
|
||||
with insufficient horizontal slope budget, a blocked direct approach with enough
|
||||
slope budget, rejected closed/low-ceiling routes, terrain-floor rejection and
|
||||
shared follower segment validation.
|
||||
|
||||
Existing test_flight_departure.py fails with unordered_map::at both on HEAD and
|
||||
this change: the extraction harness omits the newer preparation phase when
|
||||
asserting admission behavior. Baseline failure is preserved separately; not a
|
||||
new production regression. test_flight_handoff.py doubles were updated to the
|
||||
current production record and owner/replan dependencies.
|
||||
|
||||
Initial build/install passed. The server was already offline before the build;
|
||||
no process/tmux session, so no safe-logout command could be delivered. Preserve
|
||||
residual online list (110 bots) and prior raw test data. Server exit cause unknown.
|
||||
First restart ready PID248139, ports8085/8087 checked. Loader kept paused.
|
||||
Initial native flight attempt was taken over by a normal day plan before schedule
|
||||
disable; safe return requested, then successful safe logout with zero sessions.
|
||||
Final build/install is tracked separately. Temporary Renren profile isolation and
|
||||
GM teleport destination must be restored/removed after the live experiment.
|
||||
|
||||
## Final result
|
||||
|
||||
Final -j10 build and install passed (build-final.log/install-final.log). Before
|
||||
shutdown, native safe logout confirmed zero pending/zero sessions; no five-minute
|
||||
exception or forced logout was needed. Final startup fresh ready PID249564,
|
||||
ports8085/8087 and live process verified.
|
||||
|
||||
Live isolated flight: Renren159, native taxi_663 departure (-4170.55,-4748.93,
|
||||
126.787), destination portal_at_6354_source. Profile temporarily disabled ordinary
|
||||
behavior, backed up and restored byte-for-byte; no fake travel edge was added.
|
||||
At 08:27:23, vertical climb from (-4817.58,-4266.23,~619) was selected and reached
|
||||
837.77. Above-platform crossing ended at (-4905.67,-4232.75,837.77), then vertical
|
||||
touchdown at 827.77. FLIGHT_LANDED at 08:27:42: segments=64 avoidances=3,
|
||||
travelled=1279.85 error=0.00. This proves the real tower landing, not raid entry.
|
||||
Under-platform lateral escape is covered by geometry tests, not separately proven
|
||||
in-game. Raw live-flight-evidence.log and landing-confirmed.log retain proof.
|
||||
|
||||
The profile was restored byte-for-byte, native Lua reload succeeded, schedule on.
|
||||
Temporary GM teleport destination deleted and native game_tele cache reloaded.
|
||||
110 original bots are back online; loader restored to1, safe-logout block resumed.
|
||||
Eight formerly hovering raid members were recovered natively to Stormwind before
|
||||
login; Renren landed safely on the platform. All110 login/movement/flight states
|
||||
were sampled. Additional unsupported Braneven121 at z93.962 (floor24.148), failed
|
||||
flight, was natively teleported to Orgrimmar; final z16.047/floor16.088, flying=0,
|
||||
falling=0 verified. 112/132 had failed flight records but were supported on ground
|
||||
and were left in place. Recovery logs retained; no production access/push.
|
||||
|
||||
Original 06/07 test report was generated successfully at08:00:09: 40/92 arrivals
|
||||
at required meeting points, zero recorded instance entries, max9/10 cohort.
|
||||
It predates this fix. Group refill logic was explicitly rejected by user and
|
||||
remains unchanged.
|
||||
@@ -208,9 +208,12 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f
|
||||
float water = map->GetWaterLevel(player->GetPhaseShift(), x, y);
|
||||
if (std::isfinite(water) && water > ground) ground = water;
|
||||
float ceiling = map->GetTerrain()->GetMaxHeight(player->GetPhaseShift(), map->GetId(), x, y);
|
||||
float terrainFloor=map->GetTerrain()->GetStaticHeight(player->GetPhaseShift(),map->GetId(),x,y,MAX_HEIGHT,false,MAX_HEIGHT);
|
||||
if(std::isfinite(water) && water>terrainFloor)terrainFloor=water;
|
||||
return
|
||||
{
|
||||
ground, ceiling, std::isfinite(ground) && std::isfinite(ceiling) && ground > INVALID_HEIGHT + 1 && ceiling > ground + 5
|
||||
ground, ceiling, std::isfinite(ground) && std::isfinite(ceiling) && ground > INVALID_HEIGHT + 1 && ceiling > ground + 5,
|
||||
std::isfinite(terrainFloor) && terrainFloor>INVALID_HEIGHT+1?terrainFloor:std::numeric_limits<float>::infinity()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -713,13 +716,18 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
return false;
|
||||
}
|
||||
record.Published->Points.push_back(next.Next);
|
||||
record.Published->UnderOverhang.push_back(next.UnderOverhang);
|
||||
record.SegmentUnderOverhang=next.UnderOverhang;
|
||||
record.Moving = true;
|
||||
record.SegmentElapsed = 0;
|
||||
++ record.Status.Segments;
|
||||
record.Status.Avoidances += next.Avoiding ? 1 : 0;
|
||||
if (handoff) ++record.Status.Handoffs;
|
||||
bool const vertical=Horizontal(current,next.Next)<1.0f && std::fabs(next.Next.Z-current.Z)>1.0f;
|
||||
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing && vertical ? "vertical landing fallback" :
|
||||
record.Status.Detail = next.UnderOverhang ? "elevated approach lateral escape" :
|
||||
record.Memory.Elevated==Memory::ElevatedPhase::Climb ? "elevated approach vertical climb" :
|
||||
record.Memory.Elevated==Memory::ElevatedPhase::Cross ? "elevated approach above platform" :
|
||||
next.Avoiding ? "obstacle avoidance" : next.Landing && vertical ? "vertical landing fallback" :
|
||||
next.Landing ? "landing approach" : vertical && next.Next.Z>current.Z ? "vertical takeoff fallback" :
|
||||
vertical ? "vertical descent fallback" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" :
|
||||
next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
|
||||
@@ -818,7 +826,7 @@ void ClientlessFlightMgr::Update(uint32 diff)
|
||||
return Sample(player, record, x, y);
|
||||
};
|
||||
// Recheck the remaining corridor as moving GO geometry changes.
|
||||
if (! Corridor(PointOf(* player), PointOf(record.Segment), PointOf(record.Returning ? record.Start : record.Status.Landing), record.Settings, sample, ray, true))
|
||||
if (! Corridor(PointOf(* player), PointOf(record.Segment), PointOf(record.Returning ? record.Start : record.Status.Landing), record.Settings, sample, ray, true, record.SegmentUnderOverhang && !record.Returning))
|
||||
{
|
||||
player->StopMoving();
|
||||
record.Moving = false;
|
||||
|
||||
@@ -48,6 +48,7 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
ClientlessFlightPath::Settings Settings;
|
||||
ClientlessFlightPath::Memory Memory;
|
||||
Position Start, Segment, LastPosition;
|
||||
bool SegmentUnderOverhang = false;
|
||||
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
|
||||
bool Moving = false, Returning = false;
|
||||
bool ApproachingDeparture = false;
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace ClientlessFlightPath
|
||||
{
|
||||
float Ground = 0, Ceiling = 0;
|
||||
bool Valid = false;
|
||||
// Bare terrain/water floor, without overhead VMAP/GO platforms. Unknown
|
||||
// values keep the normal highest-surface restriction (fail closed).
|
||||
float TerrainFloor = std::numeric_limits<float>::infinity();
|
||||
};
|
||||
struct Settings
|
||||
{
|
||||
@@ -29,11 +32,15 @@ namespace ClientlessFlightPath
|
||||
float LowerTerrainDistance = 0;
|
||||
float HeadingX = 1, HeadingY = 0;
|
||||
bool Departed = false;
|
||||
enum class ElevatedPhase { None, Escape, Climb, Cross, Descend };
|
||||
ElevatedPhase Elevated = ElevatedPhase::None;
|
||||
Point Column, Hover, ElevatedGoal;
|
||||
};
|
||||
struct Result
|
||||
{
|
||||
Point Next;
|
||||
bool Valid = false, Avoiding = false, Landing = false;
|
||||
bool UnderOverhang = false;
|
||||
};
|
||||
using Sample = std::function < Surface(float, float) >;
|
||||
using Ray = std::function < bool(Point const &, Point const &) >;
|
||||
@@ -52,7 +59,7 @@ namespace ClientlessFlightPath
|
||||
a.X +(b.X - a.X) * t, a.Y +(b.Y - a.Y) * t, a.Z +(b.Z - a.Z) * t
|
||||
};
|
||||
}
|
||||
inline bool Corridor(Point const & from, Point const & to, Point const & goal, Settings const & cfg, Sample const & sample, Ray const & ray, bool departure = false)
|
||||
inline bool Corridor(Point const & from, Point const & to, Point const & goal, Settings const & cfg, Sample const & sample, Ray const & ray, bool departure = false, bool underOverhang = false)
|
||||
{
|
||||
int const count = std::max(1, int(std::ceil(Distance(from, to) / 5)));
|
||||
Surface const origin = sample(from.X, from.Y);
|
||||
@@ -66,7 +73,11 @@ namespace ClientlessFlightPath
|
||||
if (departure) margin = std::min(margin, std::max(0.0f, from.Z - origin.Ground) + Horizontal(from, p) * std::min(0.3f, std::max(0.0f, to.Z - from.Z) / std::max(1.0f, Horizontal(from, to)) * 0.75f));
|
||||
margin = std::min(margin, std::max(0.0f, Horizontal(p, goal) * 0.2f));
|
||||
Surface s = sample(p.X, p.Y);
|
||||
if (! s.Valid || ! std::isfinite(s.Ground) || ! std::isfinite(s.Ceiling) || p.Z + 0.5f < s.Ground + margin || p.Z + 5 > s.Ceiling) return false;
|
||||
// MAX_HEIGHT reports the TOP of a platform, not the support floor
|
||||
// beneath an airborne bot. Only a checked elevated escape may use
|
||||
// air below that surface; all volume rays still apply.
|
||||
if (! s.Valid || ! std::isfinite(s.Ground) || ! std::isfinite(s.Ceiling) ||
|
||||
p.Z + 0.5f < (underOverhang?std::min(s.Ground,s.TerrainFloor):s.Ground) + margin || p.Z + 5 > s.Ceiling) return false;
|
||||
}
|
||||
// A volume, not one center ray. Keep the lower rays above foot level so
|
||||
// a valid ground departure/touchdown does not intersect the ground plane.
|
||||
@@ -113,6 +124,57 @@ namespace ClientlessFlightPath
|
||||
if (! here.Valid || ! end.Valid || std::fabs(end.Ground - goal.Z) > 2.5f) return
|
||||
{
|
||||
};
|
||||
using Phase = Memory::ElevatedPhase;
|
||||
if (memory.Elevated != Phase::None && Distance(memory.ElevatedGoal,goal)>0.1f)
|
||||
memory.Elevated=Phase::None;
|
||||
Point hover{goal.X,goal.Y,goal.Z+10};
|
||||
bool steep = goal.Z-from.Z > distance*cfg.Slope;
|
||||
bool blockedAscent = memory.Departed && distance<=100 && !steep && goal.Z>from.Z &&
|
||||
!Corridor(from,goal,goal,cfg,sample,ray);
|
||||
if (memory.Departed && memory.Elevated==Phase::None && distance<=100 &&
|
||||
(steep || blockedAscent))
|
||||
{
|
||||
// Validate the whole manoeuvre before committing: escape laterally,
|
||||
// climb in open air, cross ABOVE the platform, then touch down.
|
||||
// A diagonal shortcut through the platform is never substituted.
|
||||
float best=std::numeric_limits<float>::infinity();
|
||||
Point column;
|
||||
if (!Corridor(hover,goal,goal,cfg,sample,ray)) return {};
|
||||
for(float radius : {0.f,25.f,50.f,100.f,150.f})
|
||||
for(unsigned i=0;i<(radius==0?1u:16u);++i)
|
||||
{
|
||||
float a=float(i)*6.283185307f/16;
|
||||
Point low{from.X+radius*std::cos(a),from.Y+radius*std::sin(a),from.Z};
|
||||
Point high{low.X,low.Y,hover.Z};
|
||||
if(!Corridor(from,low,goal,cfg,sample,ray,false,true) ||
|
||||
!Corridor(low,high,goal,cfg,sample,ray) ||
|
||||
!Corridor(high,hover,goal,cfg,sample,ray)) continue;
|
||||
float score=radius+Distance(low,high)+Distance(high,hover);
|
||||
if(score<best){best=score;column=low;}
|
||||
}
|
||||
if(!std::isfinite(best))return {};
|
||||
memory.Column=column;memory.Hover=hover;memory.ElevatedGoal=goal;
|
||||
memory.Elevated=Distance(from,column)>0.1f?Phase::Escape:Phase::Climb;
|
||||
}
|
||||
if(memory.Elevated!=Phase::None)
|
||||
{
|
||||
Point waypoint;
|
||||
for(unsigned transition=0;transition<4;++transition)
|
||||
{
|
||||
waypoint=memory.Elevated==Phase::Escape?memory.Column:
|
||||
memory.Elevated==Phase::Climb?Point{memory.Column.X,memory.Column.Y,memory.Hover.Z}:
|
||||
memory.Elevated==Phase::Cross?memory.Hover:goal;
|
||||
if(Distance(from,waypoint)>0.1f)break;
|
||||
if(memory.Elevated==Phase::Descend)return {goal,true,false,true};
|
||||
memory.Elevated=memory.Elevated==Phase::Escape?Phase::Climb:
|
||||
memory.Elevated==Phase::Climb?Phase::Cross:Phase::Descend;
|
||||
}
|
||||
bool escape=memory.Elevated==Phase::Escape;
|
||||
// Check the full remaining leg, not just the next 25 yards.
|
||||
if(!Corridor(from,waypoint,goal,cfg,sample,ray,false,escape))return {};
|
||||
Point next=Mix(from,waypoint,std::min(1.f,cfg.Step/std::max(0.1f,Distance(from,waypoint))));
|
||||
return {next,true,false,memory.Elevated==Phase::Descend,escape};
|
||||
}
|
||||
// A short, low final descent need not open another horizontal approach.
|
||||
// Validate the entire landing volume, including obstacles and terrain.
|
||||
if (memory.Departed && from.Z>=goal.Z && from.Z-goal.Z<=12 &&
|
||||
@@ -150,6 +212,9 @@ namespace ClientlessFlightPath
|
||||
}
|
||||
std::sort(heights.begin(), heights.end());
|
||||
float desired = heights[2] + cfg.Clearance;
|
||||
// Reserve enough ascent distance for elevated destinations rather than
|
||||
// following low terrain until the tower is immediately in front of us.
|
||||
if(goal.Z>from.Z)desired=std::max(desired,goal.Z+10-distance*cfg.Slope*0.8f);
|
||||
// ignore short radical peaks in nominal height
|
||||
bool const approach = distance < std::max(150.0f,(from.Z - goal.Z) / cfg.Slope + cfg.Step);
|
||||
if (approach) desired = goal.Z + distance * cfg.Slope * 0.65f;
|
||||
@@ -239,6 +304,7 @@ namespace ClientlessFlightPath
|
||||
struct SharedTrail
|
||||
{
|
||||
std::vector<Point> Points;
|
||||
std::vector<bool> UnderOverhang;
|
||||
bool Aborted=false;
|
||||
};
|
||||
struct SharedResult {Result Step;bool Waiting=false;};
|
||||
@@ -249,7 +315,8 @@ namespace ClientlessFlightPath
|
||||
while(index<trail.Points.size() && Distance(current,trail.Points[index])<0.7f)++index;
|
||||
if(index==trail.Points.size())return {{},true};
|
||||
auto next=trail.Points[index];
|
||||
return {{next,Corridor(current,next,goal,settings,sample,ray,true),false,Distance(next,goal)<0.7f},false};
|
||||
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};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Exercise elevated landings with production geometry and solid overhangs."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
harness=r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
using namespace ClientlessFlightPath;
|
||||
// Tower platform at z=200: collision surface is thin, open air beneath its lip.
|
||||
auto terrain=[](float x,float y){return Surface{std::hypot(x,y)<20?200.f:0.f,500,true,0.f};};
|
||||
auto rays=[](Point const&a,Point const&b){
|
||||
for(int 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;
|
||||
if(std::hypot(p.X,p.Y)<5 && p.Z<200)return false;
|
||||
}return true;
|
||||
};
|
||||
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;
|
||||
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));
|
||||
if(m.Elevated==Memory::ElevatedPhase::Climb){assert(Horizontal(p,n.Next)<.01f);climbed=true;}
|
||||
if(n.UnderOverhang){assert(std::fabs(p.Z-n.Next.Z)<.01f);escaped=true;}
|
||||
if(m.Elevated==Memory::ElevatedPhase::Cross){assert(n.Next.Z>=209.9f);crossed=true;}
|
||||
if(m.Elevated==Memory::ElevatedPhase::Descend){assert(Horizontal(p,n.Next)<.01f);descended=true;}
|
||||
trail.Points.push_back(n.Next);trail.UnderOverhang.push_back(n.UnderOverhang);
|
||||
std::size_t index=trail.Points.size()-1;
|
||||
auto shared=FollowTrail(p,goal,cfg,trail,index,terrain,rays);
|
||||
assert(shared.Step.Valid && shared.Step.UnderOverhang==n.UnderOverhang);
|
||||
p=n.Next;
|
||||
}
|
||||
assert(Distance(p,goal)<.1f && steps<100 && climbed && crossed && descended);
|
||||
assert(!escapeExpected || escaped);
|
||||
}
|
||||
int main(){
|
||||
run({90,0,80},false); // high target: climb before approaching the wall
|
||||
run({12,0,100},true); // directly UNDER platform: move out before rising
|
||||
run({25,0,190},false); // only 10 yards high, slope budget is 7.8: no fixed threshold
|
||||
// Reachable diagonal approach intersects platform underside. The collision
|
||||
// query, rather than a height threshold, must trigger the elevated manoeuvre.
|
||||
Memory m;m.Departed=true;Settings cfg;
|
||||
auto n=Next({40,0,190},{12,0,200},cfg,m,terrain,rays);
|
||||
assert(n.Valid && m.Elevated!=Memory::ElevatedPhase::None);
|
||||
// A blocked ascent/crossing must fail, never tunnel through a platform.
|
||||
auto blocked=[](Point const&,Point const&){return false;};
|
||||
Memory b;b.Departed=true;assert(!Next({12,0,100},{12,0,200},cfg,b,terrain,blocked).Valid);
|
||||
// The escape exception must not tunnel through bare terrain or water.
|
||||
auto hill=[](float,float){return Surface{200.f,500,true,200.f};};
|
||||
assert(!Corridor({12,0,100},{80,0,100},{12,0,200},cfg,hill,rays,false,true));
|
||||
// A low flight ceiling cannot be ignored by the vertical exception.
|
||||
auto low=[](float x,float y){return Surface{std::hypot(x,y)<20?200.f:0.f,213,true,0.f};};
|
||||
Memory c;c.Departed=true;assert(!Next({90,0,80},{12,0,200},cfg,c,low,rays).Valid);
|
||||
// Ordinary small height changes still use the normal slope.
|
||||
auto gentle=[](float x,float){return Surface{x>99?5.f:0.f,500,true};};
|
||||
auto clear=[](Point const&,Point const&){return true;};
|
||||
Memory d;d.Departed=true;auto normal=Next({0,0,10},{100,0,5},cfg,d,gentle,clear);
|
||||
assert(normal.Valid && d.Elevated==Memory::ElevatedPhase::None);
|
||||
std::cout<<"Elevated flight: slope budget, open column, solid overhang escape, above-platform crossing, touchdown, shared trail, blocked/ceiling checks PASS\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-elevated-flight-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
|
||||
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
@@ -43,14 +43,17 @@ struct World {
|
||||
auto sWorld=&world;
|
||||
enum class ClientlessFlightState {Mounting,Flying,Arrived,Failed};
|
||||
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;} Status;
|
||||
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;Position Departure;
|
||||
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;
|
||||
};
|
||||
enum class ClientlessTravelState {Moving,Arrived,Stuck};
|
||||
struct Travel {struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;
|
||||
auto sClientlessTravelMgr=&travel;
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
struct Executor{bool OwnsActiveFlight(uint32){return true;}}executor;auto sClientlessActionExecutorMgr=&executor;
|
||||
namespace ClientlessTravelTelemetry{void Note(Player*,char const*,std::string const&){} }
|
||||
bool clear=true;
|
||||
bool ClearRay(Player*,Point const&,Point const&){return clear;}
|
||||
class ClientlessFlightMgr {
|
||||
@@ -64,7 +67,7 @@ public:
|
||||
bool LandFollowing(Player*){return true;}bool AdvanceFollowing(Player*,Record&){return true;}
|
||||
void Update(uint32 diff);
|
||||
};
|
||||
'''.replace('Settings Settings;', 'ClientlessFlightPath::Settings Settings;')
|
||||
'''.replace('Settings Settings;', 'ClientlessFlightPath::Settings Settings;').replace('Memory Memory;', 'ClientlessFlightPath::Memory Memory;')
|
||||
harness += update + r'''
|
||||
int main(){
|
||||
ClientlessFlightMgr mgr;mgr._records[26]={};
|
||||
|
||||
@@ -7,13 +7,14 @@ a=s.index('Surface ClientlessFlightMgr::Sample(');b=s.index('\nbool ClientlessFl
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
using uint32=unsigned;using int32=int;
|
||||
constexpr int MAX_NUMBER_OF_GRIDS=64;constexpr float MAX_HEIGHT=100000,INVALID_HEIGHT=-100000;
|
||||
struct Grid{int x_coord=35,y_coord=23;};namespace Trinity{Grid ComputeGridCoord(float,float){return {};}}
|
||||
struct Terrain{bool child=false;uint32 GetId(){return 1;}
|
||||
struct Terrain{float floor=5;bool child=false;float GetStaticHeight(int,unsigned,float,float,float,bool,float){return floor;}uint32 GetId(){return 1;}
|
||||
bool HasChildTerrainGridFile(unsigned,int,int){return child;}float GetMaxHeight(int,unsigned,float,float){return 1000;}}terrain;
|
||||
struct Map{unsigned GetId(){return 1;}Terrain*GetTerrain(){return &terrain;}void LoadGrid(float,float){}
|
||||
float GetHeight(int,float,float,float,bool,float){return 20;}float GetWaterLevel(int,float,float){return -100000;}}map;
|
||||
@@ -21,12 +22,14 @@ struct Player{Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}};
|
||||
namespace PhasingHandler{unsigned swap=719;unsigned GetTerrainMapId(int,unsigned,Terrain*,float,float){return swap;}}
|
||||
struct TerrainMgr{static inline std::set<unsigned>files{1};static inline unsigned checked=0;
|
||||
static bool ExistMapAndVMap(unsigned id,float,float){checked=id;return files.count(id);}};
|
||||
struct Surface{float Ground=0,Ceiling=0;bool Valid=false;};
|
||||
struct Surface{float Ground=0,Ceiling=0;bool Valid=false;float TerrainFloor=0;};
|
||||
struct Record{std::unordered_map<std::string,bool>Tiles;};
|
||||
struct ClientlessFlightMgr{Surface Sample(Player*,Record&,float,float)const;};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){ClientlessFlightMgr mgr;Player p;Record a;
|
||||
auto surface=mgr.Sample(&p,a,1751,-4389);assert(surface.Valid&&surface.Ground==20&&TerrainMgr::checked==1);
|
||||
assert(surface.TerrainFloor==5);
|
||||
terrain.floor=INVALID_HEIGHT;Record missingFloor;assert(!std::isfinite(mgr.Sample(&p,missingFloor,1751,-4389).TerrainFloor));terrain.floor=5;
|
||||
terrain.child=true;Record b;assert(!mgr.Sample(&p,b,1751,-4389).Valid&&TerrainMgr::checked==719);
|
||||
TerrainMgr::files.insert(719);Record c;assert(mgr.Sample(&p,c,1751,-4389).Valid&&TerrainMgr::checked==719);
|
||||
TerrainMgr::files.clear();Record d;assert(!mgr.Sample(&p,d,1751,-4389).Valid);
|
||||
|
||||
Reference in new issue
Block a user