Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba2a4e629c | ||
|
|
f8ee1d0ff6 | ||
|
|
65e4abb0b9 | ||
|
|
768810a82d | ||
|
|
a6572242c6 | ||
|
|
fe03ea8ca5 | ||
|
|
6f8f59ec6d | ||
|
|
ae0b7513d9 | ||
|
|
3056063f12 | ||
|
|
2778cf9bdd | ||
|
|
3c361e435a | ||
|
|
be049baacc | ||
|
|
856832e7bd |
No files matched your search
@@ -0,0 +1,132 @@
|
||||
# Ursachenanalyse: Raidtest 01.11.2026, 22/23 Uhr
|
||||
|
||||
Nur Diagnose; kein Produktionscode, Botplan, Loader oder Serverzustand geändert.
|
||||
Branch feature/clientless-player-core; vorhandene Checkpoints/WIP erhalten.
|
||||
|
||||
## 1. Bestätigter Aufbruchblocker: falsche Zielvariante
|
||||
|
||||
Vorbereitung verwendete instance_visit_at_6354. BuildInstanceVariants in
|
||||
AdventureLocationMgr.cpp bevorzugt bei Raids mit vorhandener 25er-Normalvariante
|
||||
Difficulty1 für den Basisschlüssel. Difficulty0 wird als _d0 erzeugt.
|
||||
Native MapDifficulty.dbc für Karte671: modes0/1/2/3 haben10/25/10/25 Plätze.
|
||||
Dies wurde direkt aus der installierten DBC gelesen, nicht aus Namen geraten.
|
||||
Die passende 10er-Normalvariante ist instance_visit_at_6354_d0; native Eligibility
|
||||
für Renren bestätigt map671/content1/own1/invitation1 für beide Schlüssel.
|
||||
|
||||
Zusätzliche Inkonsistenz: InstanceCapacity=min(20,maxPlayers) begrenzt die 25er-
|
||||
Variante auf20, während Name und Difficulty bei25 bleiben. GroupPlan übernimmt
|
||||
InstanceCapacity. Complete verlangt exakt Capacity Mitglieder plus Rollen:
|
||||
capacity20 benötigt2T/4H/14D; tatsächlich sind10Bots mit2T/2H/6D in Gruppe2.
|
||||
Complete ist somit false, CheckFullGroup aktiviert keinen frühen Abreisehandoff.
|
||||
Um23:00 blockiert Begin vor ResolveInstance/Queue(travel_to_node) in WaitingGroup.
|
||||
Der korrekte Eingangspfad wird folglich gar nicht angefordert. Aktuell alle10
|
||||
im selben raid-Intent/WaitingGroup. Kein Beleg für ein Problem der Bastionroute.
|
||||
|
||||
Der falsche Basisschlüssel war ein Fehler der Testvorbereitung. Die20/25-
|
||||
Inkonsistenz ist ein separater Implementierungsfehler. Ein bloßer Teleport oder
|
||||
Reisereset kann beides nicht beheben. Der vorhandene Variantentest erwartet
|
||||
explizit20 für25er-Modi; Tests decken diese fehlerhafte Semantik daher nicht auf.
|
||||
Keine Tests/Builds in dieser reinen Diagnose gestartet.
|
||||
|
||||
## 2. Bestätigte Zerstreuungskette: Reiseablehnung und ungebundener Fallback
|
||||
|
||||
Historisches rotiertes Navigationlog:22:00:02 guid159 bei
|
||||
(-8864.300,682.378,97.943), ZielAllison(-8867.790,673.673,97.986).
|
||||
NAV_PLAN calculated1/complete1/type0x1, Weg9.38yd,4Punkte,maxZDelta0.03,
|
||||
finalDistance0.08 und reason ok. NAV_START trotzdem PathRejected.
|
||||
Fünf Recoveryversuche zu anderen Graphankern schlagen ebenfalls fehl;
|
||||
Flugfallback kann den Startplatz nicht erreichen.22:00:04 RECOVERY_FAILED und
|
||||
INTENT_ARRIVAL_REJECT beim vereinbarten Treffpunkt.
|
||||
|
||||
Fallback schützt einen aktiven Instanzbesuch sowie Botgruppenmitglieder mit
|
||||
GroupOverride, aber nicht den Treffpunkt einer Gruppenbildung beim Leader.
|
||||
Er durchsucht ALLE Allowed-Locations und priorisiert andere Maps lediglich mit
|
||||
Distanzaufschlag100000. Es gibt keine Bindung an vereinbarten Treffpunkt,
|
||||
Sturmwind oder aktuelle Hauptstadt. Logs zeigen Allison->GildedRose->weitere
|
||||
SW-Orte->Eisenschmiede->andere Hauptstädte, insgesamt37 Ersatzwahlen beim Leader.
|
||||
AdoptGroup übernimmt jeweils den Laufzeitzustand des Leaders. Mitglieder
|
||||
beenden laufende Taxi/Flug/Transport-Schritte vor dem Wechsel; deshalb verfolgen
|
||||
sie zeitversetzt verschiedene alte Ziele und verteilen sich über Kontinente.
|
||||
|
||||
Die genaue geometrische Ursache der ERSTEN PathRejected-Ablehnung ist offen.
|
||||
MMAP hat einen vollständigen Weg geliefert. Nachfolgende Ausführungsprüfungen
|
||||
umfassen AllowsSegment und den für Gruppen installierten GroundPathValidator
|
||||
(Höhenprobe/Steigung/LOS). Beide können ohne präzises NAV_REJECT-Log abbrechen.
|
||||
Die vorhandenen Logs rechtfertigen keine eindeutige Schuldzuweisung an eine
|
||||
bestimmte Höhen-/LOS-Probe. Zur Behebung diesen Ort gezielt reproduzieren und
|
||||
Ablehnungszweig/Koordinaten/erwartete und gemessene Höhe diagnostizieren; keine
|
||||
pauschale Abschaltung von Sicherheitsprüfungen.
|
||||
|
||||
## 3. Zwei beim Stundenwechsel verzögerte Mitglieder
|
||||
|
||||
Um23:00 warteten164/165 noch auf eine Leaderroute für die alte Gruppenbildung.
|
||||
Queue hängt Botmitgliedern route_source an; der Executor wartet bis15Sekunden
|
||||
auf eine geteilte Route, danach erfolgt eigenständige Anschlussplanung.
|
||||
SetWanted fordert während aktiver Reise StopAtNode an; Switching wartet auf
|
||||
Busy=false und führt gegebenenfalls einen Graphknoten-Handover aus.
|
||||
Nachfolgende native Diagnosen: beide travel_to_node stormwind_tradecenter
|
||||
Completed/Arrived und raid/WaitingGroup. Die anfängliche Aussage eines dauerhaften
|
||||
Intent-Hängers ist nicht belegt; das beobachtete Warten war vorübergehend.
|
||||
|
||||
## Gezielte Korrekturen für einen Folgetest
|
||||
|
||||
- Testziel explizit instance_visit_at_6354_d0; Difficulty0/capacity10/2T2H6D
|
||||
zusammen mit nativer Gruppen-Difficulty und Complete überprüfen.
|
||||
- 25er-Modi nicht als20er-Gruppe für einen25er-Raid vollständig melden;
|
||||
konsistente tatsächliche Kapazität oder ausdrückliche Nichtunterstützung.
|
||||
- Formationstreffpunkt beim Leader stabil halten; Fehlschlag führt zu lokaler
|
||||
Wiederholung/Diagnose, nicht automatischem Hauptstadtwechsel.
|
||||
- Anfangsablehnung in GoldenerRose mit genauer Ausführungsdiagnostik reproduzieren.
|
||||
- Regressionen für vollständige10er-Gruppe/früheAbreise, Treffpunktbindung bei
|
||||
Reisefehler, Stundenwechsel mit ausstehender geteilter Route und20/25-Konsistenz.
|
||||
|
||||
Belege: checkpoints/2026-11-01-raid-2200/initial-navigation-failure.log,
|
||||
investigation-console.log, reset-console.log und verified-db.tsv. Server.log
|
||||
enthält die Formation-Fallbacks und Mergefolge. Gameplay/Instanzanreise unbestätigt.
|
||||
|
||||
## Zusatz: drei Nutzerfragen zu Portalen und Kolonne, ca.23:17–23:21
|
||||
|
||||
1. Eisenschmiede: Portalspawn217855/GO195141/Spell65728 ist gebunden an
|
||||
edge1827 portal_use, enabled1/cost1, vonportal_go_217855_source nach
|
||||
portal_go_217855_target (VerwüsteteLande). Renren erreichte Quelle tatsächlich;
|
||||
23:09:48 folgte laut Log free_flight direkt zurBastion statt portal_use.
|
||||
Start wurde verworfen: no reachable open takeoff point, outdoor0,
|
||||
Diagnosecounter surface/indoor/area/column/mmap/movement=0/0/0/1/63/0.
|
||||
Danach Recovery über virtuelleStraße wird vom safetyAdapter abgelehnt.
|
||||
Also Portalbenutzung in dieser Folge NICHT ausgeführt, keine nachgewiesene
|
||||
native Portalzugangsverweigerung. Replan anQuelle: start->free_flight wiederholt.
|
||||
Plannerportalguard gilt nur isPortalSource && previous enthältCurrentState;
|
||||
Quellknoten alsStartwurzel fällt dadurch aus demGuard. Ein interaktivesPortal
|
||||
teleportiert anders als einAreaTrigger nicht beim bloßenAnlaufen.
|
||||
|
||||
2. Sturmwind-Schattenhochland: spawn200627/GO207694/Spell85545, edge1801,
|
||||
Quelle(-8186.15,413.729,116.75), Ziel(-4890.07,-6626.74,10.2846), map0.
|
||||
Enabled1/cost1. DB zeigt KEINE eingehende festeKante zurQuelle. Zurückliegender
|
||||
Livebeleg Elanora16:57:17 benutztPortal nativ mit arrivalError0; Voraussetzungen
|
||||
nicht pauschal alsGrund behaupten. Level80+-AdventureUnlock existiert.
|
||||
Aktuelle Route zumBastioneingang: SWtradecenter->taxi2->taxi6->IFportalQuelle,
|
||||
danach freieFlugplanung/Recovery. Nur dynamischerAnschluss kannSWPortal
|
||||
verbinden (allgemeineBrücken max200yd, Flugkandidaten16/Plan und6/Ursprung).
|
||||
Offline geometrische Kandidatenrangberechnung OHNEArea/Prereq/FlightFilter:
|
||||
SWportalQuelle steht vonSWtradecenter sowietaxi2 sogar aufRang1 der
|
||||
Makrokandidaten. Damit ist reineDistanzsortierung NICHT alsAblehnungsgrund
|
||||
belegt. Es fehlen personenbezogene Logs zu Kandidatenbudget/Estimate-Ablehnung
|
||||
und Folgeverbindung abPortalziel. Exakten verlorenenRoutenvergleich erst mit
|
||||
gezieltemPlantrace behaupten. Keine Runtimeplanung/Teleport zuTest erzwungen.
|
||||
|
||||
3. Kolonne: AdventureInteractionMgr kehrt beiBotLed vorGroupTravelMgr::Follow
|
||||
zurück. Botgruppenmitglieder adoptierenExecutorroute (AdoptRoute), teilen also
|
||||
Makroziel/Route, nicht automatisch individuellePositionen. Die allgemeine
|
||||
Formation()/Follow-Steuerung wird dabei nicht verwendet. AdaptRoad läuft
|
||||
nur imStartRecordedEdgePath, nicht für lokaleMMAP-Zuläufe/local_bridge/start.
|
||||
AdaptRoad erhält den gemeinsamenEndpunkt; wennSeitversatz unsicher ist, zweiter
|
||||
Versuch mitside0. Damit kann selbstStraßenadapter aufgleicheLinie zurückfallen.
|
||||
NativeTaxiflüge haben identischen nativenPfad ohne separateKolonnenversätze.
|
||||
EigeneFreeFlight hat sharedTrail mit3.5s+slot*0.6s Startverzögerung nur wenn
|
||||
passenderLeaderflightRecord mit gleichemZiel vorhanden. Kein allgemeiner
|
||||
Abstandsregler über alleReisemodi; keinePortalWarteaufstellung.
|
||||
Nutzerbeobachtung desÜbereinanderlaufens passt zu diesen Integrationslücken;
|
||||
keinBeleg, dass jedeFormationkomponente deaktiviert wäre.
|
||||
|
||||
Belege Zusatz: ironforge-portal-route-evidence.log; Sourceprüfung der erwähnten
|
||||
Methoden und nur lesendeWorldDB-Abfragen. KeineBot-/Codeänderung/Build/Restart.
|
||||
@@ -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).
|
||||
File diff suppressed because it is too large.
Load diff
@@ -56,6 +56,31 @@ planned formation-to-activity transition is exempt from intent-change departure.
|
||||
Later changes retain the existing loyalty-based departure behavior, deferred in
|
||||
combat/instances/transit. Native removal handles leadership and party dissolution.
|
||||
|
||||
### Final formation manager
|
||||
|
||||
During the final ten minutes of `groupformation`, a deterministic fallback runs
|
||||
every 30 seconds. It prefers the largest compatible bot-led group, fills only its
|
||||
currently missing roles, and may move a bot out of a smaller incomplete bot-led
|
||||
group. Human-led and browser/LFG groups are never reorganized. Every transfer is
|
||||
revalidated against faction, the inclusive five-level range, profession rules,
|
||||
the leader's chosen destination, that destination's level/access restrictions,
|
||||
area policy and native capacity. Level 85 is an isolated matching category: a
|
||||
level-85 bot is only consolidated with other level-85 bots. This is a greedy
|
||||
fallback intended to maximize complete groups without replacing normal social
|
||||
formation; it does not claim a globally optimal partition.
|
||||
|
||||
The ordinary five-minute gathering/departure rule remains authoritative. A
|
||||
complete group can therefore finish consolidation by minute 50 and gather from
|
||||
minute 55. At the block boundary, groups still incomplete use the configured
|
||||
browser behavior and do not begin an overland dungeon or raid journey. The
|
||||
manager never changes stored day-plan slots or a group's leader target.
|
||||
|
||||
GM diagnostics and explicit test fallback are available as
|
||||
`clientless groupmanager preview`, `clientless groupmanager run`, and
|
||||
`clientless groupmanager status`. `run` uses the same access, role, faction and
|
||||
transfer checks but deliberately ignores the ten-minute time gate; it does not
|
||||
override human leadership or native admission rules.
|
||||
|
||||
## Shared travel and instances
|
||||
|
||||
Followers run their AdventureBehavior controller with the leader's effective
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Adventure portal unlock build recovery
|
||||
|
||||
Safe logout was requested while the `-j10` build started in parallel. The build
|
||||
completed successfully. At the five-real-minute installation boundary eleven
|
||||
managed sessions remained (`combat=4`, `transport=0`, `flight/taxi=7`). `saveall`
|
||||
completed before shutdown.
|
||||
|
||||
| GUID | Name | Faction | Saved position | Observed transition/action |
|
||||
|---:|---|---|---|---|
|
||||
| 26 | Arthoran | Alliance | map0 `-11080.00,-483.45,54.49` | failed bounded free flight; mounted hold |
|
||||
| 60 | Ashani | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 |
|
||||
| 77 | Drakogzug | Horde | map1 `1852.91,-4570.06,24.98` | no flight/transport; failed departure recovery |
|
||||
| 80 | Noralin | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 |
|
||||
| 90 | Dorndin | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 |
|
||||
| 93 | Gargar | Horde | map571 `2277.05,5202.07,12.49` | flight arrived; zeppelin state arrived |
|
||||
| 96 | Darden | Alliance | map0 `-9381.78,-59.96,93.02` | failed bounded free flight; mounted hold |
|
||||
| 97 | Talzul | Horde | map1 `2035.92,-3467.29,171.71` | failed bounded free flight; mounted hold |
|
||||
| 132 | Braneron | Horde | map0 `2602.82,-112.11,34.02` | idle; no flight/transport |
|
||||
| 135 | Ragnhild | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 |
|
||||
| 153 | Ragnli | Alliance | map0 `-11787.06,-3180.12,-29.71` | idle; no flight/transport |
|
||||
|
||||
Loader configuration and test admission remained disabled/paused through the
|
||||
build, installation and restart window. Post-start recovery outcomes are added
|
||||
below after verification.
|
||||
|
||||
## Post-start verification
|
||||
|
||||
- Fresh worldserver PID 174775 reached ready state; TCP 8085 and 8087 listen.
|
||||
- All eleven recorded characters were loaded. Every prior free-flight, native
|
||||
taxi and transport state was absent after login; ordinary schedules then
|
||||
produced new behavior independently of the saved transition state.
|
||||
- Targeted production-method tests exercise both paths: native GameObject use
|
||||
when prerequisites pass, and the importer-validated fixed teleport when a
|
||||
level-80+ AdventureBot lacks quest prerequisites. Arrival still requires a
|
||||
changed clientless teleport sequence, correct map and <=5-yard target error.
|
||||
- A level-85 Elanora live probe planned and traversed graph-bound capital portal
|
||||
travel. The explicit Twilight Highlands acceptance and its actual native
|
||||
condition state are documented below.
|
||||
|
||||
## Explicit Twilight Highlands acceptance
|
||||
|
||||
Elanora (level 85) was placed within native interaction range of Stormwind spawn
|
||||
200627 and given only target `portal_go_200627_target`. Production navigation
|
||||
log at 2026-11-01 16:57:17 records edge 1801 `portal_use`, spawn 200627, spell
|
||||
85545, followed in the same second by `EDGE_COMPLETE` on map 0 with
|
||||
`arrivalError=0.00`. Runtime area query at the destination resolved area 5140,
|
||||
allowed, level band 84-85. The World DB has no spell-condition rows for 85545,
|
||||
so this particular portal correctly used the unchanged native click path rather
|
||||
than `PORTAL_ADVENTURE_UNLOCK`. The questless unlock path remains covered by the
|
||||
production-method regression. Temporary Elanora profile and override removed.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Generic flight-entry build recovery
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 12:02 CET after pausing
|
||||
`Clientless.Adventure.Loader` and TestBot admission.
|
||||
- The `-j10` build ran in parallel with the safe drain, per the corrected build
|
||||
policy in `AGENTS.md`, and completed successfully.
|
||||
- At 12:08 CET, more than five real minutes later, 13 managed sessions remained:
|
||||
3 combat flags, 0 active transports and 9 native flight flags. The database
|
||||
positions below are from the immediately preceding online snapshot; native
|
||||
action/flight/transport diagnostics were also captured in the tmux console.
|
||||
|
||||
| GUID | Name | faction | saved map / position | observed state |
|
||||
|---:|---|---|---|---|
|
||||
| 40 | Drakagmok | Horde | 1 / -4777.22 -1264.89 15.78 | native taxi 22->42; no free-flight/transport record |
|
||||
| 48 | Elaniel | Horde | 1 / -4647.84 -1609.87 23.93 | native taxi 22->42; no free-flight/transport record |
|
||||
| 54 | Falaeriel | Horde | 0 / 1875.88 -5018.08 166.81 | native taxi chain; prior free flight arrived |
|
||||
| 77 | Drakogzug | Horde | 1 / 1857.05 -4568.03 24.98 | travel completed; no active flight/transport |
|
||||
| 80 | Noralin | Alliance | 1 / 7990.23 1296.75 59.63 | native taxi; old ship record already Arrived |
|
||||
| 83 | Mahuwa | Horde | 571 / 4234.13 3346.45 391.19 | failed free flight/permission timeout |
|
||||
| 87 | Moggra | Horde | 1 / -3230.20 -1945.24 156.47 | native taxi to node55 |
|
||||
| 91 | Norara | Horde | 571 / 3588.17 6692.83 208.30 | failed bounded flight corridor |
|
||||
| 104 | Lillini | Alliance | 1 / 2669.46 1667.13 393.02 | native taxi; old flight/ship records Arrived |
|
||||
| 118 | Lillina | Alliance | 0 / -8768.04 583.35 133.27 | failed bounded flight corridor |
|
||||
| 132 | Braneron | Horde | 0 / 2539.96 -53.78 28.11 | idle; no flight/transport state |
|
||||
| 146 | Loraria | Horde | 1 / -4215.50 -1913.36 131.73 | native taxi 22->42 |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state |
|
||||
|
||||
No bot was individually force-logged-out, teleported, detached, or removed to
|
||||
enable the build. Installation/restart uses the documented five-minute clean
|
||||
shutdown exception. Post-start recovery results must be appended below.
|
||||
|
||||
## Restart outcome
|
||||
|
||||
Pending.
|
||||
|
||||
## Second build drain
|
||||
|
||||
- A second safe drain was requested at approximately 12:15 CET for the
|
||||
flight-budget correction. The incremental `-j10` build again ran in parallel
|
||||
and succeeded.
|
||||
- After the five-minute gate, only two sessions remained online; neither had an
|
||||
active free-flight or transport record:
|
||||
|
||||
| GUID | Name | faction | saved map / position | observed state |
|
||||
|---:|---|---|---|---|
|
||||
| 77 | Drakogzug | Horde | 1 / 1852.91 -4570.06 24.98 | completed location travel; outdoors=0 |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state |
|
||||
|
||||
The other three members of the earlier five-bot snapshot logged out safely
|
||||
before this final snapshot. No individual force logout was used.
|
||||
|
||||
## Third build drain
|
||||
|
||||
- Final macro-connector prioritization build started in parallel with a safe
|
||||
drain at approximately 15:47 CET and completed successfully with `-j10`.
|
||||
- At the 15:52 CET installation gate, GUID 77 Drakogzug and GUID 153 Ragnli
|
||||
remained in combat at the same positions recorded above. Both had zero active
|
||||
free-flight and transport records. They were not interrupted or individually
|
||||
logged out; the documented clean-shutdown exception was used.
|
||||
## Fourth build drain (portal-continuation budget fix)
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 15:54:30 CET. The `-j10`
|
||||
build started immediately in parallel and completed successfully.
|
||||
- At the five-minute installation boundary, two clientless sessions remained;
|
||||
neither was in free flight, taxi travel or on a transport:
|
||||
- GUID 77, Drakogzug, Horde, map 1 at `(1852.91, -4570.06, 24.98)`, area
|
||||
1637. Action had failed at `flight_departure`; no free-flight or transport
|
||||
state was present.
|
||||
- GUID 153, Ragnli, Alliance, map 0 at `(-11787.06, -3180.12, -29.71)`, area
|
||||
72. Action executor was idle; no free-flight or transport state was present.
|
||||
- GUID 168 Elanora completed her active movement state and logged out safely
|
||||
before the boundary. The temporary route-test Lua override was removed from
|
||||
the source tree before this build.
|
||||
- Testbot admission remained paused. AdventureBot loader remained disabled in
|
||||
runtime configuration throughout the build and restart window.
|
||||
|
||||
## Fifth build drain (destination-first probe fix)
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 16:04:15 CET. Build
|
||||
`-j10` again ran immediately in parallel and succeeded.
|
||||
- GUID 168 Elanora completed her active free-flight transition and logged out
|
||||
safely. At the five-minute installation boundary GUID 77 Drakogzug and GUID
|
||||
153 Ragnli remained online in the same saved positions recorded above.
|
||||
- `saveall` completed before shutdown. Both remaining bots again reported no
|
||||
free-flight or transport state. Drakogzug had a failed travel action at
|
||||
`flight_departure`; Ragnli was idle. Test admission and the AdventureBot
|
||||
loader remained paused/disabled.
|
||||
|
||||
## Final runtime result
|
||||
|
||||
- Fresh worldserver PID 172446 reached `(worldserver-daemon) ready`; TCP 8085
|
||||
and 8087 are listening.
|
||||
- GUIDs 77 and 153 were loaded after restart. Both had no stale free-flight or
|
||||
transport state and resumed ordinary behavior.
|
||||
- The controlled Elanora plan produced a valid direct free-flight route from a
|
||||
regular graph node to `portal_at_6354_source`; no MMAP attachment was needed.
|
||||
The Stormwind Twilight Highlands portal did not compete because edge 1801
|
||||
correctly failed `PortalPrerequisites` for this character's native spell/
|
||||
quest state. The relevant portal is otherwise the first geometric connector,
|
||||
so this was not a probe-budget failure.
|
||||
- Temporary Elanora override removed. Schedule restored. Loader configuration
|
||||
restored to enabled and test admission resumed. Loader status confirmed
|
||||
`enabled=1 schema=1 busy=0`, pools `50/5/50/5`.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# safe_logout_requested approximately 2026-11-01 01:08:20 CET
|
||||
# five_minute_threshold approximately 2026-11-01 01:13:20 CET
|
||||
# snapshot_saved 2026-11-01 01:13:32 CET, after saveall
|
||||
# aggregate_nearest_status pending=20 sessions=20 combat=2 transport=0 flight=15 loading=0
|
||||
# note Loader was confirmed enabled=0/busy=0 and did not replenish sessions. Rows are generated bots online in the final saved DB snapshot. Per-bot combat/flight flags are not exposed by the console command; aggregate flags above are the available evidence. All had a pending global safe-logout request.
|
||||
guid name faction map zone x y z
|
||||
43 Fizzira Alliance 0 4 -11788.8 -3176.69 -29.7341
|
||||
51 Tordra Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
53 Noranda Alliance 0 45 -2059.88 -2585.36 108.528
|
||||
70 Renaisia Alliance 1 148 7854.57 73.7393 78.0483
|
||||
72 Sorana Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
81 Ashara Horde 1 14 1389.54 -4370.19 25.8776
|
||||
83 Mahuwa Horde 0 4922 -3557.36 -4377.18 268.208
|
||||
84 Brynli Alliance 1 331 2779.05 -58.9212 112.253
|
||||
85 Gixni Horde 1 357 -4300.55 -646.328 25.2518
|
||||
88 Pibwin Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
99 Shadra Horde 1 331 2540.73 -1526.06 225.275
|
||||
102 Corren Alliance 0 12 -9484.66 443.337 68.0268
|
||||
119 Aethilrin Horde 1 400 -4763.22 -1180.05 16.4765
|
||||
121 Braneven Horde 0 139 2074.61 -4543.65 151.667
|
||||
125 Koruhon Horde 1 17 1259.58 -3802.96 122.248
|
||||
132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
|
||||
134 Renven Horde 1 14 763.235 -4611.83 -1.08043
|
||||
136 Maraannora Horde 1 14 749.172 -4635.61 -3.2926
|
||||
146 Loraria Horde 1 1637 1589.99 -4212.98 82.4014
|
||||
153 Ragnli Alliance 1 141 7990.23 1296.75 59.6283
|
||||
|
@@ -0,0 +1,21 @@
|
||||
# safe_logout_requested 2026-11-01 00:55:57 CET (approximately; console request followed config edit at 00:55:52)
|
||||
# five_minute_threshold 2026-11-01 01:00:57 CET
|
||||
# snapshot_saved 2026-11-01 01:01 CET, immediately after saveall
|
||||
# aggregate_nearest_status pending=17 sessions=17 combat=2 transport=0 flight=14 loading=0
|
||||
# note The enabled loader was busy and rejected runtime reload, so it continued transient login/safe-logout cycles. Rows below are the generated bots online in the final saved DB snapshot. Per-bot combat/flight flags were not exposed by the console command; aggregate flags above are the available diagnostic evidence. All had a pending global safe-logout request.
|
||||
guid name faction map zone x y z
|
||||
43 Fizzira Alliance 0 4 -11778.6 -3179.17 -28.9872
|
||||
47 Nimibin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
49 Lunna Alliance 1 141 10041.9 1838.98 1367.35
|
||||
54 Falaeriel Horde 0 2037 3538.87 -4506.39 152.38
|
||||
88 Pibwin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
104 Lillini Alliance 0 1 -5066.67 -818.772 497.329
|
||||
106 Nibelin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
118 Lillina Alliance 1 141 7990.23 1296.75 59.6283
|
||||
132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
|
||||
135 Ragnhild Alliance 1 141 9797.77 998 1313.46
|
||||
153 Ragnli Alliance 1 141 9552.06 992.021 1366.62
|
||||
160 Marndra Alliance 530 3519 -1471.88 3873.29 268.262
|
||||
161 Marnda Alliance 530 3483 -980.07 2999.83 87.3079
|
||||
163 Winienwi Alliance 530 3519 -1666.02 4773.16 85.4449
|
||||
164 Taliaarnora Alliance 530 3519 -1673.09 4869.15 137.132
|
||||
|
@@ -0,0 +1,36 @@
|
||||
# Instance/navigation first-fix build recovery
|
||||
|
||||
- Safe logout requested approximately 2026-11-01 11:26 CET after pausing
|
||||
`Clientless.Adventure.Loader` and TestBot admission.
|
||||
- Build was allowed to run in parallel by the user's explicit clarification;
|
||||
installation/restart remained gated by zero bots or the five-minute limit.
|
||||
- At 11:31 CET, nine managed bots remained after more than five minutes.
|
||||
The server was therefore eligible for a clean shutdown under the agreed rule.
|
||||
- The separately logged-in human `Allitester` (GUID 21) is not a managed bot.
|
||||
|
||||
| GUID | Name | faction | saved map/position | observed state before shutdown |
|
||||
|---:|---|---|---|---|
|
||||
| 40 | Drakagmok | Horde | 1 / -4009.801 -2003.480 121.296 | groupformation; native taxi chain; no free-flight/transport record |
|
||||
| 43 | Fizzira | Alliance | 0 / -8830.011 635.672 119.259 | social; failed held free flight, 94 segments |
|
||||
| 60 | Ashani | Alliance | 1 / 7990.230 1296.750 59.628 | leisure; no flight capability/state |
|
||||
| 77 | Drakogzug | Horde | 1 / 1852.910 -4570.060 24.981 | economy; flight and local travel arrived |
|
||||
| 83 | Mahuwa | Horde | 571 / 2906.229 6057.960 80.671 | profession; zeppelin arrived; free flight failed touchdown confirmation |
|
||||
| 96 | Darden | Alliance | 0 / -9337.900 12.744 105.163 | profession; failed held free flight, 162 segments |
|
||||
| 121 | Braneven | Horde | 0 / 1964.780 -1567.223 123.819 | rest; failed held free flight, 462 segments |
|
||||
| 132 | Braneron | Horde | 0 / 2539.960 -53.784 28.107 | no intent/travel/flight/transport state |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.057 -3180.120 -29.714 | no intent/travel/flight/transport state |
|
||||
|
||||
Aggregate immediately before diagnostics: nine sessions, three combat and six
|
||||
flight flags. No bot was individually force-logged-out, teleported, detached or
|
||||
removed to enable the build.
|
||||
|
||||
## Restart outcome
|
||||
|
||||
- Build `-j10`, installation and fresh startup succeeded.
|
||||
- Fresh worldserver PID 153100 reached `(worldserver-daemon) ready...`; TCP
|
||||
listeners 8085 and 8087 were verified.
|
||||
- All nine bots were loaded explicitly before restoring the population loader.
|
||||
- None retained an old free-flight or transport controller record. Normal new
|
||||
intent travel/combat resumed, so no recovery teleport was necessary.
|
||||
- Loader and TestBot admission were restored. Population status reported
|
||||
`enabled=1 schema=1 busy=0`, targets `50/5/50/5`; staggered loading resumed.
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only, bounded observation and deterministic test report. No bot repairs."""
|
||||
from pathlib import Path
|
||||
import collections,datetime,json,os,re,subprocess,sys,time
|
||||
import test_driver as d
|
||||
BASE=d.BASE
|
||||
REPORT=d.ROOT/'doc/clientless/2026-11-02-0800-test-report.md'
|
||||
def append(path,value):
|
||||
with path.open('a') as f:f.write(json.dumps(value,ensure_ascii=False)+'\n')
|
||||
def load(path):
|
||||
result=[]
|
||||
if path.exists():
|
||||
for line in path.read_text().splitlines():
|
||||
try:result.append(json.loads(line))
|
||||
except json.JSONDecodeError:pass
|
||||
return result
|
||||
def manifest():return json.loads((BASE/'manifest.json').read_text())
|
||||
def alive():
|
||||
r=subprocess.run(['pgrep','-x','worldserver'],text=True,capture_output=True)
|
||||
result=[]
|
||||
for value in r.stdout.split():
|
||||
try:
|
||||
if os.readlink('/proc/'+value+'/exe')=='/home/wow_admin/wow_cata_server/server/debug/bin/worldserver':result.append(int(value))
|
||||
except OSError:pass
|
||||
return result
|
||||
EVENT=re.compile(r'\b(?:GROUP_[A-Z_]+|INSTANCE_[A-Z_]+|INTENT_[A-Z_]+|EDGE_[A-Z_]+|RECOVERY_[A-Z_]+|NAV_REJECT|TEMP_TRAVEL|PORTAL_[A-Z_]+|TRAVEL_(?:PLAN[A-Z_]*|JOURNEY_COSTS|SHARED_ROUTE)|CPC_PERF)\b')
|
||||
class Tail:
|
||||
def __init__(self,path):self.path=path;self.file=None;self.initial=True
|
||||
def poll(self):
|
||||
try:
|
||||
st=self.path.stat()
|
||||
if self.file is None:
|
||||
self.file=self.path.open();self.inode=st.st_ino
|
||||
if self.initial:self.file.seek(0,2);self.initial=False
|
||||
lines=self.file.readlines()
|
||||
if self.inode!=st.st_ino:
|
||||
self.file.close();self.file=self.path.open();self.inode=st.st_ino;lines+=self.file.readlines()
|
||||
elif self.file.tell()>st.st_size:self.file.seek(0)
|
||||
stamp=d.now().isoformat()
|
||||
for line in lines:
|
||||
match=EVENT.search(line)
|
||||
if match:append(BASE/'events.jsonl',{'observed_at':stamp,'source':self.path.name,'event':match[0],'line':line.rstrip()})
|
||||
except OSError as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
def snapshot(native=True):
|
||||
record={'time':d.now().isoformat(),'server_pids':alive()}
|
||||
record['roster']=d.roster()
|
||||
record['groups']=d.query('Character','SELECT g.guid,g.leaderGuid,g.groupType,m.memberGuid,c.name,c.level,c.online FROM groups g JOIN group_member m ON m.guid=g.guid JOIN characters c ON c.guid=m.memberGuid ORDER BY g.guid,m.memberGuid;')
|
||||
record['plans']=d.plans();record['native']={}
|
||||
if native and record['server_pids']:
|
||||
raw=[];ids=[r['guid'] for r in record['roster'] if r['level']>=15]
|
||||
for pos in range(0,len(ids),20):
|
||||
chunk=ids[pos:pos+20]
|
||||
for g in chunk:
|
||||
for command in [f'clientless adventure status {g}',f'clientless actioninfo {g}',f'clientless interaction {g}',f'clientless adventure area {g}']:d.command(command)
|
||||
time.sleep(1.5);text=d.console(1400);raw.append(text)
|
||||
for g in chunk:
|
||||
bot={}
|
||||
for key,pattern in {
|
||||
'behavior':rf'Adventure GUID {g}: enabled=(\d+) schedule=(\d+) phase=(\d+) intent=\x27([^\x27]*)\x27 location=\x27([^\x27]*)\x27.*?detail=\x27([^\x27]*)\x27',
|
||||
'action':rf'Action GUID {g}: state=(\w+) action=\x27([^\x27]*)\x27 target=\x27([^\x27]*)\x27.*?detail=\x27([^\x27]*)\x27',
|
||||
'area':rf'Area access GUID {g}: .*?map=(\d+) area=(\d+).*?allowed=(\d+)',
|
||||
}.items():
|
||||
matches=list(re.finditer(pattern,text))
|
||||
if matches:bot[key]=list(matches[-1].groups())
|
||||
record['native'][str(g)]=bot
|
||||
(BASE/('snapshot-'+d.now().strftime('%Y%m%d-%H%M%S')+'.log')).write_text('\n'.join(raw))
|
||||
append(BASE/'snapshots.jsonl',record)
|
||||
return record
|
||||
def observe():
|
||||
end=datetime.datetime.fromisoformat(manifest()['evaluate_at'])
|
||||
logs=Path('/home/wow_admin/wow_cata_server/server/debug/bin')
|
||||
tails=[Tail(logs/'Server.log'),Tail(logs/'adventurebot-navigation.log')]
|
||||
for tail in tails:tail.poll()
|
||||
next_snapshot=0
|
||||
append(BASE/'lifecycle.jsonl',{'event':'observer_started','time':d.now().isoformat(),'end':end.isoformat()})
|
||||
while d.now()<end:
|
||||
for tail in tails:tail.poll()
|
||||
if time.monotonic()>=next_snapshot:
|
||||
try:snapshot()
|
||||
except Exception as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
next_snapshot=time.monotonic()+300
|
||||
# Preserve a final snapshot before the activity slot ends.
|
||||
if (end-d.now()).total_seconds()<35:
|
||||
try:snapshot()
|
||||
except Exception as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
while d.now()<end:
|
||||
for tail in tails:tail.poll()
|
||||
time.sleep(1)
|
||||
break
|
||||
time.sleep(2)
|
||||
for tail in tails:tail.poll()
|
||||
append(BASE/'lifecycle.jsonl',{'event':'observer_finished','time':d.now().isoformat()})
|
||||
def report(preview=False):
|
||||
m=manifest();end=datetime.datetime.fromisoformat(m['evaluate_at']);start=end.replace(hour=m['formation_hour'])
|
||||
snapshots=[s for s in load(BASE/'snapshots.jsonl') if start<=datetime.datetime.fromisoformat(s['time'])<=end]
|
||||
events=[]
|
||||
for item in load(BASE/'events.jsonl'):
|
||||
stamp=datetime.datetime.fromisoformat(item['observed_at'])
|
||||
match=re.match(r'(\d{4}-\d\d-\d\d_\d\d:\d\d:\d\d)',item['line'])
|
||||
if match:stamp=datetime.datetime.strptime(match[1],'%Y-%m-%d_%H:%M:%S').replace(tzinfo=d.TZ)
|
||||
if start<=stamp<end:events.append(item)
|
||||
# Server and navigation logs can repeat the same event; never count duplicates.
|
||||
unique={}
|
||||
for item in events:
|
||||
line=re.sub(r'^.*?\[(?:server\.[^]]+)\]\s*','',item['line'])
|
||||
key=(item['event'],line)
|
||||
unique.setdefault(key,item)
|
||||
events=list(unique.values());counts=collections.Counter(e['event'] for e in events)
|
||||
arrivals=set();entered=set();cohort_entered=set();positions={};reasons=collections.Counter();plans=set()
|
||||
cohort=set(m['raid_guids']);goal='instance_visit_at_6354_d0'
|
||||
for e in events:
|
||||
line=e['line'];g=re.search(r'\bguid=(\d+)\b',line)
|
||||
guid=int(g[1]) if g else None
|
||||
if guid is not None:
|
||||
if e['event']=='INTENT_ARRIVED' and "intent='groupformation'" in line and any("location='"+v+"'" in line for v in m['meeting_locations'].values()):arrivals.add(guid)
|
||||
if e['event']=='INSTANCE_ENTERED':
|
||||
entered.add(guid)
|
||||
if 'map=671 ' in line and 'mode=0' in line and guid in cohort:cohort_entered.add(guid)
|
||||
if e['event']=='INSTANCE_PLAN' and guid in cohort:plans.add(guid)
|
||||
if e['event']=='TEMP_TRAVEL' and 'position=' in line:positions[guid]=line
|
||||
if e['event'] in ['RECOVERY_FAILED','PORTAL_USE_FAILED','INSTANCE_ENTRY_DENIED','INSTANCE_TRAVEL_RETRY']:
|
||||
detail=re.search(r"(?:reason|status)='([^']*)'",line)
|
||||
reasons[(e['event'],detail[1] if detail else 'siehe Rohlog')]+=1
|
||||
max_group=0;together=False
|
||||
rolemap={r['guid']:r.get('role','unknown') for r in m['roster']}
|
||||
latest_group={}
|
||||
for s in snapshots:
|
||||
if not s['server_pids']:continue
|
||||
grouped=collections.defaultdict(list)
|
||||
for row in s['groups']:grouped[row['guid']].append(row)
|
||||
for gid,rows in grouped.items():
|
||||
members={int(r['memberGuid']) for r in rows};overlap=members&cohort
|
||||
if len(overlap)>max_group:max_group=len(overlap)
|
||||
if len(overlap)==10:together=True
|
||||
latest_group=grouped
|
||||
for snap in snapshots:
|
||||
if not snap['server_pids']:continue
|
||||
for guid,bot in snap.get('native',{}).items():
|
||||
status=bot.get('behavior')
|
||||
if status and status[2]=='2' and status[3]=='groupformation' and status[4] in m['meeting_locations'].values():arrivals.add(int(guid))
|
||||
latest=snapshots[-1] if snapshots else None
|
||||
leader_seeds={};copies=0;matched_seeds=0;unknown_seeds=0;mismatched_seeds=0
|
||||
for e in events:
|
||||
line=e['line']
|
||||
if e['event']=='TRAVEL_JOURNEY_COSTS':
|
||||
match=re.search(r'guid=(\d+) seed=(\d+)',line)
|
||||
if match:leader_seeds[int(match[1])]=int(match[2])
|
||||
elif e['event']=='TRAVEL_SHARED_ROUTE':
|
||||
match=re.search(r'leader=(\d+).*?journeySeed=(\d+)',line)
|
||||
if match:
|
||||
copies+=1;leader=int(match[1]);seed=int(match[2])
|
||||
if leader not in leader_seeds:unknown_seeds+=1
|
||||
elif leader_seeds[leader]==seed:matched_seeds+=1
|
||||
else:mismatched_seeds+=1
|
||||
stamp=d.now();future=preview or stamp<end
|
||||
title='Vorläufige Testbeobachtung' if future else 'Testauswertung'
|
||||
lines=[f'# {title}: {start:%d.%m.%Y}, Gruppenbildung {m["formation_hour"]:02d}:00 / Instanzen {m["activity_hour"]:02d}:00',
|
||||
'',f'Erstellt: {stamp:%d.%m.%Y %H:%M:%S %Z}. Geplanter Auswertungszeitpunkt: {end:%H:%M %Z}.',
|
||||
'',f'- Geprüfte Testbots: {len(m["eligible_guids"])}. Unter Stufe 15 ausgenommen: {len(m["excluded_under15"])}.',
|
||||
f'- Feste Treffpunkte: Allianz Goldene Rose in Sturmwind; Horde Broken Tusk in Orgrimmar.',
|
||||
f'- Status-Snapshots: {len(snapshots)}; ausgewertete eindeutige Logereignisse: {len(events)}.',
|
||||
f'- Erste erfasste Statusstichprobe: {snapshots[0]["time"] if snapshots else "keine"}; frühere Ereignisse sind nicht vollständig erfasst.',
|
||||
f'- Bots mit belegter Ankunft am festgelegten Formationstreffpunkt: {len(arrivals)}.',
|
||||
f'- Bots mit protokolliertem tatsächlichem Instanzbeitritt: {len(entered)}.',
|
||||
f'- Snapshots ohne laufenden Testserver: {sum(not s["server_pids"] for s in snapshots)}.',
|
||||
'', '## Bekannte Zehnergruppe: Bastion des Zwielichts, normal', '',
|
||||
f'- Maximal gemeinsam in einer nativen Gruppe beobachtet: {max_group}/10.',
|
||||
f'- Alle zehn gemeinsam nachgewiesen: {"ja" if together else "nein; durch die Stichproben nicht nachgewiesen"}.',
|
||||
f'- Bots mit Instanzplanung: {len(plans)}/10; mit tatsächlichem Beitritt zu Karte 671 im Modus 0: {len(cohort_entered)}/10.',
|
||||
'', '| GUID | Name | Rolle | Bastion betreten | Letzter nativer Status |', '|---:|---|---|---|---|']
|
||||
byid={r['guid']:r for r in m['roster']}
|
||||
for g in sorted(cohort):
|
||||
row=byid.get(g,{});status=(latest or {}).get('native',{}).get(str(g),{}).get('behavior')
|
||||
detail=(status[3]+' / '+status[4]+' / '+status[5]) if status else 'kein frischer Status erfasst'
|
||||
lines.append(f'| {g} | {row.get("name","unbekannt")} | {rolemap.get(g,"unbekannt")} | {"ja" if g in cohort_entered else "nicht nachgewiesen"} | {detail.replace("|","/")} |')
|
||||
lines+=['','## Übernahme der Anführerroute','',
|
||||
f'- Protokollierte Routenübernahmen mit Journey-Seed: {copies}.',
|
||||
f'- Gleicher zuletzt beobachteter Anführer-Seed: {matched_seeds}; abweichend: {mismatched_seeds}; Anführer-Seed im Beobachtungsfenster nicht bekannt: {unknown_seeds}.',
|
||||
'- Ein abweichender letzter Seed erfordert Prüfung der konkreten Reise und des Zeitpunkts; diese Zählung allein beweist keinen Fehler.',
|
||||
'', '## Fehler und Reiseereignisse','']
|
||||
for key in ['GROUP_MERGE','GROUP_MANAGER_RUN','GROUP_EARLY_DEPART','PORTAL_USE_START','RECOVERY_FAILED','INSTANCE_TRAVEL_RETRY','INSTANCE_ENTRY_DENIED']:
|
||||
lines.append(f'- {key}: {counts[key]}.')
|
||||
if reasons:
|
||||
lines+=['','Häufigste protokollierte Fehler:','']
|
||||
for (kind,reason),count in reasons.most_common(12):lines.append(f'- {kind}: {reason} ({count} Ereignisse).')
|
||||
lines+=['','## 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.',
|
||||
f'- Letzter Snapshot: {latest["time"] if latest else "keiner"}.',
|
||||
'',f'Rohdaten: {BASE.relative_to(d.ROOT)}/ (manifest.json, snapshots.jsonl, events.jsonl, snapshot-*.log).','']
|
||||
target=BASE/'report-preview.md' if future else REPORT
|
||||
tmp=target.with_suffix('.tmp');tmp.write_text('\n'.join(lines));tmp.replace(target)
|
||||
(BASE/'report-status.json').write_text(json.dumps({'generated_at':stamp.isoformat(),'expected_at':end.isoformat(),'preview':future,'path':str(target),'snapshots':len(snapshots),'events':len(events),'cohort_entered':sorted(cohort_entered)},indent=2))
|
||||
print(target)
|
||||
if __name__=='__main__':
|
||||
if sys.argv[1:]==['observe']:observe()
|
||||
elif sys.argv[1:]==['snapshot']:print(json.dumps({'time':snapshot()['time']}))
|
||||
elif sys.argv[1:]==['report-preview']:report(True)
|
||||
elif sys.argv[1:]==['report']:report()
|
||||
else:raise SystemExit('observe | snapshot | report | report-preview')
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-off native plan setup and read-only diagnostics; never prints credentials."""
|
||||
from pathlib import Path
|
||||
import datetime,json,os,re,subprocess,time
|
||||
from zoneinfo import ZoneInfo
|
||||
BASE=Path(__file__).resolve().parent
|
||||
ROOT=BASE.parents[3]
|
||||
CONF=Path('/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf')
|
||||
TZ=ZoneInfo('Europe/Berlin')
|
||||
DATE=20261102
|
||||
FORMATION=6
|
||||
ACTIVITY=7
|
||||
RAID=[159,160,161,162,163,164,165,166,167,176]
|
||||
MEET={'alliance':'the_gilded_rose','horde':'poi_npc_287214'}
|
||||
def now():return datetime.datetime.now(TZ)
|
||||
def query(kind,sql):
|
||||
if not re.match(r'^\s*(SELECT|SHOW)\b',sql,re.I):raise ValueError('Read-only database helper')
|
||||
conf=CONF.read_text();value=re.search(r'^'+kind+r'DatabaseInfo\s*=\s*"([^"]+)"',conf,re.M).group(1)
|
||||
h,port,u,password,db=value.split(';');env=os.environ.copy();env['MYSQL_PWD']=password
|
||||
r=subprocess.run(['mysql','--protocol=TCP','--host='+h,'--port='+port,'--user='+u,'--database='+db,'--batch','--default-character-set=utf8mb4','--connect-timeout=10'],input=sql,text=True,capture_output=True,env=env,timeout=25)
|
||||
if r.returncode:raise RuntimeError(r.stderr.replace(password,'[redacted]'))
|
||||
lines=r.stdout.splitlines()
|
||||
return [dict(zip(lines[0].split('\t'),line.split('\t'))) for line in lines[1:]] if lines else []
|
||||
def command(text):
|
||||
subprocess.run(['tmux','send-keys','-t','vcore-worldserver','-l',text],check=True,timeout=5)
|
||||
subprocess.run(['tmux','send-keys','-t','vcore-worldserver','Enter'],check=True,timeout=5)
|
||||
def console(lines=2000):
|
||||
return subprocess.check_output(['tmux','capture-pane','-p','-J','-t','vcore-worldserver','-S',str(-lines)],text=True,timeout=10)
|
||||
def roster():
|
||||
rows=query('Character',"SELECT c.guid,c.name,c.race,c.level,c.online,c.map,c.instance_id,c.position_x,c.position_y,c.position_z,HEX(COALESCE(g.profile_lua,'')) profile FROM characters c LEFT JOIN vcore_adventure_generated_bot g ON g.character_guid=c.guid WHERE c.online=1 AND (c.guid=26 OR g.state='ready') ORDER BY c.guid;")
|
||||
for r in rows:
|
||||
for k in ['guid','race','level','online','map','instance_id']:r[k]=int(r[k])
|
||||
r['faction']='alliance' if r['race'] in [1,3,4,7,11,22] else 'horde'
|
||||
profile=bytes.fromhex(r.pop('profile')).decode(errors='replace')
|
||||
role=re.search(r'\brole\s*=\s*["\'](tank|healer|damage)',profile)
|
||||
r['role']=role[1] if role else ('tank' if r['guid']==26 else 'unknown')
|
||||
return rows
|
||||
def plans():return query('World',f'SELECT character_guid,hour_slot,intent,location_key FROM vcore_adventure_day WHERE local_date={DATE} AND hour_slot IN({FORMATION},{ACTIVITY}) ORDER BY character_guid,hour_slot;')
|
||||
def apply():
|
||||
rows=roster();p=BASE/'before-plans.json'
|
||||
if not p.exists():p.write_text(json.dumps(plans(),indent=2))
|
||||
current={(int(r['character_guid']),int(r['hour_slot'])):r for r in plans()}
|
||||
pending=[]
|
||||
for r in rows:
|
||||
g=r['guid']
|
||||
if r['level']<15:continue
|
||||
a=current.get((g,ACTIVITY),{});f=current.get((g,FORMATION),{})
|
||||
valid=a.get('intent') in ['raid','instance'] and f.get('intent')=='groupformation' and f.get('location_key')==MEET[r['faction']]
|
||||
if g in RAID:valid=valid and a.get('location_key')=='instance_visit_at_6354_d0' and a.get('intent')=='raid'
|
||||
if valid:continue
|
||||
pending.append(g)
|
||||
target='raid instance_visit_at_6354_d0' if g in RAID else 'instance-or-raid auto'
|
||||
command(f'clientless adventure editplan {g} {ACTIVITY} {target} both 15 85 apply')
|
||||
command(f'clientless adventure editplan {g} {FORMATION} groupformation {MEET[r["faction"]]} {r["faction"]} 15 85 apply')
|
||||
time.sleep(2 if pending else .1)
|
||||
current=plans();lookup={(int(r['character_guid']),int(r['hour_slot'])):r for r in current}
|
||||
eligible=[];missing=[]
|
||||
for r in rows:
|
||||
if r['level']<15:continue
|
||||
g=r['guid'];a=lookup.get((g,ACTIVITY),{});f=lookup.get((g,FORMATION),{})
|
||||
good=f.get('intent')=='groupformation' and f.get('location_key')==MEET[r['faction']] and a.get('intent') in ['raid','instance']
|
||||
if g in RAID:good=good and a.get('location_key')=='instance_visit_at_6354_d0' and a.get('intent')=='raid'
|
||||
(eligible if good else missing).append(g)
|
||||
manifest={'updated_at':now().isoformat(),'date':DATE,'formation_hour':FORMATION,'activity_hour':ACTIVITY,'evaluate_at':'2026-11-02T08:00:00+01:00','raid_guids':RAID,'meeting_locations':MEET,'roster':rows,'eligible_guids':eligible,'missing_plan_guids':missing,'excluded_under15':[r['guid'] for r in rows if r['level']<15],'plans':current}
|
||||
(BASE/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2))
|
||||
(BASE/('plan-apply-'+now().strftime('%H%M%S')+'.log')).write_text(console())
|
||||
print(json.dumps({'online':len(rows),'configured':len(eligible),'missing':missing,'newly_applied':pending,'excluded_under15':manifest['excluded_under15'],'raid_online':sorted(set(RAID)&{r['guid'] for r in rows})}))
|
||||
return manifest
|
||||
if __name__=='__main__':
|
||||
import sys
|
||||
if sys.argv[1:] == ['apply']:apply()
|
||||
elif sys.argv[1:]==['roster']:print(json.dumps(roster(),ensure_ascii=False,indent=2))
|
||||
@@ -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,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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"configured_at": "2026-11-03T18:59:42.951826+01:00",
|
||||
"date": 20261103,
|
||||
"formation_hour": 19,
|
||||
"raid_hour": 20,
|
||||
"timezone": "Europe/Berlin",
|
||||
"meeting": "the_gilded_rose",
|
||||
"raid": "instance_visit_at_6354_d0",
|
||||
"difficulty": "normal 10-player",
|
||||
"roster": [
|
||||
{
|
||||
"guid": 159,
|
||||
"name": "Renren",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 160,
|
||||
"name": "Marndra",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 161,
|
||||
"name": "Marnda",
|
||||
"level": 85,
|
||||
"role": "healer"
|
||||
},
|
||||
{
|
||||
"guid": 162,
|
||||
"name": "Aruendas",
|
||||
"level": 85,
|
||||
"role": "healer"
|
||||
},
|
||||
{
|
||||
"guid": 163,
|
||||
"name": "Winienwi",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 164,
|
||||
"name": "Taliaarnora",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 165,
|
||||
"name": "Brynda",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 166,
|
||||
"name": "Corric",
|
||||
"level": 85,
|
||||
"role": "damage"
|
||||
},
|
||||
{
|
||||
"guid": 167,
|
||||
"name": "Ragnorli",
|
||||
"level": 85,
|
||||
"role": "tank"
|
||||
},
|
||||
{
|
||||
"guid": 176,
|
||||
"name": "Ashaelri",
|
||||
"level": 85,
|
||||
"role": "tank"
|
||||
}
|
||||
],
|
||||
"commit": "a6572242c6",
|
||||
"observation": "user observes; no background observer"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
# Fahrstühle im TravelGraph erfassen
|
||||
|
||||
Diese Anleitung beschreibt den vorhandenen Recorder. Sie verändert weder die
|
||||
Fahrstuhlbewegung noch dessen Haltezeiten. Alle Aufnahmen erfolgen mit einem
|
||||
normalen GM-Client im Spiel; `SPAWN_ID` und `BOT_GUID` sind zu ersetzen.
|
||||
|
||||
## Technische Einordnung
|
||||
|
||||
Ein geeigneter Fahrstuhl ist ein natives Transport-GameObject, üblicherweise
|
||||
`GAMEOBJECT_TYPE_TRANSPORT` (Typ11). Schiffe und Zeppeline verwenden normalerweise
|
||||
`GAMEOBJECT_TYPE_MO_TRANSPORT` (Typ15). Beide stellen `TransportBase` bereit;
|
||||
daher werden dieselben Recorderbefehle verwendet. Ein Fahrstuhl ist dabei kein
|
||||
Unit-Fahrzeug mit Sitzplätzen: Der GM läuft auf die Plattform und muss vom Server
|
||||
als Passenger erkannt werden.
|
||||
|
||||
Der Graph bekommt je Haltestelle einen gemeinsamen Ein-/Ausstiegsnode auf festem
|
||||
Boden. Separat werden Bodenanschluss und Land-Plattform-Übergang erfasst. Die
|
||||
Fahrt selbst kommt aus der nativen Transportanimation.
|
||||
|
||||
```text
|
||||
Bodennetz unten ↔ lift_ort_unten ↔ [Fahrstuhl] ↔ lift_ort_oben ↔ Bodennetz oben
|
||||
```
|
||||
|
||||
## 1. Den richtigen Fahrstuhl bestimmen
|
||||
|
||||
Mit dem GM neben der Plattform stehen:
|
||||
|
||||
```text
|
||||
.clientless transport near 30
|
||||
```
|
||||
|
||||
Aus der Antwort `spawn=...` notieren. Das ist die konkrete **Spawn-ID**, nicht
|
||||
`entry=...` und nicht der vollständige Objekt-GUID.
|
||||
|
||||
```text
|
||||
.clientless transport inspect SPAWN_ID
|
||||
```
|
||||
|
||||
Vor der Aufnahme muss `stopped=1` gelten. `remainingPause` ist die verbleibende
|
||||
Haltezeit in Millisekunden; `4294967295` bedeutet einen zeitlich nicht begrenzten
|
||||
Halt. Die Aufnahme braucht einen echten Halt und eine gültige positive Periode.
|
||||
Ein sichtbarer Fahrstuhl mit `stopped=0` gilt noch nicht als sicher angedockt.
|
||||
|
||||
`transport services` listet die nativen Schiffs-/Zeppelinservices aus der
|
||||
`transports`-Tabelle. Für gewöhnliche Typ11-Fahrstühle stattdessen `near`/`inspect`
|
||||
verwenden.
|
||||
|
||||
## 2. Untere Haltestelle und Einstieg aufnehmen
|
||||
|
||||
Auf **festem Boden neben** dem unteren Einstieg stehen, nicht auf der beweglichen
|
||||
Plattform. Den Node so nah an der Einstiegskante setzen, dass er sicher erreichbar
|
||||
bleibt, wenn der Fahrstuhl gerade oben ist.
|
||||
|
||||
```text
|
||||
.clientless node add lift_ort_unten elevator
|
||||
```
|
||||
|
||||
Auf die nächste vollständige Ankunft des Fahrstuhls warten. Höchstens5 Yards vom
|
||||
Node entfernt auf festem Boden die Aufnahme starten:
|
||||
|
||||
```text
|
||||
.clientless transport record start lift_ort_unten SPAWN_ID
|
||||
```
|
||||
|
||||
Nun normal auf die Plattform und zu einem sicheren Platz laufen. Nicht fliegen,
|
||||
springen oder teleportieren. Noch während **desselben Halts** prüfen und speichern:
|
||||
|
||||
```text
|
||||
.clientless transport record info
|
||||
.clientless transport record stop lift_ort_mitte
|
||||
```
|
||||
|
||||
Vor dem Speichern muss `aboard=1` erscheinen. `stop` speichert den Übergang und
|
||||
legt den benannten Stehplatz bei Bedarf automatisch an. Eine separate
|
||||
`transport stand add`-Aufnahme ist für diesen Ablauf nicht nötig.
|
||||
|
||||
Nur den kurzen Weg vom Land auf die Plattform aufnehmen. **Nicht erst oben
|
||||
`record stop` ausführen:** Sobald der Fahrstuhl abfährt, wird die Aufnahme ungültig.
|
||||
Die gesamte Einstiegsstrecke darf höchstens60 Yards lang sein.
|
||||
|
||||
## 3. Obere Haltestelle aufnehmen
|
||||
|
||||
Mit demselben Fahrstuhl nach oben fahren, aussteigen und auf festem Boden den
|
||||
oberen Node anlegen:
|
||||
|
||||
```text
|
||||
.clientless node add lift_ort_oben elevator
|
||||
```
|
||||
|
||||
Am besten den nächsten vollständigen Halt abwarten, damit genügend Zeit bleibt.
|
||||
Vom oberen Node wieder auf **dieselbe Plattform und denselben lokalen Stehplatz**
|
||||
laufen und dort speichern:
|
||||
|
||||
```text
|
||||
.clientless transport record start lift_ort_oben SPAWN_ID
|
||||
.clientless transport record info
|
||||
.clientless transport record stop lift_ort_mitte
|
||||
.clientless transport stops
|
||||
```
|
||||
|
||||
Denselben Standnamen verwenden. Weil er für den Transport-Entry bereits existiert,
|
||||
muss der GM beim zweiten Speichern innerhalb1,5 Yards des gespeicherten lokalen
|
||||
Punkts stehen. Für eine Verbindung dürfen die lokalen Deck-Endpunkte beider
|
||||
Aufnahmen höchstens3 Yards auseinanderliegen. Ein Nachbaraufzug mit demselben
|
||||
Entry ist trotzdem ein anderes Fahrzeug: Beide Aufnahmen brauchen denselben Spawn.
|
||||
|
||||
Die Aufnahme wird beim Aussteigen umgekehrt abgespielt. Deshalb müssen beide
|
||||
Einstiegswege auch rückwärts begehbar sein.
|
||||
|
||||
## 4. Fahrtkante und Bodenanschlüsse anlegen
|
||||
|
||||
Für einen Fahrstuhl, der beide Richtungen bedient:
|
||||
|
||||
```text
|
||||
.clientless transport link lift_ort_unten lift_ort_oben elevator 100 both
|
||||
```
|
||||
|
||||
`both` erzeugt die Verbindung in beiden Richtungen. Ohne `both` wird nur die
|
||||
angegebene Richtung angelegt. Der Befehl prüft die vorhandenen Haltestellen und
|
||||
lädt den Graphen anschließend neu.
|
||||
|
||||
Die `100` ist weiterhin ein erforderlicher positiver Parameter des Linkbefehls.
|
||||
Seit dem Entfernungskostenmodell bestimmt dieser alte DB-Kostenwert **nicht**
|
||||
mehr die tatsächliche Routenbewertung. Diese verwendet die Länge der nativen
|
||||
Transportbewegung × Multiplikator mit der Variation der jeweiligen Reise.
|
||||
`elevator` verwendet aktuell denselben Multiplikator wie `tram`:
|
||||
`Clientless.Travel.CostMultiplier.Tram`. Es gibt noch keinen eigenen Elevator-Faktor.
|
||||
|
||||
Beide Etagen an vorhandene Bodennodes anbinden. Beispiel: auf einem bereits
|
||||
vorhandenen unteren Bodennode `ort_weg_unten` stehen:
|
||||
|
||||
```text
|
||||
.clientless route start ort_weg_unten lift_ort_unten
|
||||
```
|
||||
|
||||
Den normalen Bodenweg bis `lift_ort_unten` laufen, dann:
|
||||
|
||||
```text
|
||||
.clientless route stop
|
||||
```
|
||||
|
||||
Die Groundaufnahme wird bidirektional gespeichert. Oben entsprechend aufnehmen.
|
||||
**Keine Groundkante durch den Schacht und keine Groundaufnahme während der Fahrt.**
|
||||
Vollständig geprüfte MMAP-Anschlüsse sind ebenfalls möglich; eine aufgezeichnete
|
||||
Bodenanbindung macht den gewünschten Zugang ausdrücklich nachvollziehbar.
|
||||
|
||||
## 5. Beide Richtungen mit einem Bot prüfen
|
||||
|
||||
Einen untätigen CPC-Bot am unteren Node verwenden. Ein noch laufender Tagesauftrag
|
||||
wird durch `transport travel` nicht unterbrochen; für einen isolierten Test muss
|
||||
der vorherige Auftrag erst sicher beendet sein.
|
||||
|
||||
```text
|
||||
.clientless transport travel BOT_GUID lift_ort_oben
|
||||
.clientless transport status BOT_GUID
|
||||
.clientless actioninfo BOT_GUID
|
||||
```
|
||||
|
||||
Der erwartete Ablauf ist `Waiting → Boarding → Riding → Leaving → Arrived`.
|
||||
`Arrived` muss am oberen **Landnode** erreicht werden, nicht nur an Bord.
|
||||
Anschließend dieselbe Prüfung zurück nach `lift_ort_unten` durchführen und danach
|
||||
auch eine normale Weiterreise in das obere/untere Bodennetz testen.
|
||||
|
||||
`travel` fordert eine Reise zum Node über den normalen Planer an; es erzwingt
|
||||
nicht die Fahrstuhlkante. Für den ersten Test möglichst einen Bot ohne Freiflug
|
||||
und eine Strecke ohne günstigere Alternativverbindung wählen. Am Status prüfen,
|
||||
ob tatsächlich eine Transportfahrt gestartet wurde.
|
||||
|
||||
## Fehlerbehebung und Grenzen
|
||||
|
||||
- `spawn ... not loaded`: GM auf die richtige Map und in Sichtweite des Fahrstuhls
|
||||
bringen. Bei mehreren Plattformen den konkreten Spawn erneut mit `near` prüfen.
|
||||
- `aboard=0`: Der Server erkennt den GM nicht als Passenger. So lässt sich keine
|
||||
gültige Fahrstuhlverbindung speichern; reine sichtbare Mitbewegung genügt nicht.
|
||||
- Abfahrt vor `record stop`, falscher Stehplatz oder falscher Transport:
|
||||
`record info` lesen, dann `record cancel` und beim nächsten Halt neu aufnehmen.
|
||||
- `Dock already recorded`: Eine gespeicherte Aufnahme wird nicht überschrieben.
|
||||
Nur die betreffende Haltestelle mit `transport forget lift_ort_unten` entfernen
|
||||
und neu aufnehmen. Node und Bodenrouten bleiben dabei erhalten.
|
||||
- Wurde ein Haltestellennode verschoben, dessen Einstiegsaufnahme neu erstellen.
|
||||
- `both` ist kein Nachweis erfolgreicher Fahrten: Ein-/Ausstieg, beide Richtungen
|
||||
und verpasste Halte am echten Fahrstuhl testen.
|
||||
- Der Controller wartet auf Halte und fährt als Passenger mit. Er hat keine
|
||||
allgemeine Bedienlogik für Rufknöpfe, Hebel oder Etagenwahl. Manuell zu
|
||||
aktivierende Fahrstühle benötigen dafür gegebenenfalls eine zusätzliche Logik.
|
||||
- Beliebige Fahrstuhlmodelle haben nicht automatisch eine freie Decknavigation.
|
||||
Der gespeicherte gemeinsame Stehplatz bleibt deshalb wichtig.
|
||||
|
||||
Diagnosen stehen in `adventurebot-navigation.log`. `transport reload` lädt die
|
||||
Aufnahmen erneut; laufende Fahrten behalten ihre beim Start übernommenen Daten.
|
||||
|
||||
Grundlagen und weitere Verkehrsmittel: [transport-tools.md](transport-tools.md).
|
||||
|
||||
## Laufzeitverhalten der Bots
|
||||
|
||||
Bei nativen Fahrstühlen (GameObject-Typ 11) beginnen Einstieg und Ausstieg
|
||||
nur am aufgezeichneten Halt, wenn die Plattform tatsächlich steht. Der Einstieg
|
||||
beginnt erst bei mindestens 500 Millisekunden Reststandzeit; der Ausstieg
|
||||
benötigt keine Mindestreststandzeit. Fährt sie während eines
|
||||
Übergangs wieder los, bleibt eine vorhandene Fahrgastbindung erhalten.
|
||||
|
||||
Bleibt ein Bot fünf Minuten als Fahrgast hängen, bringt ihn ein nativer
|
||||
Teleport zum aufgezeichneten Zielnode seiner Fahrt: oben eingestiegen führt
|
||||
nach unten, unten eingestiegen nach oben. Fehlversuche oder Abbruch ändern
|
||||
dieses ursprüngliche Fallbackziel nicht. Schiffe, Zeppeline und die Tiefenbahn
|
||||
behalten ihre bisherigen Restzeitprüfungen und erhalten diesen Fallback nicht.
|
||||
|
||||
Bei einem nach Serverneustart übernommenen Fahrgast fehlt das ursprüngliche
|
||||
Einstiegsereignis. Für den Fallback wird dann die nächstgelegene aufgezeichnete
|
||||
Etage als Ausgangspunkt und ihre eindeutige Gegenetage als Ziel verwendet.
|
||||
@@ -176,10 +176,14 @@ entsprechend wichtig. Ein anderer Wagen ist eine andere Aufnahmeverbindung.
|
||||
.clientless transport link lift_lower lift_upper elevator 100 both
|
||||
```
|
||||
|
||||
Der Kostenwert ist ein positiver **Graphkostenwert**, keine automatisch mit
|
||||
Groundlängen vergleichbare Sekundenzahl. Zum ersten isolierten Funktionstest
|
||||
ist der genaue Wert unkritisch; konkurrierende Fernrouten benötigen später eine
|
||||
gemeinsame Kostenkalibrierung. `both` ist optional und erzeugt eine bidirektionale
|
||||
Der Linkbefehl verlangt weiterhin einen positiven Kostenparameter (hier100).
|
||||
Seit dem Entfernungskostenmodell wird der alte DB-Wert bei der Routenplanung
|
||||
jedoch durch die native Transportstrecke × Verkehrsmittel-Multiplikator und
|
||||
die Variation der jeweiligen Reise ersetzt. Fahrstühle verwenden derzeit den
|
||||
Tram-Multiplikator `Clientless.Travel.CostMultiplier.Tram`.
|
||||
|
||||
Für den vollständigen Fahrstuhlablauf siehe
|
||||
[fahrstuehle-aufzeichnen.md](fahrstuehle-aufzeichnen.md). `both` ist optional und erzeugt eine bidirektionale
|
||||
Kante. Es darf nur verwendet werden, wenn das reale Verkehrsmittel beide
|
||||
Richtungen bedient. Ohne `both` entsteht nur die angegebene Fahrtrichtung.
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Reisekosten
|
||||
|
||||
Der Planer bewertet Strecken nach Entfernung × Verkehrsmittel-Multiplikator.
|
||||
Geschwindigkeitsunterschiede werden wie vom Nutzer gewünscht ausschließlich über
|
||||
die Multiplikatoren abgebildet. Es wird kein Reisezeitmodell verwendet.
|
||||
|
||||
Aufgezeichnete Bodenrouten verwenden ihre Linienlänge einschließlich der
|
||||
Anschlüsse. Taxis verwenden native Flugpfadpunkte statt Fahrpreis oder alter
|
||||
Pauschalkosten (Sturmwind–Eisenschmiede, Kante 15: 6457.25 Yards).
|
||||
Schiffe, Zeppeline und Tram verwenden native Transportgeometrie; Kartenwechsel
|
||||
und Teleports erzeugen keine künstlichen Koordinatensprünge. Statische
|
||||
Transportanimationen berücksichtigen Spawnposition und ParentRotation.
|
||||
Freiflug und lokale Anschlüsse verwenden ihre geometrische Entfernung.
|
||||
Portale erhalten eine Basis von 1, da die Entfernung zwischen Karten keinen
|
||||
sinnvollen physischen Reiseweg beschreibt. Unmessbare Verbindungen werden nicht
|
||||
mit erfundenen niedrigen Kosten bevorzugt. Direkter Flug konkurriert mit dem
|
||||
Graphen, statt unabhängig von dessen Kosten sofort ausgewählt zu werden.
|
||||
|
||||
Konfiguration: `Clientless.Travel.CostMultiplier.Ground`, `.Taxi`, `.Flight`,
|
||||
`.Ship`, `.Zeppelin`, `.Tram`, `.Portal`; derzeit alle 1. Faktoren werden auf
|
||||
0.01 bis 100 begrenzt. `Clientless.Travel.CostVariation` ist derzeit 0.20
|
||||
(zulässig 0 bis 0.90). Pro Reise wird jeder Verkehrsmittelfaktor einmal unabhängig
|
||||
variiert; innerhalb der Reise bleiben die Gewichte stabil. Ein neues Ziel oder
|
||||
eine nach Abschluss begonnene Reise erhält ein neues Profil. Fehlgeschlagene
|
||||
Wiederholungen und Recovery derselben Reise behalten das bestehende Profil.
|
||||
|
||||
Gruppenmitglieder übernehmen Route und Kostenprofil des Anführers. Auch
|
||||
Anschlussplanung und Recovery verwenden dieses Profil. Native Logs enthalten
|
||||
Journey-Seeds und Faktoren zur Überprüfung; eine gemeinsame Makroroute garantiert
|
||||
keine räumlich versetzte Kolonnenbewegung.
|
||||
|
||||
Validierung: acht gezielte Regressionstests bestanden, CMake-Build mit -j10 und
|
||||
Installation erfolgreich. Native Taxi-/Transportentfernungen und gemeinsame
|
||||
Journey-Seeds wurden beobachtet. Der Gruppentest 02.11.2026 um 06/07 Uhr prüft
|
||||
die tatsächlichen Reiseergebnisse; erfolgreicher Build ist keine Gameplay-Abnahme.
|
||||
Build-/Testlogs: `checkpoints/2026-11-02-travel-costs/`.
|
||||
@@ -285,19 +285,9 @@ namespace VMAP
|
||||
fclose(rf);
|
||||
return LoadResult::FileNotFound;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string tilefile = basePath + getTileFileName(mapID, tileX, tileY);
|
||||
FILE* tf = fopen(tilefile.c_str(), "rb");
|
||||
if (!tf)
|
||||
result = LoadResult::FileNotFound;
|
||||
else
|
||||
{
|
||||
if (!readChunk(tf, chunk, VMAP_MAGIC, 8))
|
||||
result = LoadResult::VersionMismatch;
|
||||
fclose(tf);
|
||||
}
|
||||
}
|
||||
if (!readChunk(tf, chunk, VMAP_MAGIC, 8))
|
||||
result = LoadResult::VersionMismatch;
|
||||
fclose(tf);
|
||||
fclose(rf);
|
||||
return result;
|
||||
}
|
||||
@@ -349,15 +339,13 @@ namespace VMAP
|
||||
|
||||
void StaticMapTree::UnloadMap(VMapManager2* vm)
|
||||
{
|
||||
for (loadedSpawnMap::iterator i = iLoadedSpawns.begin(); i != iLoadedSpawns.end(); ++i)
|
||||
while (!iLoadedTiles.empty())
|
||||
{
|
||||
for (uint32 refCount = 0; refCount < i->second; ++refCount)
|
||||
vm->releaseModelInstance(iTreeValues[i->first].getWorldModel()->GetName());
|
||||
|
||||
iTreeValues[i->first].setUnloaded();
|
||||
uint32 tileX, tileY;
|
||||
unpackTileID(iLoadedTiles.begin()->first, tileX, tileY);
|
||||
UnloadMapTile(tileX, tileY, vm);
|
||||
}
|
||||
iLoadedSpawns.clear();
|
||||
iLoadedTiles.clear();
|
||||
}
|
||||
|
||||
//=========================================================
|
||||
@@ -369,7 +357,13 @@ namespace VMAP
|
||||
TC_LOG_ERROR("misc", "StaticMapTree::LoadMapTile() : tree has not been initialized [%u, %u]", tileX, tileY);
|
||||
return LoadResult::ReadFromFileFailed;
|
||||
}
|
||||
uint32 tileID = packTileID(tileX, tileY);
|
||||
auto loaded = iLoadedTiles.find(tileID);
|
||||
if (loaded != iLoadedTiles.end())
|
||||
return loaded->second.Result;
|
||||
|
||||
LoadResult result = LoadResult::FileNotFound;
|
||||
LoadedTile tile;
|
||||
|
||||
TileFileOpenResult fileResult = OpenMapTileFile(iBasePath, iMapID, tileX, tileY, vm);
|
||||
if (fileResult.File)
|
||||
@@ -388,29 +382,38 @@ namespace VMAP
|
||||
ModelSpawn spawn;
|
||||
if (ModelSpawn::readFromFile(fileResult.File, spawn))
|
||||
{
|
||||
// acquire model instance
|
||||
WorldModel* model = vm->acquireModelInstance(iBasePath, spawn.name, spawn.flags);
|
||||
if (!model)
|
||||
TC_LOG_ERROR("misc", "StaticMapTree::LoadMapTile() : could not acquire WorldModel pointer [%u, %u]", tileX, tileY);
|
||||
|
||||
// update tree
|
||||
auto spawnIndex = iSpawnIndices.find(spawn.ID);
|
||||
if (spawnIndex != iSpawnIndices.end())
|
||||
{
|
||||
uint32 referencedVal = spawnIndex->second;
|
||||
if (referencedVal >= iNTreeValues)
|
||||
{
|
||||
TC_LOG_ERROR("maps", "StaticMapTree::LoadMapTile() : invalid tree element (%u/%u) referenced in tile %s", referencedVal, iNTreeValues, fileResult.Name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
WorldModel* model = vm->acquireModelInstance(iBasePath, spawn.name, spawn.flags);
|
||||
if (!model)
|
||||
{
|
||||
TC_LOG_ERROR("misc", "StaticMapTree::LoadMapTile() : could not acquire WorldModel pointer [%u, %u]", tileX, tileY);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!iLoadedSpawns.count(referencedVal))
|
||||
{
|
||||
if (referencedVal >= iNTreeValues)
|
||||
{
|
||||
TC_LOG_ERROR("maps", "StaticMapTree::LoadMapTile() : invalid tree element (%u/%u) referenced in tile %s", referencedVal, iNTreeValues, fileResult.Name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
iTreeValues[referencedVal] = ModelInstance(spawn, model);
|
||||
iLoadedSpawns[referencedVal] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (iTreeValues[referencedVal].getWorldModel() != model)
|
||||
{
|
||||
TC_LOG_ERROR("maps", "StaticMapTree::LoadMapTile() : conflicting model for spawn %u in tile %s", spawn.ID, fileResult.Name.c_str());
|
||||
vm->releaseModelInstance(spawn.name);
|
||||
result = LoadResult::ReadFromFileFailed;
|
||||
continue;
|
||||
}
|
||||
++iLoadedSpawns[referencedVal];
|
||||
#ifdef VMAP_DEBUG
|
||||
if (iTreeValues[referencedVal].ID != spawn.ID)
|
||||
@@ -419,6 +422,7 @@ namespace VMAP
|
||||
TC_LOG_DEBUG("maps", "StaticMapTree::LoadMapTile() : name collision on GUID=%u", spawn.ID);
|
||||
#endif
|
||||
}
|
||||
tile.Models.push_back({ std::move(spawn.name), referencedVal });
|
||||
}
|
||||
else if (int32(iMapID) == fileResult.UsedMapId)
|
||||
{
|
||||
@@ -435,11 +439,10 @@ namespace VMAP
|
||||
result = LoadResult::ReadFromFileFailed;
|
||||
}
|
||||
}
|
||||
iLoadedTiles[packTileID(tileX, tileY)] = true;
|
||||
fclose(fileResult.File);
|
||||
}
|
||||
else
|
||||
iLoadedTiles[packTileID(tileX, tileY)] = false;
|
||||
tile.Result = result;
|
||||
iLoadedTiles.emplace(tileID, std::move(tile));
|
||||
TC_METRIC_EVENT("map_events", "LoadMapTile",
|
||||
"Map: " + std::to_string(iMapID) + " TileX: " + std::to_string(tileX) + " TileY: " + std::to_string(tileY));
|
||||
return result;
|
||||
@@ -456,49 +459,17 @@ namespace VMAP
|
||||
VMAP_ERROR_LOG("misc", "StaticMapTree::UnloadMapTile() : trying to unload non-loaded tile - Map:%u X:%u Y:%u", iMapID, tileX, tileY);
|
||||
return;
|
||||
}
|
||||
if (tile->second) // file associated with tile
|
||||
for (LoadedModelReference const& model : tile->second.Models)
|
||||
{
|
||||
TileFileOpenResult fileResult = OpenMapTileFile(iBasePath, iMapID, tileX, tileY, vm);
|
||||
if (fileResult.File)
|
||||
auto referencedNode = iLoadedSpawns.find(model.TreeIndex);
|
||||
if (referencedNode == iLoadedSpawns.end())
|
||||
VMAP_ERROR_LOG("misc", "StaticMapTree::UnloadMapTile() : trying to unload non-referenced model '%s'", model.Name.c_str());
|
||||
else if (--referencedNode->second == 0)
|
||||
{
|
||||
bool result=true;
|
||||
char chunk[8];
|
||||
if (!readChunk(fileResult.File, chunk, VMAP_MAGIC, 8))
|
||||
result = false;
|
||||
uint32 numSpawns;
|
||||
if (fread(&numSpawns, sizeof(uint32), 1, fileResult.File) != 1)
|
||||
result = false;
|
||||
for (uint32 i=0; i<numSpawns && result; ++i)
|
||||
{
|
||||
// read model spawns
|
||||
ModelSpawn spawn;
|
||||
result = ModelSpawn::readFromFile(fileResult.File, spawn);
|
||||
if (result)
|
||||
{
|
||||
// release model instance
|
||||
vm->releaseModelInstance(spawn.name);
|
||||
|
||||
// update tree
|
||||
auto spawnIndex = iSpawnIndices.find(spawn.ID);
|
||||
if (spawnIndex == iSpawnIndices.end())
|
||||
result = false;
|
||||
else
|
||||
{
|
||||
uint32 referencedNode = spawnIndex->second;
|
||||
if (!iLoadedSpawns.count(referencedNode))
|
||||
VMAP_ERROR_LOG("misc", "StaticMapTree::UnloadMapTile() : trying to unload non-referenced model '%s' (ID:%u)", spawn.name.c_str(), spawn.ID);
|
||||
else if (--iLoadedSpawns[referencedNode] == 0)
|
||||
{
|
||||
iTreeValues[referencedNode].setUnloaded();
|
||||
iLoadedSpawns.erase(referencedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (int32(iMapID) == fileResult.UsedMapId) // logic documented in StaticMapTree::LoadMapTile
|
||||
result = false;
|
||||
}
|
||||
fclose(fileResult.File);
|
||||
iTreeValues[model.TreeIndex].setUnloaded();
|
||||
iLoadedSpawns.erase(referencedNode);
|
||||
}
|
||||
vm->releaseModelInstance(model.Name);
|
||||
}
|
||||
iLoadedTiles.erase(tile);
|
||||
TC_METRIC_EVENT("map_events", "UnloadMapTile",
|
||||
|
||||
@@ -48,7 +48,17 @@ namespace VMAP
|
||||
|
||||
class TC_COMMON_API StaticMapTree
|
||||
{
|
||||
typedef std::unordered_map<uint32, bool> loadedTileMap;
|
||||
struct LoadedModelReference
|
||||
{
|
||||
std::string Name;
|
||||
uint32 TreeIndex;
|
||||
};
|
||||
struct LoadedTile
|
||||
{
|
||||
LoadResult Result;
|
||||
std::vector<LoadedModelReference> Models;
|
||||
};
|
||||
typedef std::unordered_map<uint32, LoadedTile> loadedTileMap;
|
||||
typedef std::unordered_map<uint32, uint32> loadedSpawnMap;
|
||||
private:
|
||||
uint32 iMapID;
|
||||
@@ -59,7 +69,8 @@ namespace VMAP
|
||||
|
||||
// Store all the map tile idents that are loaded for that map
|
||||
// some maps are not splitted into tiles and we have to make sure, not removing the map before all tiles are removed
|
||||
// empty tiles have no tile file, hence map with bool instead of just a set (consistency check)
|
||||
// Keep precisely the references acquired by each tile. A failed load
|
||||
// must never release another tile's or a dynamic object's model.
|
||||
loadedTileMap iLoadedTiles;
|
||||
std::vector<std::pair<int32, int32>> iLoadedPrimaryTiles;
|
||||
// stores <tree_index, reference_count> to invalidate tree values, unload map, and to be able to report errors
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
#include "BattlefieldMgr.h"
|
||||
#include "Containers.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "SpellAuraEffects.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -141,6 +143,57 @@ bool BattlefieldMgr::IsWorldPvpArea(uint32 zoneId) const
|
||||
return std::find(BattlefieldIdToZoneId.begin(), BattlefieldIdToZoneId.end(), zoneId) != BattlefieldIdToZoneId.end();
|
||||
}
|
||||
|
||||
bool BattlefieldMgr::IsWintergraspFlightAllowed(uint32 mapId, uint32 areaId) const
|
||||
{
|
||||
// Cataclysm's static mount flags omit flight throughout Wintergrasp.
|
||||
// The actual restriction is temporary and belongs to the battle state.
|
||||
if (mapId != 571 || !DBCManager::IsInArea(areaId, 4197))
|
||||
return false;
|
||||
for (auto const& [map, battlefields] : _battlefieldsByMap)
|
||||
if (map->GetId() == mapId)
|
||||
for (auto const& battlefield : battlefields)
|
||||
if (battlefield->GetZoneId() == 4197 && battlefield->IsEnabled() &&
|
||||
!battlefield->CanFlyIn())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BattlefieldMgr::UpdateWintergraspFlight(Player* player) const
|
||||
{
|
||||
// The client also needs this native mount-permission aura; server-only
|
||||
// AreaTable exceptions do not enable every mount in the client UI.
|
||||
constexpr uint32 flightPermission = 91262;
|
||||
bool allowed = IsWintergraspFlightAllowed(player->GetMapId(), player->GetAreaId());
|
||||
uint32 otherRestrictions = 0;
|
||||
bool hasOtherRestrictions = false;
|
||||
for (AuraEffect const* effect : player->GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS))
|
||||
if (effect->GetId() != flightPermission)
|
||||
{
|
||||
hasOtherRestrictions = true;
|
||||
otherRestrictions |= uint32(effect->GetMiscValue());
|
||||
}
|
||||
if (hasOtherRestrictions && !(otherRestrictions & uint32(AreaMountFlags::AllowFlyingMounts)))
|
||||
allowed = false;
|
||||
if (allowed)
|
||||
{
|
||||
if (!player->HasAura(flightPermission))
|
||||
player->AddAura(flightPermission, player);
|
||||
}
|
||||
else
|
||||
player->RemoveAurasDueToSpell(flightPermission);
|
||||
}
|
||||
|
||||
void BattlefieldMgr::UpdateWintergraspFlight(Map* map) const
|
||||
{
|
||||
for (auto const& reference : map->GetPlayers())
|
||||
if (Player* player = reference.GetSource())
|
||||
if (player->GetZoneId() == 4197)
|
||||
{
|
||||
UpdateWintergraspFlight(player);
|
||||
player->UpdateMountCapability();
|
||||
}
|
||||
}
|
||||
|
||||
Battlefield* BattlefieldMgr::GetBattlefieldToZoneId(Map const* map, uint32 zoneId)
|
||||
{
|
||||
auto itr = _battlefieldsByZone.find({ map, zoneId });
|
||||
|
||||
@@ -52,6 +52,9 @@ class TC_GAME_API BattlefieldMgr
|
||||
void HandlePlayerLeaveZone(Player* player, uint32 zoneId);
|
||||
|
||||
bool IsWorldPvpArea(uint32 zoneId) const;
|
||||
bool IsWintergraspFlightAllowed(uint32 mapId, uint32 areaId) const;
|
||||
void UpdateWintergraspFlight(Player* player) const;
|
||||
void UpdateWintergraspFlight(Map* map) const;
|
||||
|
||||
// return assigned battlefield
|
||||
Battlefield* GetBattlefieldToZoneId(Map const* map, uint32 zoneId);
|
||||
|
||||
@@ -7086,6 +7086,7 @@ void Player::UpdateArea(uint32 newArea)
|
||||
else
|
||||
RemoveRestFlag(REST_FLAG_IN_FACTION_AREA);
|
||||
|
||||
sBattlefieldMgr->UpdateWintergraspFlight(this);
|
||||
UpdateMountCapability();
|
||||
}
|
||||
|
||||
|
||||
@@ -7911,7 +7911,11 @@ MountCapabilityEntry const* Unit::GetMountCapability(uint32 mountType) const
|
||||
mountFlags |= AreaMountFlags(auraEffect->GetMiscValue());
|
||||
}
|
||||
else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(areaId))
|
||||
{
|
||||
mountFlags = areaTable->GetMountFlags();
|
||||
if (sBattlefieldMgr->IsWintergraspFlightAllowed(GetMapId(), areaId))
|
||||
mountFlags |= AreaMountFlags::AllowFlyingMounts;
|
||||
}
|
||||
|
||||
ZLiquidStatus liquidStatus = GetLiquidStatus();
|
||||
isUnderwater = (liquidStatus & LIQUID_MAP_UNDER_WATER) != 0;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include "SpellInfo.h"
|
||||
#include "Battleground.h"
|
||||
#include "BattlefieldMgr.h"
|
||||
#include "Corpse.h"
|
||||
#include "Creature.h"
|
||||
#include "DBCStores.h"
|
||||
@@ -1797,7 +1798,11 @@ SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 a
|
||||
mountFlags |= AreaMountFlags(auraEffect->GetMiscValue());
|
||||
}
|
||||
else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(area_id))
|
||||
{
|
||||
mountFlags = areaTable->GetMountFlags();
|
||||
if (sBattlefieldMgr->IsWintergraspFlightAllowed(map_id, area_id))
|
||||
mountFlags |= AreaMountFlags::AllowFlyingMounts;
|
||||
}
|
||||
|
||||
if (!(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts)))
|
||||
return SPELL_FAILED_INCORRECT_AREA;
|
||||
|
||||
@@ -510,7 +510,11 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
|
||||
if ((r.Intent=="instance" || r.Intent=="raid"))
|
||||
{
|
||||
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
|
||||
Queue(player,r,"travel_to_location","id",l->BaseKey.empty()?r.Location:l->BaseKey,true);
|
||||
// The entrance is a first-class TravelGraph transition. Route to its
|
||||
// source node instead of an approximate catalog point; the native
|
||||
// AreaTrigger radius remains the final arrival contract.
|
||||
Queue(player,r,"travel_to_node","node",
|
||||
"portal_at_"+std::to_string(r.Instance.Entry)+"_source",true);
|
||||
}
|
||||
else if (l->IsArea)
|
||||
{
|
||||
@@ -1619,11 +1623,36 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
if (Busy(guid)) return;
|
||||
auto status = sClientlessActionExecutorMgr->GetStatus(guid);
|
||||
auto location = sAdventureLocationMgr->Find(r.Location);
|
||||
bool arrived = status && status->State == ClientlessActionExecutionState::Completed && location && player->GetMapId() == location->Point.MapId;
|
||||
if (arrived && location->IsArea) arrived = location->AreaId &&(player->GetAreaId() == location->AreaId || player->GetZoneId() == location->AreaId) && status->Detail != "best-effort travel completed at nearest useful reachable point";
|
||||
else if (arrived) arrived = player->GetExactDist(location->Point.X, location->Point.Y, location->Point.Z) <= 6;
|
||||
bool const completed=status && status->State==ClientlessActionExecutionState::Completed;
|
||||
bool arrived=completed && location && player->GetMapId()==location->Point.MapId;
|
||||
char const* arrivalContract="catalog-radius";
|
||||
if (arrived && (r.Intent=="instance" || r.Intent=="raid") && r.Instance.Active)
|
||||
{
|
||||
arrivalContract="native-area-trigger";
|
||||
auto entrance=sAreaTriggerStore.LookupEntry(r.Instance.Entry);
|
||||
arrived=entrance && player->GetMapId()==uint32(entrance->ContinentID) &&
|
||||
player->IsInAreaTriggerRadius(entrance);
|
||||
}
|
||||
else if (arrived && location->IsArea)
|
||||
{
|
||||
arrivalContract="area-or-zone";
|
||||
arrived=location->AreaId && (player->GetAreaId()==location->AreaId ||
|
||||
player->GetZoneId()==location->AreaId) &&
|
||||
status->Detail!="best-effort travel completed at nearest useful reachable point";
|
||||
}
|
||||
else if (arrived)
|
||||
arrived=player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z)<=6;
|
||||
if (! arrived)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"INTENT_ARRIVAL_REJECT guid=%u intent='%s' location='%s' contract='%s' completed=%u status='%s' map=%u expectedMap=%u distance=%.2f entry=%u inTrigger=%u",
|
||||
guid,r.Intent.c_str(),r.Location.c_str(),arrivalContract,completed?1u:0u,
|
||||
status?status->Detail.c_str():"missing",player->GetMapId(),
|
||||
location?location->Point.MapId:0u,
|
||||
location?player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z):-1.0f,
|
||||
r.Instance.Active?r.Instance.Entry:0u,
|
||||
(r.Instance.Active && sAreaTriggerStore.LookupEntry(r.Instance.Entry) &&
|
||||
player->IsInAreaTriggerRadius(sAreaTriggerStore.LookupEntry(r.Instance.Entry)))?1u:0u);
|
||||
Fallback(player, r);
|
||||
return;
|
||||
}
|
||||
@@ -1632,7 +1661,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
r.State = Phase::OnSite;
|
||||
r.Detail = "performing intent at destination";
|
||||
sClientlessLuaEngine->UpdateWorldPresence(player,r.Intent,true);
|
||||
TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s'", guid, r.Intent.c_str(), r.Location.c_str());
|
||||
TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s' contract='%s'", guid, r.Intent.c_str(), r.Location.c_str(),arrivalContract);
|
||||
}
|
||||
if (r.State == Phase::OnSite)
|
||||
{
|
||||
@@ -1695,6 +1724,10 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
{
|
||||
std::string args;std::getline(in,args);return EditPlan(handler,args);
|
||||
}
|
||||
if(op=="groupmanager")
|
||||
{
|
||||
std::string args;std::getline(in,args);return sAdventureGroupMgr->Command(handler,args.c_str());
|
||||
}
|
||||
if (op == "import") return sAdventureLocationMgr->Import(handler);
|
||||
if (op == "repairimport") return sAdventureLocationMgr->Import(handler,true);
|
||||
if (op == "reload")
|
||||
@@ -2184,6 +2217,33 @@ bool AdventureBehaviorMgr::ProtectGroupLeave(Player* player) const
|
||||
r.State==Phase::Handover;
|
||||
}
|
||||
|
||||
bool AdventureBehaviorMgr::ProtectInstanceGroupLeave(Player* player) const
|
||||
{
|
||||
auto group=player->GetGroup();
|
||||
if(!group)return false;
|
||||
auto now=GameTime::GetGameTime();
|
||||
for(auto reference=group->GetFirstMember();reference;reference=reference->next())
|
||||
{
|
||||
auto member=reference->GetSource();
|
||||
if(!member || !member->IsInWorld())continue;
|
||||
// The leader can already be waiting inside while late members travel.
|
||||
// This also covers human leaders without an AdventureBot record.
|
||||
if(member->GetMap()->IsDungeon())return true;
|
||||
auto it=_records.find(member->GetGUID().GetCounter());
|
||||
if(it==_records.end())continue;
|
||||
auto const& r=it->second;
|
||||
if(r.Instance.Active)
|
||||
{
|
||||
if(r.Instance.Stage!=AdventureInstancePolicy::Phase::Rest)return true;
|
||||
continue;
|
||||
}
|
||||
if((r.Intent=="raid" || r.Intent=="instance") && r.Deadline>now)
|
||||
if(auto location=sAdventureLocationMgr->Find(r.Location))
|
||||
if(location->InstanceMap)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
|
||||
{
|
||||
InterruptForCombat(player);
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
std::string InteractionIntent(Player* player) const;
|
||||
std::string InteractionSchedule(Player* player);
|
||||
bool ProtectGroupLeave(Player* player) const;
|
||||
bool ProtectInstanceGroupLeave(Player* player) const;
|
||||
private:
|
||||
bool EditPlan(ChatHandler* handler, std::string const& args);
|
||||
enum class Phase {Idle, Travel, OnSite, Switching, Handover, WaitingGroup};
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "GroupMgr.h"
|
||||
#include "LFGPackets.h"
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -339,6 +340,189 @@ bool AdventureGroupMgr::Merge(Player* p,AdventureGroupPlan const& plan)
|
||||
}
|
||||
return true; // A mutation occurred; caller must not reuse its old Group pointer.
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::ManagedTransfer(Player* leader,Player* member)
|
||||
{
|
||||
if(!leader || !member || leader==member || !Ready(leader) || !Ready(member) ||
|
||||
leader->GetTeam()!=member->GetTeam() || !CanInvite(leader,member) || !NeedsRole(leader,member))return false;
|
||||
auto target=leader->GetGroup(),source=member->GetGroup();
|
||||
ObjectGuid originalLeader=source?source->GetLeaderGUID():ObjectGuid::Empty;
|
||||
if(target && target->GetLeaderGUID()!=leader->GetGUID())return false;
|
||||
if(source==target)return false;
|
||||
if(source)
|
||||
{
|
||||
if(!BotLed(member) || source->isLFGGroup())return false;
|
||||
if(source->GetLeaderGUID()==member->GetGUID() && source->GetMembersCount()>1)
|
||||
{
|
||||
Player* successor=nullptr;
|
||||
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
|
||||
if(auto candidate=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(candidate->GetSession() && candidate->GetSession()->IsClientless()){successor=candidate;break;}
|
||||
if(!successor)return false; // Never hand forced leadership to a real player.
|
||||
source->ChangeLeader(successor->GetGUID());source->SendUpdate();
|
||||
}
|
||||
}
|
||||
CancelBrowser(member);
|
||||
ObjectGuid old=source?source->GetGUID():ObjectGuid::Empty;
|
||||
ObjectGuid oldLeader=originalLeader;
|
||||
bool oldRaid=source && source->isRaidGroup();
|
||||
auto oldDungeonMode=source?source->GetDungeonDifficulty():DUNGEON_DIFFICULTY_NORMAL;
|
||||
auto oldRaidMode=source?source->GetRaidDifficulty():RAID_DIFFICULTY_10MAN_NORMAL;
|
||||
std::vector<ObjectGuid> oldMembers;
|
||||
if(source)for(auto const& slot:source->GetMemberSlots())oldMembers.push_back(slot.guid);
|
||||
if(source)source->RemoveMember(member->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
|
||||
Invite(leader,member,Activity(sAdventureBehaviorMgr->GroupPlan(leader)),true);
|
||||
if(member->GetGroupInvite())
|
||||
{
|
||||
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};
|
||||
response.Accept=true;member->GetSession()->HandlePartyInviteResponseOpcode(response);
|
||||
}
|
||||
if(member->GetGroup() && member->GetGroup()==leader->GetGroup())
|
||||
{
|
||||
_records[member->GetGUID()].MergeAt=GameTime::GetGameTime()+120;
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_TRANSFER member=%u from=%s leader=%u target='%s'",
|
||||
member->GetGUID().GetCounter(),old.ToString().c_str(),leader->GetGUID().GetCounter(),
|
||||
sAdventureBehaviorMgr->GroupPlan(leader).NextLocation.c_str());
|
||||
return true;
|
||||
}
|
||||
// Native invitation rejection must not strand a member removed from a
|
||||
// still-existing source group. Reuse the same bounded rollback as organic
|
||||
// merging, including recreation of a disbanded two-bot source.
|
||||
if(old)
|
||||
{
|
||||
if(auto previous=sGroupMgr->GetGroupByGUID(old.GetCounter()))
|
||||
{
|
||||
previous->AddMember(member);
|
||||
if(oldLeader==member->GetGUID())previous->ChangeLeader(member->GetGUID());
|
||||
previous->SendUpdate();
|
||||
}
|
||||
else if(oldMembers.size()==2)
|
||||
{
|
||||
auto peer=ObjectAccessor::FindPlayer(oldMembers[0]==member->GetGUID()?oldMembers[1]:oldMembers[0]);
|
||||
if(peer && !peer->GetGroup() && !peer->GetGroupInvite())
|
||||
{
|
||||
auto restored=new Group();auto owner=oldLeader==member->GetGUID()?member:peer;
|
||||
if(restored->Create(owner))
|
||||
{
|
||||
sGroupMgr->AddGroup(restored);if(oldRaid)restored->ConvertToRaid();
|
||||
restored->SetDungeonDifficulty(oldDungeonMode);restored->SetRaidDifficulty(oldRaidMode);
|
||||
restored->AddMember(owner==member?peer:member);restored->SendUpdate();
|
||||
}
|
||||
else delete restored;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned AdventureGroupMgr::RunFormationManager(ChatHandler* handler,bool apply,bool forced)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();unsigned candidates=0,moves=0;
|
||||
auto forming=[&](Player* p)
|
||||
{
|
||||
if(!Ready(p) || !p->GetSession()->IsClientless() || p->isUsingLfg() || p->InBattlegroundQueue() || p->GetGroupInvite())return false;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
|
||||
if(!plan.Enabled || !plan.Forming || !AdventureGroupPolicy::Eligible(plan.NextIntent) || (!forced && !AdventureGroupPolicy::ManagerWindow(plan.Deadline,now)))return false;
|
||||
if(auto group=p->GetGroup())
|
||||
{
|
||||
if(!BotLed(p) || group->isLFGGroup())return false;
|
||||
if(Complete(p))return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
std::vector<Player*> pool;
|
||||
for(auto const& entry:_records)if(auto p=ObjectAccessor::FindPlayer(entry.first))if(forming(p))pool.push_back(p);
|
||||
candidates=pool.size();
|
||||
auto groupSize=[](Player* p){return p->GetGroup()?p->GetGroup()->GetMembersCount():1u;};
|
||||
auto leaderOf=[](Player* p)->Player*
|
||||
{
|
||||
if(auto group=p->GetGroup())return ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
||||
return p;
|
||||
};
|
||||
for(unsigned pass=0;pass<pool.size();++pass)
|
||||
{
|
||||
std::vector<Player*> leaders;
|
||||
for(auto p:pool)if(forming(p))
|
||||
{
|
||||
auto leader=leaderOf(p);if(!leader || !forming(leader))continue;
|
||||
if(std::find(leaders.begin(),leaders.end(),leader)==leaders.end())leaders.push_back(leader);
|
||||
}
|
||||
std::sort(leaders.begin(),leaders.end(),[&](Player* a,Player* b)
|
||||
{auto sa=groupSize(a),sb=groupSize(b);return sa!=sb?sa>sb:a->GetGUID()<b->GetGUID();});
|
||||
bool changed=false;
|
||||
for(auto leader:leaders)
|
||||
{
|
||||
if(!forming(leader))continue;
|
||||
auto targetPlan=sAdventureBehaviorMgr->GroupPlan(leader);auto targetGroup=leader->GetGroup();
|
||||
if((targetGroup?targetGroup->GetMembersCount():1)>=Capacity(targetPlan))continue;
|
||||
Player* best=nullptr;unsigned bestSource=999;
|
||||
for(auto member:pool)
|
||||
{
|
||||
if(member==leader || !forming(member) || member->GetGroup()==targetGroup)continue;
|
||||
auto memberPlan=sAdventureBehaviorMgr->GroupPlan(member);
|
||||
if(Activity(memberPlan)!=Activity(targetPlan) || member->GetTeam()!=leader->GetTeam() ||
|
||||
!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),leader->getLevel()))continue;
|
||||
bool levels=true;if(targetGroup)for(auto const& slot:targetGroup->GetMemberSlots())
|
||||
if(auto present=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),present->getLevel())){levels=false;break;}
|
||||
if(!levels || !ProfessionCompatible(leader,member,Activity(targetPlan)) || !CanInvite(leader,member) || !NeedsRole(leader,member))continue;
|
||||
auto source=member->GetGroup();unsigned sourceSize=source?source->GetMembersCount():1;
|
||||
if(source && source->GetLeaderGUID()==member->GetGUID() && sourceSize>1)
|
||||
{
|
||||
bool botSuccessor=false;
|
||||
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
|
||||
if(auto successor=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(successor->GetSession() && successor->GetSession()->IsClientless()){botSuccessor=true;break;}
|
||||
if(!botSuccessor)continue;
|
||||
}
|
||||
if(targetGroup && sourceSize>=targetGroup->GetMembersCount())continue;
|
||||
if(sourceSize<bestSource || (sourceSize==bestSource && (!best || member->GetGUID()<best->GetGUID())))
|
||||
{best=member;bestSource=sourceSize;}
|
||||
}
|
||||
if(!best)continue;
|
||||
if(handler)handler->PSendSysMessage("Group manager %s: member=%u '%s' -> leader=%u '%s' target='%s' role=%u",
|
||||
apply?"move":"proposal",best->GetGUID().GetCounter(),best->GetName().c_str(),leader->GetGUID().GetCounter(),leader->GetName().c_str(),
|
||||
targetPlan.NextLocation.c_str(),unsigned(BrowserRole(best)));
|
||||
if(!apply){++moves;pool.erase(std::remove(pool.begin(),pool.end(),best),pool.end());changed=true;break;}
|
||||
if(ManagedTransfer(leader,best)){++moves;changed=true;break;}
|
||||
}
|
||||
if(!changed)break;
|
||||
}
|
||||
_formationManagerLast=now;_formationManagerMoves=moves;_formationManagerCandidates=candidates;
|
||||
_formationManagerDetail=apply?"applied":"preview";
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_RUN apply=%u forced=%u candidates=%u moves=%u",apply?1:0,forced?1:0,candidates,moves);
|
||||
if(handler)handler->PSendSysMessage("Group manager %s: candidates=%u moves=%u.",apply?"applied":"preview",candidates,moves);
|
||||
return moves;
|
||||
}
|
||||
|
||||
void AdventureGroupMgr::MaybeRunFormationManager(time_t now)
|
||||
{
|
||||
if(now<_formationManagerAt)return;
|
||||
_formationManagerAt=now+30;
|
||||
bool due=false;
|
||||
for(auto const& entry:_records)if(auto player=ObjectAccessor::FindPlayer(entry.first))
|
||||
{
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(player);
|
||||
if(plan.Enabled && plan.Forming && AdventureGroupPolicy::ManagerWindow(plan.Deadline,now))
|
||||
{due=true;break;}
|
||||
}
|
||||
if(!due)return;
|
||||
RunFormationManager(nullptr,true,false);
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::Command(ChatHandler* handler,char const* args)
|
||||
{
|
||||
std::istringstream in(args?args:"");std::string op;in>>op;
|
||||
if(op=="preview"){RunFormationManager(handler,false,true);return true;}
|
||||
if(op=="run"){RunFormationManager(handler,true,true);return true;}
|
||||
if(op=="status")
|
||||
{
|
||||
handler->PSendSysMessage("Group manager last=%llu candidates=%u moves=%u mode='%s' nextAutomatic=%llu",
|
||||
static_cast<unsigned long long>(_formationManagerLast),_formationManagerCandidates,_formationManagerMoves,_formationManagerDetail.c_str(),
|
||||
static_cast<unsigned long long>(_formationManagerAt));return true;
|
||||
}
|
||||
handler->SendSysMessage("Usage: clientless groupmanager preview|run|status");return false;
|
||||
}
|
||||
void AdventureGroupMgr::CheckFullGroup(Player* p)
|
||||
{
|
||||
auto group=p->GetGroup();if(!group)return;
|
||||
@@ -361,6 +545,7 @@ void AdventureGroupMgr::CheckFullGroup(Player* p)
|
||||
void AdventureGroupMgr::Update(Player* p)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
|
||||
MaybeRunFormationManager(now);
|
||||
CheckFullGroup(p);
|
||||
if(PlayerLeadership(p))return;
|
||||
if(!Ready(p) || p->isUsingLfg() || p->InBattlegroundQueue())return;
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
bool Forming(Player* player) const;
|
||||
bool PlannedTransition(Player* player);
|
||||
void Report(ChatHandler* handler,Player* player) const;
|
||||
bool Command(ChatHandler* handler,char const* args);
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
@@ -50,6 +51,9 @@ private:
|
||||
unsigned PendingInvites(Player* leader,Player* except=nullptr) const;
|
||||
bool SurplusRole(Player* member);
|
||||
bool Merge(Player* player,AdventureGroupPlan const& plan);
|
||||
bool ManagedTransfer(Player* leader,Player* member);
|
||||
unsigned RunFormationManager(ChatHandler* handler,bool apply,bool forced);
|
||||
void MaybeRunFormationManager(time_t now);
|
||||
void CheckFullGroup(Player* player);
|
||||
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
|
||||
void Speak(Player* player,std::string const& message);
|
||||
@@ -57,6 +61,9 @@ private:
|
||||
bool RecruitPlayer(Player* player,std::string const& intent);
|
||||
std::unordered_map<ObjectGuid,time_t> _playerInvites;
|
||||
time_t _nextChannelMessage=0;
|
||||
time_t _formationManagerAt=0,_formationManagerLast=0;
|
||||
unsigned _formationManagerMoves=0,_formationManagerCandidates=0;
|
||||
std::string _formationManagerDetail="not run";
|
||||
std::unordered_map<ObjectGuid,Record> _records;
|
||||
};
|
||||
#define sAdventureGroupMgr AdventureGroupMgr::instance()
|
||||
|
||||
@@ -40,6 +40,13 @@ inline bool AcceptPlayerLevel(unsigned bot,unsigned player)
|
||||
{return bot<=player || bot-player<=5;}
|
||||
inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other)
|
||||
{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);}
|
||||
inline bool ManagerLevelCompatible(unsigned level,unsigned other)
|
||||
{
|
||||
if(level==85 || other==85)return level==85 && other==85;
|
||||
return level>other?level-other<=5:other-level<=5;
|
||||
}
|
||||
inline bool ManagerWindow(long long deadline,long long now)
|
||||
{return deadline>now && deadline-now<=600;}
|
||||
template<class Day,class Meeting>
|
||||
void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting)
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
@@ -471,10 +473,25 @@ bool AdventureGroupTravelMgr::ValidateGroundPath(Player* player,std::vector<Posi
|
||||
{
|
||||
float t=float(step)/steps,x=a.GetPositionX()+(b.GetPositionX()-a.GetPositionX())*t,y=a.GetPositionY()+(b.GetPositionY()-a.GetPositionY())*t,z=a.GetPositionZ()+(b.GetPositionZ()-a.GetPositionZ())*t;
|
||||
float floor=player->GetMap()->GetHeight(player->GetPhaseShift(),x,y,z+2,true,6);
|
||||
if(floor<=INVALID_HEIGHT || !AdventureGroupTravelPolicy::GroundSample(z,floor,last,length/steps))return false;
|
||||
if(floor<=INVALID_HEIGHT || !AdventureGroupTravelPolicy::GroundSample(z,floor,last,length/steps))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"GROUP_GROUND_REJECT guid=%u map=%u check='ground' segment=%u sample=%u/%u position=(%.3f %.3f %.3f) floor=%.3f previousFloor=%.3f heightError=%.3f floorStep=%.3f allowedHeightError=%.3f allowedFloorStep=%.3f",
|
||||
player->GetGUID().GetCounter(),player->GetMapId(),uint32(i),step,steps,x,y,z,floor,last,
|
||||
std::fabs(floor-z),std::fabs(floor-last),AdventureGroupTravelPolicy::MaxGroundStep,
|
||||
AdventureGroupTravelPolicy::MaxGroundStep+AdventureGroupTravelPolicy::MaxGroundSlope*length/steps);
|
||||
return false;
|
||||
}
|
||||
last=floor;
|
||||
}
|
||||
if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),a.GetPositionX(),a.GetPositionY(),a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))return false;
|
||||
if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),a.GetPositionX(),a.GetPositionY(),a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"GROUP_GROUND_REJECT guid=%u map=%u check='line-of-sight' segment=%u from=(%.3f %.3f %.3f) to=(%.3f %.3f %.3f)",
|
||||
player->GetGUID().GetCounter(),player->GetMapId(),uint32(i),a.GetPositionX(),a.GetPositionY(),
|
||||
a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
if(protectedFormation){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);}
|
||||
bool protectedTravel=sAdventureGroupTravelMgr->Protected(p) || p->isUsingLfg() || p->InBattlegroundQueue() || p->InBattleground();
|
||||
if(protectedTravel){r.LastActivity=now;r.AfkSince=0;}
|
||||
bool protectedInstance=sAdventureBehaviorMgr->ProtectInstanceGroupLeave(p);
|
||||
if(protectedInstance){r.LastActivity=now;r.AfkSince=0;}
|
||||
if(!protectedFormation && !protectedTravel && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
|
||||
{
|
||||
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
|
||||
@@ -186,8 +188,8 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
|
||||
if(!sAdventureGroupMgr->PlannedTransition(p) && frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
|
||||
}
|
||||
if(sAdventureGroupTravelMgr->Separated(p)){Leave(p,r,"leader-distance");return false;}
|
||||
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
|
||||
if(!protectedInstance && sAdventureGroupTravelMgr->Separated(p)){Leave(p,r,"leader-distance");return false;}
|
||||
if(!protectedInstance && AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
|
||||
}
|
||||
// Existing warrior executor supplies damage assistance. Roles/healing
|
||||
// deliberately remain a later profile-driven extension.
|
||||
|
||||
@@ -206,8 +206,43 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
|
||||
return itr != _records.end() ? &itr->second.Status : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::OwnsActiveFlight(uint32 characterGuid) const
|
||||
{
|
||||
auto itr = _records.find(characterGuid);
|
||||
return itr != _records.end() && itr->second.Active && itr->second.FlightActive;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
if(action.Name=="travel_to_node" || action.Name=="travel_to_location")
|
||||
{
|
||||
auto goal=action.Parameters.find(action.Name=="travel_to_node"?"node":"id");
|
||||
std::string key=action.Name+":"+(goal==action.Parameters.end()?std::string():goal->second);
|
||||
// Inherit preferences even while the leader's route is still being
|
||||
// published. The shared-route timeout must not roll a follower's own
|
||||
// modal preference before a local catch-up plan.
|
||||
ClientlessTravelCost::Profile const* inherited=nullptr;
|
||||
auto requested=action.Parameters.find("route_source");
|
||||
auto group=player->GetGroup();
|
||||
if(requested!=action.Parameters.end() && group)
|
||||
{
|
||||
uint32 sourceGuid=uint32(std::strtoul(requested->second.c_str(),nullptr,10));
|
||||
auto source=_records.find(sourceGuid);
|
||||
if(group->GetLeaderGUID().GetCounter()==sourceGuid && sourceGuid!=player->GetGUID().GetCounter() &&
|
||||
source!=_records.end() && source->second.HasJourneyCosts && source->second.JourneyCostKey==key)
|
||||
inherited=&source->second.JourneyCosts;
|
||||
}
|
||||
if(inherited || !record.HasJourneyCosts || record.JourneyCostKey!=key || record.Status.State==ClientlessActionExecutionState::Completed)
|
||||
{
|
||||
record.JourneyCosts=inherited?*inherited:sClientlessTravelGraphMgr->NewJourneyCosts();
|
||||
record.JourneyCostKey=key;record.HasJourneyCosts=true;
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"TRAVEL_JOURNEY_COSTS guid=%u seed=%u goal='%s' ground=%.3f taxi=%.3f flight=%.3f ship=%.3f zeppelin=%.3f tram=%.3f portal=%.3f",
|
||||
player->GetGUID().GetCounter(),record.JourneyCosts.Seed,key.c_str(),
|
||||
record.JourneyCosts.Factors[0],record.JourneyCosts.Factors[1],record.JourneyCosts.Factors[2],
|
||||
record.JourneyCosts.Factors[3],record.JourneyCosts.Factors[4],record.JourneyCosts.Factors[5],record.JourneyCosts.Factors[6]);
|
||||
}
|
||||
}
|
||||
record.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
|
||||
record.TransportActive = false;
|
||||
record.FlightActive = false;
|
||||
@@ -629,7 +664,7 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
|
||||
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, destinationNode, startRadius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys))
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_node' destination='%s' reason='%s'",
|
||||
@@ -821,7 +856,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, anchorNode, startRadius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal))
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"C820_LOCATION_PLAN_FAILED location='%s' reason='%s'",
|
||||
@@ -984,6 +1019,12 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
}
|
||||
if(!access)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"ROUTE_ACCESS_DENIED guid=%u name='%s' step=%u/%u node='%s' edgeType='%s' "
|
||||
"edgeDbId=%u nodeAllowed=%u",
|
||||
characterGuid, player->GetName().c_str(), uint32(record.RouteStep + 1),
|
||||
uint32(record.Route.size()), step.NodeKey.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, accessNode && sAdventureAreaMgr->AllowsNode(player,*accessNode) ? 1u : 0u);
|
||||
record.Status.State=ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail="route denied by area/node catalog";record.Active=false;
|
||||
return false;
|
||||
@@ -1025,7 +1066,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
}
|
||||
Position target;target.Relocate(step.X,step.Y,step.Z,step.O);
|
||||
if (!validSource || step.MapId!=player->GetMapId() ||
|
||||
!sClientlessFlightMgr->Start(player,target,error))
|
||||
!sClientlessFlightMgr->Start(player,target,error,true))
|
||||
{
|
||||
ClientlessTravelTelemetry::Note(player,"flight-start-rejected",error+" "+ClientlessTravelTelemetry::State(player));
|
||||
return RecoverFlightLeg(player,characterGuid,record,error.empty()?"free flight source not physically reached":error);
|
||||
@@ -1522,6 +1563,8 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
portalSourceApproach))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
if (TryRouteApproachFlight(player, characterGuid, record))
|
||||
return true;
|
||||
record.Status.Detail = "travel start failed for route node " + step.NodeKey;
|
||||
record.Active = false;
|
||||
return false;
|
||||
@@ -1577,7 +1620,11 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
record.Status.Detail = reason;
|
||||
return false;
|
||||
};
|
||||
if(portal && !sClientlessTravelGraphMgr->PortalPrerequisites(player,step.EdgeDatabaseId))
|
||||
bool const nativePrerequisites = portal &&
|
||||
sClientlessTravelGraphMgr->PortalNativePrerequisites(player,step.EdgeDatabaseId);
|
||||
bool const adventureUnlock = portal && !nativePrerequisites &&
|
||||
sClientlessTravelGraphMgr->AdventurePortalUnlockAllowed(player,step.EdgeDatabaseId);
|
||||
if(portal && !nativePrerequisites && !adventureUnlock)
|
||||
return fail("native portal prerequisites not satisfied");
|
||||
if (!portal || !record.RouteStep || step.EdgeReverse ||
|
||||
player->GetMapId() != portal->Source.MapId || !player->IsAlive() ||
|
||||
@@ -1620,11 +1667,27 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "PORTAL_USE_START guid=%u edge=%u spawn=%u entry=%u spell=%u sourceMap=%u targetMap=%u",
|
||||
characterGuid, step.EdgeDatabaseId, portal->SpawnId, portal->Entry, portal->SpellId,
|
||||
portal->Source.MapId, step.MapId);
|
||||
// Same packet handler as a player's click. Native Use/AI, party checks,
|
||||
// spell cast restrictions and scripted conditions remain authoritative.
|
||||
WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8);
|
||||
usePacket << object->GetGUID();
|
||||
player->GetSession()->HandleGameObjectUseOpcode(usePacket);
|
||||
if (adventureUnlock)
|
||||
{
|
||||
// These performative bots never complete quests. Reproduce the fixed,
|
||||
// importer-validated destination of the bound public portal while the
|
||||
// normal executor still verifies source object, range, faction/area
|
||||
// policy and the resulting teleport sequence/arrival coordinates.
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"PORTAL_ADVENTURE_UNLOCK guid=%u level=%u edge=%u spawn=%u",
|
||||
characterGuid, player->getLevel(), step.EdgeDatabaseId, portal->SpawnId);
|
||||
if (!player->TeleportTo(portal->Target.MapId, portal->Target.X,
|
||||
portal->Target.Y, portal->Target.Z, portal->Target.O))
|
||||
return fail("adventure portal unlock teleport failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same packet handler as a player's click. Native Use/AI, party checks,
|
||||
// spell cast restrictions and scripted conditions remain authoritative.
|
||||
WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8);
|
||||
usePacket << object->GetGUID();
|
||||
player->GetSession()->HandleGameObjectUseOpcode(usePacket);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1822,8 +1885,11 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
|
||||
size_t original=path.size();
|
||||
if(!adapter->second(player,path))
|
||||
{
|
||||
player->StopMoving();record.Active=false;record.Status.State=ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail="recorded group road rejected by safety adapter";return false;
|
||||
player->StopMoving();
|
||||
uint32 const failedEdge = record.RouteStep < record.Route.size()
|
||||
? record.Route[record.RouteStep].EdgeDatabaseId : 0;
|
||||
return RecoverTravel(player,characterGuid,record,failedEdge,
|
||||
"recorded group road rejected by safety adapter");
|
||||
}
|
||||
if(path.size()<2)
|
||||
{if(player->IsSplineEnabled())player->StopMoving();record.FormationWaiting=true;record.Status.State=ClientlessActionExecutionState::Moving;record.Status.Detail="waiting for leading formation row";return true;}
|
||||
@@ -1916,7 +1982,7 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
|
||||
if (!std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target, true)))
|
||||
return false;
|
||||
std::string error;
|
||||
if (!sClientlessFlightMgr->Start(player, target, error))
|
||||
if (!sClientlessFlightMgr->Start(player, target, error, true))
|
||||
{
|
||||
ClientlessTravelTelemetry::Note(player,"flight-fallback-rejected",error+" "+ClientlessTravelTelemetry::State(player));
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_FALLBACK_UNAVAILABLE guid=%u reason='%s'",
|
||||
@@ -1998,7 +2064,7 @@ void ClientlessActionExecutorMgr::CheckGroundExit(Player* player, uint32 charact
|
||||
if (std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target)))
|
||||
{
|
||||
std::string error;
|
||||
if (sClientlessFlightMgr->Start(player, target, error))
|
||||
if (sClientlessFlightMgr->Start(player, target, error, true))
|
||||
{
|
||||
record.GroundExitActive = true;
|
||||
record.FlightActive = true;
|
||||
@@ -2192,6 +2258,42 @@ bool ClientlessActionExecutorMgr::TryRescueTravel(Player* player,uint32 characte
|
||||
return StartRouteStep(player,characterGuid,record);
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::TryRouteApproachFlight(Player* player, uint32 characterGuid, Record& record)
|
||||
{
|
||||
if (record.RouteStep >= record.Route.size())
|
||||
return false;
|
||||
auto const& step = record.Route[record.RouteStep];
|
||||
// A local connector may be flown without changing the group's macro route.
|
||||
// Recorded roads and transport edges retain their own execution semantics.
|
||||
if (step.MapId != player->GetMapId() || step.EdgeDatabaseId ||
|
||||
(step.EdgeType != "start" && step.EdgeType != "local_bridge") ||
|
||||
player->GetExactDist(step.X, step.Y, step.Z) < 5.0f)
|
||||
return false;
|
||||
std::string const key = "approach-flight:" + step.NodeKey;
|
||||
if (!record.FailedBridgeKeys.insert(key).second)
|
||||
return false;
|
||||
Position target;
|
||||
target.Relocate(step.X, step.Y, step.Z, step.O);
|
||||
std::string error;
|
||||
// Start validates native flying permissions, collision, solid landing and
|
||||
// an MMAP connection from the landing to the actual (possibly indoor) node.
|
||||
if (!sClientlessFlightMgr->Start(player, target, error, true))
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "ROUTE_APPROACH_FLIGHT_REJECT guid=%u node='%s' reason='%s'",
|
||||
characterGuid, step.NodeKey.c_str(), error.c_str());
|
||||
return false;
|
||||
}
|
||||
record.DeferredStartActive = false;
|
||||
record.EdgePath.clear();
|
||||
record.FlightActive = true;
|
||||
record.Active = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.Detail = "flying to existing route node after rejected ground approach";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "ROUTE_APPROACH_FLIGHT guid=%u node='%s' step=%u",
|
||||
characterGuid, step.NodeKey.c_str(), uint32(record.RouteStep));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
Player* player,
|
||||
uint32 characterGuid,
|
||||
@@ -2204,10 +2306,20 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
|
||||
if (reason.rfind("route-local-", 0) == 0 && TryRouteApproachFlight(player, characterGuid, record))
|
||||
return true;
|
||||
|
||||
if(record.RescueActive)
|
||||
{record.RescueActive=false;record.RescueWalkFailed=true;}
|
||||
if (failedEdgeDatabaseId)
|
||||
record.FailedEdgeDatabaseIds.insert(failedEdgeDatabaseId);
|
||||
// StartTravel can accept a request whose first spline is subsequently
|
||||
// rejected by the owner validator. Remember dynamic bridges here as well.
|
||||
if (!failedEdgeDatabaseId && reason.rfind("route-local-", 0) == 0 &&
|
||||
record.RouteStep > 0 && record.RouteStep < record.Route.size() &&
|
||||
record.Route[record.RouteStep].EdgeType == "local_bridge")
|
||||
record.FailedBridgeKeys.insert(record.Route[record.RouteStep - 1].NodeKey +
|
||||
"->" + record.Route[record.RouteStep].NodeKey);
|
||||
// A failed initial local approach must not be tried five times at the same spot.
|
||||
if (!failedEdgeDatabaseId && record.RouteStep == 0 && !record.Route.empty() &&
|
||||
reason.rfind("route-local-", 0) == 0)
|
||||
@@ -2303,7 +2415,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, anchorNode, radius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal);
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts);
|
||||
}
|
||||
else
|
||||
error = "no graph node exists on destination map";
|
||||
@@ -2319,7 +2431,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys);
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts);
|
||||
|
||||
if (planned)
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
@@ -2658,7 +2770,10 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
float dz = player->GetPositionZ() - destination.Z;
|
||||
float remaining = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
if (remaining > 2.0f)
|
||||
bool const portalApproach = record.RouteStep + 1 < record.Route.size() &&
|
||||
record.Route[record.RouteStep + 1].EdgeType == "portal_use";
|
||||
float const arrivalTolerance = portalApproach ? 5.0f : 2.0f;
|
||||
if (remaining > arrivalTolerance)
|
||||
{
|
||||
uint32 const failedEdgeId =
|
||||
record.RouteStep < record.Route.size()
|
||||
@@ -2999,6 +3114,11 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
||||
auto const snapshot=source->second;
|
||||
auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node");
|
||||
if(goal==action.Parameters.end() || goal->second!=(action.Name=="travel_to_location"?snapshot.FinalLocationId:snapshot.PlannedDestinationNode))return false;
|
||||
// Catch-up planning and later recoveries share the leader's sampled modal
|
||||
// costs. A follower must never choose a new transport preference here.
|
||||
record.JourneyCosts=snapshot.JourneyCosts;
|
||||
record.JourneyCostKey=snapshot.JourneyCostKey;
|
||||
record.HasJourneyCosts=snapshot.HasJourneyCosts;
|
||||
auto start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1);
|
||||
auto anchor=route[start];
|
||||
std::vector<ClientlessTravelRouteStep> joined;
|
||||
@@ -3008,7 +3128,7 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
||||
else
|
||||
{
|
||||
bool complete=false;std::string reached,error;
|
||||
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error) || !complete)return false;
|
||||
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error,nullptr,nullptr,nullptr,&record.JourneyCosts) || !complete)return false;
|
||||
}
|
||||
joined.insert(joined.end(),route.begin()+start+1,route.end());
|
||||
record.Route=std::move(joined);record.RouteStep=0;
|
||||
@@ -3018,6 +3138,6 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
||||
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
|
||||
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
|
||||
record.HardDestination=snapshot.HardDestination;record.Active=true;
|
||||
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()));
|
||||
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u journeySeed=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()),record.JourneyCosts.Seed);
|
||||
return StartRouteStep(player,player->GetGUID().GetCounter(),record);
|
||||
}
|
||||
@@ -50,12 +50,16 @@ public:
|
||||
void SetGroundPathAdapter(uint32 guid,GroundPathAdapter adapter);
|
||||
bool RequestStopAtNode(uint32 characterGuid);
|
||||
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
|
||||
bool OwnsActiveFlight(uint32 characterGuid) const;
|
||||
|
||||
static char const* StateName(ClientlessActionExecutionState state);
|
||||
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
ClientlessTravelCost::Profile JourneyCosts;
|
||||
std::string JourneyCostKey;
|
||||
bool HasJourneyCosts = false;
|
||||
ClientlessActionExecutionStatus Status;
|
||||
bool Active = false;
|
||||
std::shared_ptr<ClientlessAction> PendingShared;
|
||||
@@ -139,6 +143,7 @@ private:
|
||||
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
|
||||
bool GetTravelGoal(Record const& record, ClientlessTravelNode& goal) const;
|
||||
bool TryFallbackFlight(Player* player, uint32 characterGuid, Record& record);
|
||||
bool TryRouteApproachFlight(Player* player, uint32 characterGuid, Record& record);
|
||||
bool TryRescueTravel(Player* player, uint32 characterGuid, Record& record);
|
||||
void CheckGroundExit(Player* player, uint32 characterGuid, Record& record);
|
||||
void UpdateGroundExit(Player* player, uint32 characterGuid, Record& record);
|
||||
|
||||
@@ -66,6 +66,18 @@ namespace
|
||||
if(!ClearRay(player,{x+offset.X,y+offset.Y,surface.Ground+0.8f},{x+offset.X,y+offset.Y,top}))return false;
|
||||
return true;
|
||||
}
|
||||
bool OpenColumnFrom(Player* player,float x,float y,float z,float height)
|
||||
{
|
||||
// A height query from MAX_HEIGHT can legitimately return a bridge,
|
||||
// tree canopy or other collision surface far above the player. Such a
|
||||
// surface is useful as an obstacle, but it is not the player's support
|
||||
// floor. Test the actual take-off column from the player's feet.
|
||||
if (!std::isfinite(z) || height < 5.0f) return false;
|
||||
for (auto offset : std::array<Point,5>{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}})
|
||||
if (!ClearRay(player,{x+offset.X,y+offset.Y,z+0.8f},
|
||||
{x+offset.X,y+offset.Y,z+height})) return false;
|
||||
return true;
|
||||
}
|
||||
char const * StateName(ClientlessFlightState state)
|
||||
{
|
||||
switch (state)
|
||||
@@ -147,7 +159,10 @@ float ClientlessFlightMgr::Estimate(Player const * player, uint32 map, Position
|
||||
uint32 const area = AreaZone(player, map, from).first;
|
||||
if (auto const * data = sAreaTableStore.LookupEntry(area))
|
||||
{
|
||||
bool const dynamicRules = data->World_pvp_ID != 0 ||(map == player->GetMapId() && area == player->GetAreaId() && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS));
|
||||
bool const wintergrasp = map == 571 && DBCManager::IsInArea(area, 4197);
|
||||
bool const wintergraspFlight = sBattlefieldMgr->IsWintergraspFlightAllowed(map, area);
|
||||
if (wintergrasp && !wintergraspFlight) return Infinity();
|
||||
bool const dynamicRules = wintergraspFlight || data->World_pvp_ID != 0 ||(map == player->GetMapId() && area == player->GetAreaId() && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS));
|
||||
if (! dynamicRules && ! data->GetMountFlags().HasFlag(AreaMountFlags::AllowFlyingMounts)) return Infinity();
|
||||
}
|
||||
// Unknown terrain permission is an estimate only; native mount capability
|
||||
@@ -196,14 +211,22 @@ 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()
|
||||
};
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error)
|
||||
{return Begin(player,target,error,false);}
|
||||
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error,bool actionOwned)
|
||||
{
|
||||
bool started=Begin(player,target,error,false);
|
||||
if(started)
|
||||
if(auto found=_records.find(player->GetGUID().GetCounter());found!=_records.end())found->second.ActionOwned=actionOwned;
|
||||
return started;
|
||||
}
|
||||
bool ClientlessFlightMgr::PrepareFollowingMount(Player* player)
|
||||
{
|
||||
if(!Enabled(player) || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() ||
|
||||
@@ -268,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;
|
||||
@@ -313,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()))
|
||||
@@ -393,13 +445,30 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
|
||||
bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f &&
|
||||
player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5});
|
||||
float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
|
||||
if (!origin.Valid || (!airborne && (!std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3)))
|
||||
bool const localSupport=std::isfinite(feet) && feet>INVALID_HEIGHT+1 &&
|
||||
std::fabs(feet-player->GetPositionZ())<=3;
|
||||
bool const overheadSurface=origin.Valid && origin.Ground>player->GetPositionZ()+3;
|
||||
bool const openFromFeet=overheadSurface && OpenColumnFrom(player,player->GetPositionX(),
|
||||
player->GetPositionY(),player->GetPositionZ(),
|
||||
std::min(record.Settings.Clearance,origin.Ground-player->GetPositionZ()-1.0f));
|
||||
// Not falling/in water was verified above. If the global height sample
|
||||
// selected a remote surface above us, a clear local vertical column is a
|
||||
// valid departure even though that remote surface cannot be our floor.
|
||||
bool const supportedDeparture=localSupport || openFromFeet;
|
||||
if (!origin.Valid || (!airborne && !supportedDeparture))
|
||||
{
|
||||
if(std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true;
|
||||
if(localSupport && ApproachDeparture(player,record,following))return true;
|
||||
error = "departure-unavailable: no supported open takeoff point";
|
||||
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u mounted=%u canFly=%u",player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,origin.Valid?1:0,player->IsMounted()?1:0,player->CanFly()?1:0);
|
||||
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u localSupport=%u overhead=%u openFromFeet=%u mounted=%u canFly=%u",
|
||||
player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,
|
||||
origin.Valid?1:0,localSupport?1:0,overheadSurface?1:0,openFromFeet?1:0,
|
||||
player->IsMounted()?1:0,player->CanFly()?1:0);
|
||||
return false;
|
||||
}
|
||||
if(openFromFeet)
|
||||
ClientlessTravelTelemetry::Note(player,"flight-departure-overhead-surface",
|
||||
"z="+std::to_string(player->GetPositionZ())+" surface="+std::to_string(origin.Ground)+
|
||||
" feet="+std::to_string(feet));
|
||||
bool foundLanding = false;
|
||||
std::array<uint32,5> landingRejected{}; // surface, water, column, MMAP, area
|
||||
auto landingSummary=[&]()
|
||||
@@ -462,17 +531,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)
|
||||
@@ -484,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");
|
||||
@@ -552,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;
|
||||
}
|
||||
@@ -588,6 +662,26 @@ void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string con
|
||||
|
||||
bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff)
|
||||
{
|
||||
if(record.Following && !record.Returning)
|
||||
{
|
||||
while(record.Following->Replacement)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -613,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;
|
||||
}
|
||||
@@ -628,9 +723,21 @@ 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);
|
||||
if(shared.Waiting)return true;
|
||||
auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray,
|
||||
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
|
||||
@@ -647,6 +754,18 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
}
|
||||
if (! next.Valid ||(! record.Returning && record.Status.Travelled > Distance(PointOf(record.Start), goal) * 2 + 500))
|
||||
{
|
||||
// A detached/pathological flight gets one fresh airborne plan. If the
|
||||
// new corridor is still impossible, finish with a checked local
|
||||
// touchdown so the ordinary behavior controller can plan from ground.
|
||||
if(!record.Returning && record.DetachedRecoveryCount && FindReturnLanding(player,record))
|
||||
{
|
||||
record.Returning=true;
|
||||
record.AbortReason="airborne recovery could not reach the original target";
|
||||
record.Breadcrumbs.clear();
|
||||
record.Memory={};record.Memory.Departed=true;
|
||||
record.Status.Detail="airborne recovery landing";
|
||||
return Advance(player,record);
|
||||
}
|
||||
Fail(player, record, "no safe bounded flight corridor; holding position mounted");
|
||||
return false;
|
||||
}
|
||||
@@ -661,18 +780,29 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
init.SetFly();
|
||||
init.SetWalk(false);
|
||||
init.SetVelocity(player->GetSpeed(MOVE_FLIGHT));
|
||||
if (init.Launch() <= 0)
|
||||
int32 const duration = init.Launch();
|
||||
if (duration <= 0)
|
||||
{
|
||||
Fail(player, record, "flight spline rejected");
|
||||
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.Progress.Begin(Distance(current, next.Next), uint32(duration));
|
||||
++ record.Status.Segments;
|
||||
record.Status.Avoidances += next.Avoiding ? 1 : 0;
|
||||
if (handoff) ++record.Status.Handoffs;
|
||||
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing ? "landing approach" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" : next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
|
||||
bool const vertical=Horizontal(current,next.Next)<1.0f && std::fabs(next.Next.Z-current.Z)>1.0f;
|
||||
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";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_SEGMENT guid=%u n=%u avoid=%u handoff=%u target=(%.2f %.2f %.2f) detail='%s'", player->GetGUID().GetCounter(), record.Status.Segments, next.Avoiding ? 1 : 0, handoff ? 1 : 0, next.Next.X, next.Next.Y, next.Next.Z, record.Status.Detail.c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -709,6 +839,8 @@ void ClientlessFlightMgr::Update(uint32 diff)
|
||||
// Count actual motion, not overlapping issued spline lengths.
|
||||
record.Status.Travelled += Distance(PointOf(record.LastPosition), PointOf(*player));
|
||||
record.LastPosition.Relocate(player);
|
||||
if (record.Status.State == ClientlessFlightState::Flying && record.Moving)
|
||||
record.Progress.Observe(Distance(PointOf(*player), PointOf(record.Segment)), diff);
|
||||
if (record.Status.State == ClientlessFlightState::Mounting)
|
||||
{
|
||||
if (player->IsMounted() && player->CanFly()) record.Status.State = ClientlessFlightState::Flying;
|
||||
@@ -724,6 +856,23 @@ void ClientlessFlightMgr::Update(uint32 diff)
|
||||
Fail(player, record, "flight permission lost or total timeout");
|
||||
continue;
|
||||
}
|
||||
bool const ownerLost=record.ActionOwned && !sClientlessActionExecutorMgr->OwnsActiveFlight(guid);
|
||||
bool const pathological=record.Status.Segments>=12 && record.Status.Avoidances*2>=record.Status.Segments;
|
||||
if(!record.Returning && !record.Following && record.DetachedRecoveryCount==0 && (ownerLost || pathological))
|
||||
{
|
||||
++record.DetachedRecoveryCount;
|
||||
if(ownerLost)record.ActionOwned=false;
|
||||
player->StopMoving();record.Moving=false;record.SegmentElapsed=0;
|
||||
record.Start.Relocate(player);record.LastPosition.Relocate(player);
|
||||
record.Memory={};record.Breadcrumbs.clear();
|
||||
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)",
|
||||
guid,ownerLost?1:0,record.Status.Segments,record.Status.Avoidances,record.Status.Landing.GetPositionX(),record.Status.Landing.GetPositionY(),record.Status.Landing.GetPositionZ());
|
||||
Advance(player,record);
|
||||
continue;
|
||||
}
|
||||
if (record.CheckTimer > diff)
|
||||
{
|
||||
record.CheckTimer -= diff;
|
||||
@@ -737,9 +886,27 @@ void ClientlessFlightMgr::Update(uint32 diff)
|
||||
}
|
||||
if (record.Moving && player->IsSplineEnabled())
|
||||
{
|
||||
if (record.SegmentElapsed > 10000)
|
||||
if (record.Progress.Stalled())
|
||||
{
|
||||
Fail(player, record, "flight segment made no confirmed progress");
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"FLIGHT_STALLED guid=%u idleMs=%u segmentMs=%u expectedMs=%u overdue=%u recovery=%u",
|
||||
guid, record.Progress.IdleMs, record.Progress.ElapsedMs, record.Progress.ExpectedMs,
|
||||
record.Progress.Overdue()?1u:0u, record.StallRecoveries);
|
||||
if (record.Returning || record.StallRecoveries >= 2)
|
||||
{
|
||||
Fail(player, record, "flight stalled for 60 seconds; local recovery exhausted");
|
||||
continue;
|
||||
}
|
||||
++record.StallRecoveries;
|
||||
player->StopMoving();
|
||||
record.Moving = false;
|
||||
// Keep the leader trail and destination. Re-evaluate the local
|
||||
// join/corridor; never replace the macro route or return yet.
|
||||
record.Join = {};
|
||||
bool const departed = record.Memory.Departed;
|
||||
record.Memory = {};
|
||||
record.Memory.Departed = departed;
|
||||
Advance(player, record);
|
||||
continue;
|
||||
}
|
||||
auto ray =[&](Point const & a, Point const & b)
|
||||
@@ -751,13 +918,13 @@ 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;
|
||||
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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Common.h"
|
||||
#include "Position.h"
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include "ClientlessFlightProgress.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
@@ -31,7 +32,7 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
bool Preferred(Player const * player, uint32 mapId, Position const & position) const;
|
||||
bool Enabled(Player const * player) const;
|
||||
void BeginPlanning(Player const* player = nullptr);
|
||||
bool Start(Player * player, Position const & target, std::string & error);
|
||||
bool Start(Player * player, Position const & target, std::string & error, bool actionOwned = false);
|
||||
bool Follow(Player* player, Position const& target, std::string& error);
|
||||
bool LandFollowing(Player* player);
|
||||
bool PrepareFollowingMount(Player* player);
|
||||
@@ -44,21 +45,29 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
std::shared_ptr<ClientlessFlightPath::SharedTrail> Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
|
||||
std::shared_ptr<ClientlessFlightPath::SharedTrail> Following;
|
||||
std::size_t SharedIndex=0;
|
||||
ClientlessFlightPath::SharedJoin Join;
|
||||
uint32 SharedWaitSince=0;
|
||||
ClientlessFlightStatus Status;
|
||||
ClientlessFlightPath::Settings Settings;
|
||||
ClientlessFlightPath::Memory Memory;
|
||||
Position Start, Segment, LastPosition;
|
||||
bool SegmentUnderOverhang = false;
|
||||
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
|
||||
bool Moving = false, Returning = false;
|
||||
bool ApproachingDeparture = false;
|
||||
Position Departure;
|
||||
uint32 TargetUpdated = 0, FormationDelay = 0;
|
||||
uint32 DetachedRecoveryCount = 0;
|
||||
ClientlessFlightProgress::Watch Progress;
|
||||
uint32 StallRecoveries = 0;
|
||||
bool ActionOwned = false;
|
||||
std::string AbortReason;
|
||||
std::vector < Position > Breadcrumbs;
|
||||
std::unordered_map < std::string, bool > Tiles;
|
||||
};
|
||||
ClientlessFlightPath::Surface Sample(Player * player, Record & record, float x, float y) const;
|
||||
bool Advance(Player * player, Record & record, bool 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);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
// Geometry-only receding-horizon navigation. Server adapters supply phased
|
||||
// terrain/ceiling and static+dynamic collision queries. No Player shortcuts.
|
||||
@@ -18,6 +19,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 +33,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 +60,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 +74,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.
|
||||
@@ -106,18 +118,80 @@ 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
|
||||
// may replan while that spline is still close to its support surface.
|
||||
if (!memory.Departed && from.Z >= here.Ground + cfg.Safety)
|
||||
memory.Departed = true;
|
||||
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 &&
|
||||
Distance(from,goal)<=cfg.Step && Corridor(from,goal,goal,cfg,sample,ray,false))
|
||||
return {goal,true,false,true};
|
||||
// A normal diagonal touchdown remains the first choice. When the bot
|
||||
// is already above the landing column and there is not enough lateral
|
||||
// room for that slope, descend in the verified column instead of
|
||||
// opening another wide approach.
|
||||
if (memory.Departed && distance < 2.0f && from.Z > goal.Z + 1.5f &&
|
||||
Corridor(from,goal,goal,cfg,sample,ray,false))
|
||||
return {goal,true,false,true};
|
||||
if (distance < 2 && std::fabs(from.Z - goal.Z) < 1.5f) return
|
||||
{
|
||||
goal, Corridor(from, goal, goal, cfg, sample, ray, true), false, true
|
||||
@@ -143,6 +217,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;
|
||||
@@ -187,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
|
||||
@@ -208,27 +286,103 @@ namespace ClientlessFlightPath
|
||||
// must not postpone descent until directly above the destination.
|
||||
if (openApproach)
|
||||
{
|
||||
memory.Departed = true;
|
||||
return best;
|
||||
}
|
||||
}
|
||||
if (best.Valid) memory.Departed = true;
|
||||
// Diagonal departure candidates were exhausted. Tight settlements
|
||||
// can still have a completely open vertical column, so use it before
|
||||
// asking the caller to walk to a different takeoff point.
|
||||
if (!best.Valid && departure)
|
||||
{
|
||||
float const top = std::min(here.Ceiling - 6.0f,
|
||||
std::max(from.Z + 20.0f, here.Ground + std::min(cfg.Clearance, 35.0f)));
|
||||
Point vertical{from.X,from.Y,top};
|
||||
if (top > from.Z + 2.0f && Corridor(from,vertical,goal,cfg,sample,ray,true))
|
||||
{
|
||||
return {vertical,true,false,false};
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
struct SharedTrail
|
||||
{
|
||||
std::vector<Point> Points;
|
||||
bool Aborted=false;
|
||||
std::vector<bool> UnderOverhang;
|
||||
Point Landing;
|
||||
bool LandingReady=false;
|
||||
bool Aborted=false, Complete=false;
|
||||
std::shared_ptr<SharedTrail> Replacement;
|
||||
std::shared_ptr<SharedTrail> Replace(Point const& landing)
|
||||
{
|
||||
auto next=std::make_shared<SharedTrail>();
|
||||
next->Landing=landing;next->LandingReady=true;
|
||||
Replacement=next;Aborted=true;
|
||||
return next;
|
||||
}
|
||||
bool BindLanding(Point& goal) const
|
||||
{
|
||||
if(!LandingReady || Aborted)return false;
|
||||
goal=Landing;return true;
|
||||
}
|
||||
};
|
||||
struct SharedJoin
|
||||
{
|
||||
bool Active=true;
|
||||
std::size_t Target=std::numeric_limits<std::size_t>::max();
|
||||
Memory Navigation;
|
||||
};
|
||||
struct SharedResult {Result Step;bool Waiting=false;};
|
||||
inline SharedResult FollowTrail(Point const& current,Point const& goal,Settings const& settings,
|
||||
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray)
|
||||
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray,float lookaheadDistance=0,SharedJoin* join=nullptr)
|
||||
{
|
||||
if(trail.Aborted)return {};
|
||||
if(!trail.LandingReady)return {{},true};
|
||||
if(join && join->Active)
|
||||
{
|
||||
if(trail.Points.empty())return {{},true};
|
||||
if(join->Target==std::numeric_limits<std::size_t>::max())
|
||||
{
|
||||
// Join a nearby remaining leg instead of replaying the leader's
|
||||
// entire history after a taxi, recovery or delayed departure.
|
||||
std::vector<std::size_t> candidates;
|
||||
for(std::size_t i=index;i<trail.Points.size();++i)candidates.push_back(i);
|
||||
std::sort(candidates.begin(),candidates.end(),[&](auto a,auto b){
|
||||
float da=Distance(current,trail.Points[a]),db=Distance(current,trail.Points[b]);
|
||||
return da==db?a<b:da<db;
|
||||
});
|
||||
for(std::size_t n=0;n<std::min<std::size_t>(8,candidates.size());++n)
|
||||
{
|
||||
auto candidate=candidates[n];Memory memory;
|
||||
auto step=Next(current,trail.Points[candidate],settings,memory,sample,ray,true);
|
||||
if(!step.Valid)continue;
|
||||
join->Target=candidate;join->Navigation=memory;
|
||||
if(Distance(current,trail.Points[candidate])>=0.7f)
|
||||
{step.Landing=false;return {step,false};}
|
||||
break;
|
||||
}
|
||||
if(join->Target==std::numeric_limits<std::size_t>::max())return {};
|
||||
}
|
||||
if(Distance(current,trail.Points[join->Target])>=0.7f)
|
||||
{
|
||||
auto step=Next(current,trail.Points[join->Target],settings,join->Navigation,sample,ray,true);
|
||||
step.Landing=false;return {step,false};
|
||||
}
|
||||
index=join->Target;join->Active=false;
|
||||
}
|
||||
while(index<trail.Points.size() && Distance(current,trail.Points[index])<0.7f)++index;
|
||||
if(index==trail.Points.size())return {{},true};
|
||||
// Extend a running spline before its endpoint. A corner may only be
|
||||
// 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];
|
||||
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,36 @@
|
||||
#ifndef VCORE_CLIENTLESS_FLIGHT_PROGRESS_H
|
||||
#define VCORE_CLIENTLESS_FLIGHT_PROGRESS_H
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
// Progress is measured against a fixed segment endpoint, not issued splines.
|
||||
namespace ClientlessFlightProgress
|
||||
{
|
||||
struct Watch
|
||||
{
|
||||
unsigned IdleMs = 0, ElapsedMs = 0, ExpectedMs = 0;
|
||||
float BestDistance = 0;
|
||||
void Begin(float distance, unsigned expectedMs)
|
||||
{
|
||||
BestDistance = distance;
|
||||
ExpectedMs = expectedMs;
|
||||
IdleMs = ElapsedMs = 0;
|
||||
}
|
||||
void Observe(float distance, unsigned diff)
|
||||
{
|
||||
ElapsedMs += std::min(diff, 30000u);
|
||||
// Accumulate sub-tick motion; jitter or moving away is not progress.
|
||||
if (std::isfinite(distance) && BestDistance - distance >= 0.5f)
|
||||
{
|
||||
BestDistance = distance;
|
||||
IdleMs = 0;
|
||||
}
|
||||
else
|
||||
IdleMs = std::min(60000u, IdleMs + std::min(diff, 30000u));
|
||||
}
|
||||
bool Stalled() const { return IdleMs >= 60000; }
|
||||
// Duration overruns are diagnostic only: a progressing flight is healthy.
|
||||
bool Overdue() const { return ElapsedMs > ExpectedMs + 5000u; }
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -231,6 +231,8 @@ int Skill(lua_State* L)
|
||||
if(!sSkillLineStore.LookupEntry(skill) || value>maximum)return luaL_error(L,"invalid skill or skill value exceeds maximum");
|
||||
p->SetSkill(uint16(skill),p->GetSkillStep(uint16(skill)),uint16(value),uint16(maximum));lua_pushboolean(L,p->GetPureSkillValue(skill)==value);return 1;
|
||||
}
|
||||
int HasAura(lua_State* L){auto p=Actor(L);lua_pushboolean(L,p->HasAura(Number(L,2)));return 1;}
|
||||
int RemoveAura(lua_State* L){auto p=Actor(L);p->RemoveAurasDueToSpell(Number(L,2));return 0;}
|
||||
int Count(lua_State* L){auto p=Actor(L);lua_pushinteger(L,p->GetItemCount(Number(L,2),false));return 1;}
|
||||
int Quest(lua_State* L){auto p=Actor(L);auto id=Number(L,2);lua_pushinteger(L,p->GetQuestStatus(id));lua_pushboolean(L,p->GetQuestRewardStatus(id));return 2;}
|
||||
int Find(lua_State* L)
|
||||
@@ -425,7 +427,7 @@ bool Start(std::string const& id,fs::path path,std::string& error)
|
||||
for(auto name:{"io","os","package","debug","dofile","loadfile","require","load","collectgarbage","coroutine","pcall","xpcall"}){lua_pushnil(L);lua_setglobal(L,name);}
|
||||
lua_newtable(L);
|
||||
for(auto const& fn:std::initializer_list<std::pair<char const*,lua_CFunction>>{
|
||||
{"npc_hello",NpcHello},{"menu",Menu},{"spellclick",SpellClick},{"exit_vehicle",ExitVehicle},{"request_cast",RequestCast},{"request_cast_at",RequestCastAt},{"use_item_at",UseItemAt},{"area_trigger",AreaTrigger},{"inventory",Inventory},{"stop",Stop},{"now_ms",Now},{"spawn",Spawn},{"online",Online},{"_create",Create},{"_create_result",CreateResult},{"wait",Wait},{"state",Snapshot},{"teleport",Teleport},{"level",Level},{"stats",Stats},{"stat",Stat},{"learn",Learn},{"item",Item},{"equip",Equip},{"clear_slot",ClearSlot},{"remove_item",RemoveItem},{"skill",Skill},{"item_count",Count},{"quest",Quest},{"use",Use},{"hello",Hello},{"accept",Accept},{"abandon",Abandon},{"reward",Reward},{"gossip",Gossip},{"cast",Cast},{"move",Move},{"face",Face},{"attack",Attack},{"target_state",TargetState},{"objectives",Objectives},{"request_reward",RequestReward},{"loot",Loot},{"loot_item",LootItem},{"loot_release",LootRelease},{"use_item",UseItem},{"record",Record},{"check",Check}})Bind(L,r.get(),fn.first,fn.second);
|
||||
{"npc_hello",NpcHello},{"menu",Menu},{"spellclick",SpellClick},{"exit_vehicle",ExitVehicle},{"request_cast",RequestCast},{"request_cast_at",RequestCastAt},{"use_item_at",UseItemAt},{"area_trigger",AreaTrigger},{"inventory",Inventory},{"stop",Stop},{"now_ms",Now},{"spawn",Spawn},{"online",Online},{"_create",Create},{"_create_result",CreateResult},{"wait",Wait},{"state",Snapshot},{"teleport",Teleport},{"level",Level},{"stats",Stats},{"stat",Stat},{"learn",Learn},{"item",Item},{"equip",Equip},{"clear_slot",ClearSlot},{"remove_item",RemoveItem},{"skill",Skill},{"has_aura",HasAura},{"remove_aura",RemoveAura},{"item_count",Count},{"quest",Quest},{"use",Use},{"hello",Hello},{"accept",Accept},{"abandon",Abandon},{"reward",Reward},{"gossip",Gossip},{"cast",Cast},{"move",Move},{"face",Face},{"attack",Attack},{"target_state",TargetState},{"objectives",Objectives},{"request_reward",RequestReward},{"loot",Loot},{"loot_item",LootItem},{"loot_release",LootRelease},{"use_item",UseItem},{"record",Record},{"check",Check}})Bind(L,r.get(),fn.first,fn.second);
|
||||
Bind(L,r.get(),"nearest_creature",Nearest);Bind(L,r.get(),"nearest_object",Nearest,1);
|
||||
Bind(L,r.get(),"creature",Find);Bind(L,r.get(),"object",Find,1);lua_setglobal(L,"testbot");
|
||||
char const* helpers=R"lua(
|
||||
|
||||
@@ -85,7 +85,8 @@ bool ReturnToRecordedStand(Player* player,GameObject* object,Position const& anc
|
||||
auto transport=object->ToTransportBase();
|
||||
if(!transport || player->GetTransport()!=transport || !player->IsAlive() || player->IsInCombat() ||
|
||||
player->IsBeingTeleported() || player->GetMap()!=object->GetMap() ||
|
||||
!object->GetTransportDockState(dock) || !dock.Stopped || uint64(dock.RemainingPause)<uint64(exitBudget)+1000)return false;
|
||||
!object->GetTransportDockState(dock) || !dock.Stopped ||
|
||||
(object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT && uint64(dock.RemainingPause)<uint64(exitBudget)+1000))return false;
|
||||
float x=anchor.GetPositionX(),y=anchor.GetPositionY(),z=anchor.GetPositionZ(),o=anchor.GetOrientation();
|
||||
if(!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) ||
|
||||
std::fabs(x)>100 || std::fabs(y)>100 || std::fabs(z)>100)return false;
|
||||
@@ -120,7 +121,8 @@ bool ClientlessTransportDeckMgr::Update(Player* player,GameObject* object,Positi
|
||||
Point anchor{exitAnchor.GetPositionX(),exitAnchor.GetPositionY(),exitAnchor.GetPositionZ()};
|
||||
TransportDockState dock;
|
||||
if(!object->GetTransportDockState(dock) || !dock.Stopped)return false;
|
||||
if(Distance(Local(player),anchor)<=0.65f)return dock.RemainingPause>=exitBudget;
|
||||
if(Distance(Local(player),anchor)<=0.65f)
|
||||
return object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT || dock.RemainingPause>=exitBudget;
|
||||
return ReturnToRecordedStand(player,object,exitAnchor,exitBudget);
|
||||
}
|
||||
uint32 guid=player->GetGUID().GetCounter();auto& state=Passengers[guid];
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
#include "Player.h"
|
||||
#include "Transport.h"
|
||||
#include "TransportMgr.h"
|
||||
#include "Spline.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include <G3D/Quat.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
@@ -326,6 +328,63 @@ ClientlessTransportStop const* ClientlessTransportMgr::GetStop(std::string const
|
||||
if(!EnsureLoaded())return nullptr;
|
||||
auto found=_stops.find(node);return found==_stops.end()?nullptr:&found->second;
|
||||
}
|
||||
float ClientlessTransportMgr::ConnectionDistance(std::string const& from,std::string const& to)
|
||||
{
|
||||
auto source=GetStop(from),target=GetStop(to);
|
||||
if(!source || !target || source->Entry!=target->Entry)return 0;
|
||||
struct Sample { uint32 Map;Position Pos;double Arc; };
|
||||
std::vector<Sample> samples;
|
||||
double totalLength=0;
|
||||
if(auto native=sTransportMgr->GetTransportTemplate(source->Entry))
|
||||
{
|
||||
// Native splines already expose cumulative arclength at each stop.
|
||||
// Sum their legs, never geographic jumps between maps/teleports.
|
||||
for(auto const& leg:native->PathLegs)
|
||||
{
|
||||
if(!leg.Spline || leg.Spline->length()<=0)continue;
|
||||
G3D::Vector3 point;
|
||||
leg.Spline->evaluate_percent(0,point);
|
||||
samples.push_back({leg.MapId,Position(point.x,point.y,point.z),totalLength});
|
||||
for(auto const& segment:leg.Segments)
|
||||
{
|
||||
leg.Spline->evaluate_percent(float(segment.DistanceFromLegStartAtEnd/leg.Spline->length()),point);
|
||||
samples.push_back({leg.MapId,Position(point.x,point.y,point.z),totalLength+segment.DistanceFromLegStartAtEnd});
|
||||
}
|
||||
totalLength+=leg.Spline->length();
|
||||
}
|
||||
}
|
||||
else if(auto animation=sTransportMgr->GetTransportAnimInfo(source->Entry))
|
||||
{
|
||||
auto spawn=sObjectMgr->GetGameObjectData(source->Spawn);
|
||||
if(!spawn)return 0;
|
||||
QuaternionData parent;
|
||||
if(auto addon=sObjectMgr->GetGameObjectAddon(source->Spawn))parent=addon->ParentRotation;
|
||||
auto rotation=G3D::Quat(parent.x,parent.y,parent.z,parent.w).toRotationMatrix();
|
||||
for(auto const& node:animation->Path)
|
||||
{
|
||||
auto const& p=node.second->Pos;
|
||||
G3D::Vector3 world=G3D::Vector3(p.X,p.Y,p.Z)*rotation;
|
||||
Position position(spawn->spawnPoint.GetPositionX()+world.x,spawn->spawnPoint.GetPositionY()+world.y,spawn->spawnPoint.GetPositionZ()+world.z);
|
||||
if(!samples.empty())totalLength+=samples.back().Pos.GetExactDist(position);
|
||||
samples.push_back({source->Map,position,totalLength});
|
||||
}
|
||||
if(samples.size()>1)totalLength+=samples.back().Pos.GetExactDist(samples.front().Pos);
|
||||
}
|
||||
if(samples.size()<2 || totalLength<=0)return 0;
|
||||
auto nearest=[&](ClientlessTransportStop const& stop)
|
||||
{
|
||||
size_t result=samples.size();float best=std::numeric_limits<float>::infinity();
|
||||
for(size_t i=0;i<samples.size();++i)if(samples[i].Map==stop.Map)
|
||||
{float distance=samples[i].Pos.GetExactDist(stop.Dock);if(distance<best){best=distance;result=i;}}
|
||||
return result;
|
||||
};
|
||||
auto begin=nearest(*source),end=nearest(*target);
|
||||
if(begin>=samples.size() || end>=samples.size() || begin==end)return 0;
|
||||
double length=samples[end].Arc-samples[begin].Arc;
|
||||
if(length<0)length+=totalLength;
|
||||
return float(length);
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::ObserveJourney(Player* player,uint32 lastMap,Position const& ground,std::string& from,std::string& to)
|
||||
{
|
||||
from.clear();to.clear();if(!player->GetTransport() || !EnsureLoaded())return false;
|
||||
@@ -405,6 +464,8 @@ bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std:
|
||||
Ride ride;
|
||||
ride.From = source; ride.To = _stops.at(to);
|
||||
ride.RequiredPause = uint32(PathLength(source) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000.0f) + uint32(source.Points.size()) * 100 + 1500;
|
||||
if(auto object=FindObject(player,source);object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT)
|
||||
ride.RequiredPause=500;
|
||||
ride.Status.Phase = ClientlessTransportPhase::Waiting;
|
||||
ride.Status.Detail = "waiting for recorded transport at departure dock";
|
||||
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
|
||||
@@ -435,10 +496,21 @@ bool ClientlessTransportMgr::ResumePassenger(Player* player)
|
||||
auto node=sClientlessTravelGraphMgr->FindNode(stop.Node);
|
||||
if(!object || object->ToTransportBase()!=player->GetTransport() || !node ||
|
||||
!sAdventureAreaMgr->AllowsNode(player,*node))continue;
|
||||
if(!source || stop.Node<source->Node)source=&stop;
|
||||
if(!source || (object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ?
|
||||
Distance(*player,stop.Dock)<Distance(*player,source->Dock) : stop.Node<source->Node))source=&stop;
|
||||
}
|
||||
if(!source)return false; // unknown vehicles and forbidden stops retain their owner
|
||||
Ride ride;ride.From=ride.To=*source;ride.Resumed=true;
|
||||
// A restored passenger has no live boarding event. Use the nearest recorded
|
||||
// floor as its recovery origin, only when there is one unambiguous opposite.
|
||||
auto object=FindObject(player,*source);
|
||||
if(object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT)
|
||||
for(auto const& entry:_stops)
|
||||
if(entry.first!=source->Node && SameService(*source,entry.second))
|
||||
{
|
||||
if(!ride.ElevatorDestination.empty()){ride.ElevatorDestination.clear();break;}
|
||||
ride.ElevatorDestination=entry.first;
|
||||
}
|
||||
ride.Status.Phase=ClientlessTransportPhase::Riding;
|
||||
ride.Status.Detail="restored passenger; waiting for a permitted recorded dock";
|
||||
ClientlessTransportDeckMgr::Clear(guid);
|
||||
@@ -490,6 +562,34 @@ void ClientlessTransportMgr::Cancel(uint32 guid)
|
||||
if (itr != _rides.end()) itr->second.Abort = true;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff)
|
||||
{
|
||||
if(!object || object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT ||
|
||||
player->GetTransport()!=object->ToTransportBase())return false;
|
||||
if(ride.ElevatorDestination.empty() && ride.From.Node!=ride.To.Node)
|
||||
ride.ElevatorDestination=ride.To.Node;
|
||||
ride.ElevatorPassengerMs=uint32(std::min<uint64>(300000,uint64(ride.ElevatorPassengerMs)+diff));
|
||||
if(ride.ElevatorPassengerMs<300000 || !player->IsAlive())return false;
|
||||
auto destination=_stops.find(ride.ElevatorDestination);
|
||||
if(destination==_stops.end() || destination->second.Points.empty() ||
|
||||
!SameService(ride.From,destination->second))return false;
|
||||
auto node=sClientlessTravelGraphMgr->FindNode(destination->first);
|
||||
if(!node || (sAdventureAreaMgr->Applies(player) && !sAdventureAreaMgr->AllowsNode(player,*node)))return false;
|
||||
auto const& target=destination->second.Points.front().Pos;
|
||||
player->StopMoving();
|
||||
// The native teleport removes the real passenger attachment and completes
|
||||
// through the normal clientless teleport ACK, including same-map transfers.
|
||||
if(!player->TeleportTo(destination->second.Map,target.GetPositionX(),target.GetPositionY(),
|
||||
target.GetPositionZ(),target.GetOrientation()))return false;
|
||||
ride.To=destination->second;
|
||||
ride.Status.Recovering=false;
|
||||
ride.ElevatorFallback=true;
|
||||
SetPhase(player,ride,ClientlessTransportPhase::Riding,"five-minute elevator fallback to destination node");
|
||||
TC_LOG_WARN("server.adventurebot.navigation","ELEVATOR_STUCK_TELEPORT guid=%u from='%s' to='%s' passengerMs=%u",
|
||||
player->GetGUID().GetCounter(),ride.From.Node.c_str(),ride.To.Node.c_str(),ride.ElevatorPassengerMs);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
{
|
||||
using Phase = ClientlessTransportPhase;
|
||||
@@ -500,10 +600,19 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
return;
|
||||
}
|
||||
if(ride.ElevatorFallback)
|
||||
{
|
||||
if(!player->GetTransport() && player->GetMapId()==ride.To.Map &&
|
||||
Distance(*player,ride.To.Points.front().Pos)<2.5f)
|
||||
SetPhase(player,ride,ride.Abort?Phase::Failed:Phase::Arrived,"elevator fallback reached destination node");
|
||||
return;
|
||||
}
|
||||
GameObject* object = FindObject(player, ride.From);
|
||||
if(RecoverStuckElevator(player,object,ride,diff))return;
|
||||
if(ride.Status.Recovering){RecoverPassenger(player,ride,diff);return;}
|
||||
if(ride.RetryDelay){ride.RetryDelay=diff>=ride.RetryDelay?0:ride.RetryDelay-diff;return;}
|
||||
GameObject* object = FindObject(player, ride.From);
|
||||
uint32 limit = uint32(std::min<uint64>(1800000, std::max<uint64>(180000, uint64(ride.From.Period) * 3 + 30000)));
|
||||
if(object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT)limit=std::max(limit,330000u);
|
||||
auto fail = [&](char const* detail)
|
||||
{
|
||||
player->StopMoving();
|
||||
@@ -548,7 +657,8 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
return;
|
||||
}
|
||||
if (ride.Abort) { fail("transport cancelled on land"); return; }
|
||||
if (!AtDock(player, object, ride.From, ride.RequiredPause))
|
||||
uint32 boardingPause=object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ? 500u : ride.RequiredPause;
|
||||
if (!AtDock(player, object, ride.From, boardingPause))
|
||||
{
|
||||
if(!ride.WaitTried && !player->GetTransport() && !player->IsInCombat())
|
||||
{
|
||||
@@ -612,7 +722,8 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
if (permittedArrival && AtDock(player, object, ride.To, 0)) arrival = &ride.To;
|
||||
else if (!ride.Resumed && ride.Abort && AtDock(player, object, ride.From, 0)) arrival = &ride.From;
|
||||
auto const& exit = arrival ? *arrival : ride.From;
|
||||
uint32 exitBudget=uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*std::max(100u,std::min(diff,2000u)*2)+2500;
|
||||
uint32 exitBudget=object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT ? 0u :
|
||||
uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*std::max(100u,std::min(diff,2000u)*2)+2500;
|
||||
bool ready=ClientlessTransportDeckMgr::Update(player,object,exit.Points.back().Pos,arrival!=nullptr,exitBudget);
|
||||
if (!arrival || !ready || !AtDock(player,object,*arrival,exitBudget)) return;
|
||||
if (arrival == &ride.From) ride.To = ride.From;
|
||||
@@ -622,11 +733,16 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
if (ride.Status.Phase != Phase::Boarding && ride.Status.Phase != Phase::Leaving) return;
|
||||
bool boarding = ride.Status.Phase == Phase::Boarding;
|
||||
auto const& stop = boarding ? ride.From : ride.To;
|
||||
if (!object || !AtDock(player, object, stop, 0))
|
||||
bool landedElevatorExit = !boarding && object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT &&
|
||||
!player->GetTransport() && ride.Point<stop.Points.size() && !stop.Points[ride.Point].OnTransport;
|
||||
if (!landedElevatorExit && (!object || !AtDock(player, object, stop, 0)))
|
||||
{
|
||||
if (player->GetTransport())
|
||||
{
|
||||
player->StopMoving(); ride.Abort = true;
|
||||
player->StopMoving();
|
||||
// A short elevator stop can end after a legitimate boarding/exit
|
||||
// began. Retain the passenger and retry, without cancelling its trip.
|
||||
if(!object || object->GetGoType()!=GAMEOBJECT_TYPE_TRANSPORT)ride.Abort = true;
|
||||
SetPhase(player, ride, Phase::Riding, "departure during transition; retaining passenger until safe dock");
|
||||
}
|
||||
else fail("transport departed during land transition");
|
||||
|
||||
@@ -50,6 +50,7 @@ public:
|
||||
static ClientlessTransportMgr* instance();
|
||||
bool Reload();
|
||||
bool HasConnection(std::string const& from, std::string const& to);
|
||||
float ConnectionDistance(std::string const& from, std::string const& to);
|
||||
bool ObserveJourney(Player* passenger, uint32 lastMap, Position const& lastGround, std::string& from, std::string& to);
|
||||
bool ArrivalNode(Player* player, std::string const& from, std::string& to);
|
||||
ClientlessTransportStop const* GetStop(std::string const& node);
|
||||
@@ -82,6 +83,9 @@ private:
|
||||
uint32 RequiredPause = 0;
|
||||
uint32 PointTimeout = 0;
|
||||
uint32 RetryDelay = 0;
|
||||
uint32 ElevatorPassengerMs = 0;
|
||||
std::string ElevatorDestination;
|
||||
bool ElevatorFallback = false;
|
||||
bool Moving = false;
|
||||
bool Abort = false;
|
||||
bool Resumed = false;
|
||||
@@ -95,6 +99,7 @@ private:
|
||||
bool Sample(Player* player, Recording& recording, bool force);
|
||||
bool Save(Player* player, std::string const& stand, std::string& error);
|
||||
void UpdateRide(Player* player, Ride& ride, uint32 diff);
|
||||
bool RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff);
|
||||
void RecoverPassenger(Player* player, Ride& ride, uint32 diff);
|
||||
bool SelectResumedDock(Player* player, GameObject* object, Ride& ride);
|
||||
void SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef VCORE_CLIENTLESS_TRAVEL_COST_H
|
||||
#define VCORE_CLIENTLESS_TRAVEL_COST_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ClientlessTravelCost
|
||||
{
|
||||
enum Mode : unsigned { Ground, Taxi, Flight, Ship, Zeppelin, Tram, Portal, Count };
|
||||
inline Mode Type(std::string const& type)
|
||||
{
|
||||
if(type=="taxi")return Taxi;
|
||||
if(type=="free_flight")return Flight;
|
||||
if(type=="ship")return Ship;
|
||||
if(type=="zeppelin")return Zeppelin;
|
||||
if(type=="tram" || type=="elevator")return Tram;
|
||||
if(type=="portal" || type=="portal_use")return Portal;
|
||||
return Ground;
|
||||
}
|
||||
inline char const* Name(Mode mode)
|
||||
{
|
||||
static char const* const names[]={"Ground","Taxi","Flight","Ship","Zeppelin","Tram","Portal"};
|
||||
return names[mode];
|
||||
}
|
||||
struct Profile
|
||||
{
|
||||
std::array<float,Count> Factors{{1,1,1,1,1,1,1}};
|
||||
std::uint32_t Seed=0;
|
||||
float Cost(Mode mode,float distance) const
|
||||
{
|
||||
// A teleport traverses no world-space road. Its transition remains
|
||||
// positive; coordinates on different maps are never subtracted.
|
||||
float length=mode==Portal?1.0f:std::max(0.0f,distance);
|
||||
return length*Factors[mode];
|
||||
}
|
||||
float Cost(std::string const& type,float distance)const{return Cost(Type(type),distance);}
|
||||
};
|
||||
inline Profile Sample(Profile base,std::uint32_t seed,float variation)
|
||||
{
|
||||
variation=std::isfinite(variation)?std::clamp(variation,0.0f,0.9f):0.0f;
|
||||
base.Seed=seed;
|
||||
// Separate deterministic draws per mode, never per edge or search probe.
|
||||
for(unsigned mode=0;mode<Count;++mode)
|
||||
{
|
||||
std::uint32_t x=seed+0x9e3779b9u*(mode+1);
|
||||
x^=x>>16;x*=0x7feb352du;x^=x>>15;x*=0x846ca68bu;x^=x>>16;
|
||||
float u=float(x>>8)/16777215.0f;
|
||||
float factor=std::isfinite(base.Factors[mode])?std::clamp(base.Factors[mode],0.01f,100.0f):1.0f;
|
||||
base.Factors[mode]=factor*(1.0f+variation*(2.0f*u-1.0f));
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3,8 +3,12 @@
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Config.h"
|
||||
#include "Random.h"
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
@@ -125,7 +129,7 @@ ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDat
|
||||
return itr != _portalUses.end() ? &itr->second : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
bool ClientlessTravelGraphMgr::PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
{
|
||||
auto portal=GetPortalUse(edgeDatabaseId);
|
||||
auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr;
|
||||
@@ -136,6 +140,23 @@ bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 e
|
||||
return !portal->Requirements.empty();
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::AdventurePortalUnlockAllowed(Player const* player,
|
||||
uint32 edgeDatabaseId) const
|
||||
{
|
||||
// AdventureBots cannot acquire quest progress. Once they have reached the
|
||||
// Cataclysm level range, graph-bound public travel portals are therefore
|
||||
// considered unlocked. This does not bypass faction/area/node policy or
|
||||
// make arbitrary, unbound GameObjects usable.
|
||||
return player && player->getLevel() >= 80 && GetPortalUse(edgeDatabaseId) &&
|
||||
sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
{
|
||||
return PortalNativePrerequisites(player,edgeDatabaseId) ||
|
||||
AdventurePortalUnlockAllowed(player,edgeDatabaseId);
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::Reload()
|
||||
{
|
||||
_positionProbes.Clear();
|
||||
@@ -299,6 +320,7 @@ bool ClientlessTravelGraphMgr::Reload()
|
||||
while (pointResult->NextRow());
|
||||
}
|
||||
|
||||
RebuildEdgeDistances();
|
||||
_loaded = true;
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
@@ -308,6 +330,61 @@ bool ClientlessTravelGraphMgr::Reload()
|
||||
return !_nodes.empty();
|
||||
}
|
||||
|
||||
ClientlessTravelCost::Profile ClientlessTravelGraphMgr::NewJourneyCosts() const
|
||||
{
|
||||
ClientlessTravelCost::Profile base;
|
||||
for(unsigned mode=0;mode<ClientlessTravelCost::Count;++mode)
|
||||
base.Factors[mode]=sConfigMgr->GetFloatDefault(
|
||||
std::string("Clientless.Travel.CostMultiplier.")+ClientlessTravelCost::Name(ClientlessTravelCost::Mode(mode)),1.0f);
|
||||
return ClientlessTravelCost::Sample(base,urand(1,0xffffffffu),
|
||||
sConfigMgr->GetFloatDefault("Clientless.Travel.CostVariation",0.20f));
|
||||
}
|
||||
|
||||
void ClientlessTravelGraphMgr::RebuildEdgeDistances()
|
||||
{
|
||||
for(auto& outgoing:_edges)for(auto& edge:outgoing.second)
|
||||
{
|
||||
auto const& from=_nodes.at(edge.FromNode);auto const& to=_nodes.at(edge.ToNode);
|
||||
float length=0;
|
||||
auto segment=[&](uint32 map,float x,float y,float z,uint32 otherMap,float a,float b,float c)
|
||||
{if(map==otherMap)length+=std::hypot(std::hypot(x-a,y-b),z-c);};
|
||||
if(edge.Type=="portal" || edge.Type=="portal_use")length=1;
|
||||
else if(edge.Type=="taxi")
|
||||
{
|
||||
if(edge.FromNode.rfind("taxi_",0)==0 && edge.ToNode.rfind("taxi_",0)==0)
|
||||
{
|
||||
uint32 path=0,fare=0;
|
||||
sObjectMgr->GetTaxiPath(uint32(std::strtoul(edge.FromNode.c_str()+5,nullptr,10)),
|
||||
uint32(std::strtoul(edge.ToNode.c_str()+5,nullptr,10)),path,fare);
|
||||
if(path<sTaxiPathNodesByPath.size())
|
||||
{
|
||||
auto const& nodes=sTaxiPathNodesByPath[path];
|
||||
for(size_t i=1;i<nodes.size();++i)
|
||||
segment(nodes[i-1]->ContinentID,nodes[i-1]->Loc.X,nodes[i-1]->Loc.Y,nodes[i-1]->Loc.Z,
|
||||
nodes[i]->ContinentID,nodes[i]->Loc.X,nodes[i]->Loc.Y,nodes[i]->Loc.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(ClientlessTransportMgr::IsTransportEdge(edge.Type))
|
||||
length=sClientlessTransportMgr->ConnectionDistance(edge.FromNode,edge.ToNode);
|
||||
else if(auto points=GetEdgePoints(edge.DatabaseId);points && !points->empty())
|
||||
{
|
||||
auto const& first=edge.Reverse?points->back():points->front();
|
||||
auto const& last=edge.Reverse?points->front():points->back();
|
||||
segment(from.MapId,from.X,from.Y,from.Z,first.MapId,first.X,first.Y,first.Z);
|
||||
for(size_t i=1;i<points->size();++i)
|
||||
segment((*points)[i-1].MapId,(*points)[i-1].X,(*points)[i-1].Y,(*points)[i-1].Z,
|
||||
(*points)[i].MapId,(*points)[i].X,(*points)[i].Y,(*points)[i].Z);
|
||||
segment(last.MapId,last.X,last.Y,last.Z,to.MapId,to.X,to.Y,to.Z);
|
||||
}
|
||||
if(length<=0 && from.MapId==to.MapId)
|
||||
segment(from.MapId,from.X,from.Y,from.Z,to.MapId,to.X,to.Y,to.Z);
|
||||
edge.Cost=std::isfinite(length)&&length>0?length:std::numeric_limits<float>::infinity();
|
||||
TC_LOG_DEBUG("server.adventurebot.navigation","TRAVEL_EDGE_DISTANCE edge=%u type='%s' reverse=%u distance=%.2f",
|
||||
edge.DatabaseId,edge.Type.c_str(),edge.Reverse?1u:0u,edge.Cost);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ClientlessTravelGraphMgr::FindNearestStartNode(
|
||||
Player const* player,
|
||||
float maxDistance) const
|
||||
@@ -346,8 +423,11 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
||||
float startRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds)
|
||||
std::unordered_set<uint32> const* blockedEdgeIds,
|
||||
ClientlessTravelCost::Profile const* journeyCosts)
|
||||
{
|
||||
ClientlessTravelCost::Profile const defaults;
|
||||
auto const& costs=journeyCosts?*journeyCosts:defaults;
|
||||
route.clear();
|
||||
error.clear();
|
||||
|
||||
@@ -430,7 +510,7 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||
continue;
|
||||
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
|
||||
if (candidate >= distance[edge.ToNode])
|
||||
continue;
|
||||
|
||||
@@ -557,7 +637,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds,
|
||||
std::unordered_set<std::string> const* blockedBridgeKeys,
|
||||
ClientlessTravelLocationGoal const* locationGoal)
|
||||
ClientlessTravelLocationGoal const* locationGoal,
|
||||
ClientlessTravelCost::Profile const* journeyCosts)
|
||||
{
|
||||
struct Trace
|
||||
{
|
||||
@@ -621,8 +702,17 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
|
||||
{ error="destination location denied by area catalog"; return false; }
|
||||
}
|
||||
ClientlessTravelCost::Profile const defaults;
|
||||
auto const& costs=journeyCosts?*journeyCosts:defaults;
|
||||
auto groundCost=[&](float length){return costs.Cost(ClientlessTravelCost::Ground,length);};
|
||||
auto flightDistanceCost=[&](float length){return costs.Cost(ClientlessTravelCost::Flight,length);};
|
||||
auto edgeCost=[&](ClientlessTravelEdge const& edge){return costs.Cost(edge.Type,edge.Cost);};
|
||||
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
|
||||
// Preserve part of the global budget for nodes reached later in the
|
||||
// search. In particular, a flight to a portal source must still leave
|
||||
// enough probes for the flight from its destination map to the goal.
|
||||
constexpr std::uint64_t MaxFlightEstimatesPerOrigin = 6;
|
||||
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
|
||||
{
|
||||
if (!flightEnabled || from.MapId != map ||
|
||||
@@ -644,12 +734,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
return std::numeric_limits<float>::infinity();
|
||||
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
||||
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
||||
if(access && std::isfinite(result))
|
||||
if(std::isfinite(result))
|
||||
{
|
||||
float distance=a.GetExactDist(&b);
|
||||
if(distance<20)result=std::numeric_limits<float>::infinity();
|
||||
else result=(sClientlessFlightMgr->Preferred(player,map,a) || sClientlessFlightMgr->Preferred(player,map,b))?
|
||||
40+distance*0.4f:180+distance*1.5f;
|
||||
float length=a.GetExactDist(&b);
|
||||
result=access && length<20?std::numeric_limits<float>::infinity():flightDistanceCost(length);
|
||||
}
|
||||
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
||||
flightEstimates.emplace(estimateKey, result);
|
||||
@@ -665,45 +753,38 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
bridgeRadius = MaxVirtualNodeBridge;
|
||||
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
|
||||
|
||||
// The overwhelmingly common free-flight case is a same-map Location. Do
|
||||
// not construct a macro graph first: Estimate already enforces licenses,
|
||||
// mount flags, area policy, distance and the complete catalog segment.
|
||||
// Native departure/landing validation still happens in FlightMgr::Start.
|
||||
if (locationGoal && locationGoal->MapId == player->GetMapId())
|
||||
float directFlightCost=std::numeric_limits<float>::infinity();
|
||||
auto directFlightRoute=[&]()
|
||||
{
|
||||
ClientlessTravelNode departureNode;
|
||||
departureNode.Key = "flight_departure";
|
||||
departureNode.MapId = player->GetMapId();
|
||||
departureNode.X = player->GetPositionX();
|
||||
departureNode.Y = player->GetPositionY();
|
||||
departureNode.Z = player->GetPositionZ();
|
||||
float const directFlight = flightCost(departureNode, locationGoal->MapId,
|
||||
locationGoal->X, locationGoal->Y, locationGoal->Z);
|
||||
if (std::isfinite(directFlight))
|
||||
{
|
||||
ClientlessTravelRouteStep departure;
|
||||
departure.NodeKey = "flight_departure";
|
||||
departure.NodeType = "ground";
|
||||
departure.EdgeType = "start";
|
||||
departure.MapId = player->GetMapId();
|
||||
departure.X = player->GetPositionX();
|
||||
departure.Y = player->GetPositionY();
|
||||
departure.Z = player->GetPositionZ();
|
||||
route.push_back(std::move(departure));
|
||||
ClientlessTravelRouteStep departure;
|
||||
departure.NodeKey = "flight_departure";
|
||||
departure.NodeType = "ground";
|
||||
departure.EdgeType = "start";
|
||||
departure.MapId = player->GetMapId();
|
||||
departure.X = player->GetPositionX();
|
||||
departure.Y = player->GetPositionY();
|
||||
departure.Z = player->GetPositionZ();
|
||||
route.push_back(std::move(departure));
|
||||
|
||||
ClientlessTravelRouteStep flight;
|
||||
flight.NodeKey = "flight_location";
|
||||
flight.NodeType = "location";
|
||||
flight.EdgeType = "free_flight";
|
||||
flight.MapId = locationGoal->MapId;
|
||||
flight.X = locationGoal->X;
|
||||
flight.Y = locationGoal->Y;
|
||||
flight.Z = locationGoal->Z;
|
||||
route.push_back(std::move(flight));
|
||||
complete = true;
|
||||
reachedNode = "flight_location";
|
||||
return true;
|
||||
}
|
||||
ClientlessTravelRouteStep flight;
|
||||
flight.NodeKey = "flight_location";
|
||||
flight.NodeType = "location";
|
||||
flight.EdgeType = "free_flight";
|
||||
flight.MapId = locationGoal->MapId;
|
||||
flight.X = locationGoal->X;
|
||||
flight.Y = locationGoal->Y;
|
||||
flight.Z = locationGoal->Z;
|
||||
route.push_back(std::move(flight));
|
||||
complete = true;
|
||||
reachedNode = "flight_location";
|
||||
return true;
|
||||
};
|
||||
if(locationGoal && locationGoal->MapId==player->GetMapId())
|
||||
{
|
||||
ClientlessTravelNode departure;
|
||||
departure.Key="flight_departure";departure.MapId=player->GetMapId();
|
||||
departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
||||
directFlightCost=flightCost(departure,locationGoal->MapId,locationGoal->X,locationGoal->Y,locationGoal->Z);
|
||||
}
|
||||
|
||||
struct State
|
||||
@@ -785,6 +866,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
std::unordered_set<std::string> flightRoots;
|
||||
uint32 startCount = 0;
|
||||
// Unreachable nearby pavement/indoor anchors must not consume the entire
|
||||
// flight budget before graph search can consider flight-only connections.
|
||||
std::uint64_t startFlightProbes = 0;
|
||||
constexpr std::uint64_t MaxStartFlightProbes = 4;
|
||||
for (auto const& pair : _nodes)
|
||||
{
|
||||
ClientlessTravelNode const& node = pair.second;
|
||||
@@ -798,7 +883,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (localDistance > startRadius)
|
||||
continue;
|
||||
float startCost = localDistance;
|
||||
float startCost = groundCost(localDistance);
|
||||
|
||||
// VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node.
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key))
|
||||
@@ -815,9 +900,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
ClientlessTravelNode departure;
|
||||
departure.Key="flight_departure";
|
||||
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
||||
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
|
||||
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
|
||||
if(std::isfinite(air) && (!reachable || air<entryNav.PathLength))
|
||||
// A valid local MMAP entry is the normal, organic graph access and
|
||||
// must not spend the bounded flight-estimate budget. Free flight is
|
||||
// the recovery/re-entry mode for nodes that local navigation cannot
|
||||
// reach, not a shortcut racing every nearby pavement node.
|
||||
float air = std::numeric_limits<float>::infinity();
|
||||
if (!reachable && startFlightProbes < MaxStartFlightProbes && !(blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
|
||||
{
|
||||
++startFlightProbes;
|
||||
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
|
||||
}
|
||||
if(std::isfinite(air) && !reachable)
|
||||
{flightRoots.insert(node.Key);startCost=air;}
|
||||
else
|
||||
{
|
||||
@@ -842,7 +936,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
|
||||
continue;
|
||||
}
|
||||
startCost = entryNav.PathLength;
|
||||
startCost = groundCost(entryNav.PathLength);
|
||||
if (!reachable)
|
||||
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
|
||||
}
|
||||
@@ -960,9 +1054,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
continue;
|
||||
uint32 const startIndex = candidate.Edge.Reverse
|
||||
? candidate.Count - 1 - candidate.Index : candidate.Index;
|
||||
float newCost = (alreadyThere ? 0.0f : nav.PathLength) +
|
||||
float newCost = (alreadyThere ? 0.0f : groundCost(nav.PathLength)) +
|
||||
roadSectionCost(candidate.Edge.DatabaseId, candidate.Edge.Reverse,
|
||||
startIndex, candidate.Count - 1, candidate.Edge.Cost);
|
||||
startIndex, candidate.Count - 1, edgeCost(candidate.Edge));
|
||||
auto old = distance.find(key);
|
||||
if (old != distance.end() && old->second <= newCost)
|
||||
continue;
|
||||
@@ -972,7 +1066,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
previous.erase(key);
|
||||
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
|
||||
candidate.Edge.Reverse, candidate.Point,
|
||||
alreadyThere ? 0.0f : nav.PathLength };
|
||||
alreadyThere ? 0.0f : groundCost(nav.PathLength) };
|
||||
open.push({ newCost, state });
|
||||
++accepted;
|
||||
}
|
||||
@@ -983,10 +1077,92 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
if (!startCount && !accepted)
|
||||
{
|
||||
if(std::isfinite(directFlightCost))return directFlightRoute();
|
||||
error = "no reachable travel node or recorded ground edge near current position";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reserve a bounded part of the flight budget for complete local portal
|
||||
// connections before speculative road exits spend it. Nearby ground roots
|
||||
// are entry options, not a reason to overlook flight-only portal islands.
|
||||
if (flightEnabled && !locationGoal)
|
||||
{
|
||||
struct PortalConnection
|
||||
{
|
||||
float Rank;
|
||||
std::string Root;
|
||||
ClientlessTravelEdge Edge;
|
||||
};
|
||||
std::vector<PortalConnection> connections;
|
||||
auto const& goal = destinationItr->second;
|
||||
for (auto const& outgoing : _edges)
|
||||
for (auto const& edge : outgoing.second)
|
||||
{
|
||||
if (edge.Type != "portal" && edge.Type != "portal_use") continue;
|
||||
if (!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge) ||
|
||||
(blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))) continue;
|
||||
auto const& source = _nodes.at(edge.FromNode);
|
||||
auto const& target = _nodes.at(edge.ToNode);
|
||||
if (target.MapId != goal.MapId) continue;
|
||||
float dx=goal.X-target.X,dy=goal.Y-target.Y,dz=goal.Z-target.Z;
|
||||
float finish = flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
|
||||
for (auto const& root : states)
|
||||
{
|
||||
auto const& origin = _nodes.at(root.second.Node);
|
||||
if (origin.MapId != source.MapId || origin.Key == source.Key) continue;
|
||||
auto originEdges = _edges.find(origin.Key);
|
||||
if (originEdges != _edges.end() && std::any_of(originEdges->second.begin(),originEdges->second.end(),
|
||||
[](ClientlessTravelEdge const& e){return e.Type=="portal" || e.Type=="portal_use";})) continue;
|
||||
dx=source.X-origin.X;dy=source.Y-origin.Y;dz=source.Z-origin.Z;
|
||||
float direct=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
if (direct > 1500.0f) continue;
|
||||
connections.push_back({distance.at(root.first)+flightDistanceCost(direct)+edgeCost(edge)+finish,root.first,edge});
|
||||
}
|
||||
}
|
||||
std::sort(connections.begin(),connections.end(),[](PortalConnection const& a,PortalConnection const& b)
|
||||
{
|
||||
if(a.Rank!=b.Rank)return a.Rank<b.Rank;
|
||||
if(a.Edge.DatabaseId!=b.Edge.DatabaseId)return a.Edge.DatabaseId<b.Edge.DatabaseId;
|
||||
return a.Root<b.Root;
|
||||
});
|
||||
auto const callsBefore = trace.FlightCalls;
|
||||
unsigned attempts=0;
|
||||
for (auto const& candidate : connections)
|
||||
{
|
||||
if (++attempts>8 || trace.FlightCalls-callsBefore>=6) break;
|
||||
auto const edge=candidate.Edge;
|
||||
auto const& origin=_nodes.at(states.at(candidate.Root).Node);
|
||||
auto const& source=_nodes.at(edge.FromNode);
|
||||
auto const& target=_nodes.at(edge.ToNode);
|
||||
if (blockedBridgeKeys && (blockedBridgeKeys->count("flight:"+origin.Key+"->"+source.Key) ||
|
||||
blockedBridgeKeys->count("flight:"+target.Key+"->"+goal.Key))) continue;
|
||||
float entry=flightCost(origin,source.MapId,source.X,source.Y,source.Z,true);
|
||||
if (!std::isfinite(entry)) continue;
|
||||
float finish=target.Key==goal.Key?0.0f:flightCost(target,goal.MapId,goal.X,goal.Y,goal.Z,true);
|
||||
if (!std::isfinite(finish)) continue;
|
||||
auto relax=[&](std::string const& from,ClientlessTravelNode const& to,
|
||||
std::string const& type,uint32 id,bool reverse,float cost)
|
||||
{
|
||||
State next{to.Key,false};auto key=stateKey(next);
|
||||
float score=distance.at(from)+cost;
|
||||
auto old=distance.find(key);
|
||||
if(old==distance.end() || score<old->second)
|
||||
{
|
||||
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[from]+1;
|
||||
previous[key]={from,states.at(from).Node,type,id,reverse};
|
||||
open.push({score,next});
|
||||
}
|
||||
return key;
|
||||
};
|
||||
auto sourceKey=relax(candidate.Root,source,"free_flight",0,false,entry);
|
||||
auto targetKey=relax(sourceKey,target,edge.Type,edge.DatabaseId,edge.Reverse,edgeCost(edge));
|
||||
if(target.Key!=goal.Key)relax(targetKey,goal,"free_flight",0,false,finish);
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"PORTAL_FLIGHT_CONNECTION from='%s' source='%s' target='%s' goal='%s' entry=%.1f finish=%.1f",
|
||||
origin.Key.c_str(),source.Key.c_str(),target.Key.c_str(),goal.Key.c_str(),entry,finish);
|
||||
}
|
||||
}
|
||||
|
||||
// C8.20: virtual node bridges are equal-cost ground alternatives and
|
||||
// participate even when stored graph routes also exist.
|
||||
bool const repairRequired = true;
|
||||
@@ -1135,13 +1311,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
candidate.EdgeId = edge.DatabaseId;
|
||||
candidate.Reverse = edge.Reverse;
|
||||
candidate.EndIndex = i;
|
||||
candidate.RoadCost = edge.Cost * (travelled / totalLength);
|
||||
candidate.RoadCost = edgeCost(edge) * (travelled / totalLength);
|
||||
candidate.DistanceSq = dsq;
|
||||
candidate.Point = point;
|
||||
bool const groundExitExhausted = blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count("ground_exit_edge:" + identity);
|
||||
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
|
||||
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
|
||||
? groundCost(std::sqrt(dsq)) : std::numeric_limits<float>::infinity();
|
||||
|
||||
// A recorded edge can contain hundreds of samples. Calling
|
||||
// FlightMgr::Estimate for every sample made the synchronous
|
||||
@@ -1158,10 +1334,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
|
||||
{
|
||||
float const direct = std::sqrt(dsq);
|
||||
// Forty plus 0.4 * distance is the cheapest possible
|
||||
// free-flight cost. It is an admissible ordering key;
|
||||
// rejected area segments simply advance to the next item.
|
||||
rankedFlightExits.push_back({ candidate.RoadCost + 40.0f + direct * 0.4f,
|
||||
// Use this journey's flight factor for both candidate
|
||||
// ordering and execution cost, including expensive modes.
|
||||
rankedFlightExits.push_back({ candidate.RoadCost + flightDistanceCost(direct),
|
||||
std::move(candidate) });
|
||||
}
|
||||
}
|
||||
@@ -1217,32 +1392,40 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
uint32 StartIndex = 0;
|
||||
ClientlessTravelEdgePoint EntryPoint;
|
||||
} bestExit;
|
||||
float bestGoalCost = std::numeric_limits<float>::infinity();
|
||||
float bestGoalCost = directFlightCost;
|
||||
std::string bestGoalState;
|
||||
bool bestFinalFlight = false;
|
||||
// Free flight is a macro connector, not a reason to turn every graph map
|
||||
// into a complete directed graph. A flyer only needs direct links to the
|
||||
// requested destination and to nodes that enter a map-changing or physical
|
||||
// transport edge (portal, ship, zeppelin or tram). Taxi nodes are excluded:
|
||||
// a free flyer can already reach their same-map destination directly, and
|
||||
// adding every taxi source recreates an almost-complete graph. Ordinary
|
||||
// same-map ground
|
||||
// nodes remain reachable through their recorded graph, while a location
|
||||
// goal is evaluated directly from every expanded node below.
|
||||
std::unordered_set<std::string> flightConnectorNodes;
|
||||
flightConnectorNodes.insert(destinationNode);
|
||||
for (auto const& edgeGroup : _edges)
|
||||
for (ClientlessTravelEdge const& edge : edgeGroup.second)
|
||||
// Every permitted node is a potential way back into the persistent graph.
|
||||
// Do not materialize an all-to-all air graph, though: candidates are ranked
|
||||
// on demand below and the shared per-plan Estimate cap remains authoritative.
|
||||
// This is important for isolated anchors such as the capital Cataclysm
|
||||
// portals, but is deliberately not portal-specific.
|
||||
auto flightRemainderLowerBound = [&](ClientlessTravelNode const& candidate)
|
||||
{
|
||||
ClientlessTravelNode const& destination = destinationItr->second;
|
||||
auto lowerBoundToGoal = [&](ClientlessTravelNode const& node)
|
||||
{
|
||||
auto fromItr = _nodes.find(edge.FromNode);
|
||||
auto toItr = _nodes.find(edge.ToNode);
|
||||
bool const changesMap = fromItr != _nodes.end() && toItr != _nodes.end() &&
|
||||
fromItr->second.MapId != toItr->second.MapId;
|
||||
bool const physicalTransport = edge.Type == "portal" || edge.Type == "ship" ||
|
||||
edge.Type == "zeppelin" || edge.Type == "tram" || edge.Type == "transport";
|
||||
if (changesMap || physicalTransport)
|
||||
flightConnectorNodes.insert(edge.FromNode);
|
||||
}
|
||||
if (node.MapId != destination.MapId)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
|
||||
return flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
|
||||
};
|
||||
float best = lowerBoundToGoal(candidate);
|
||||
auto edges = _edges.find(candidate.Key);
|
||||
if (edges != _edges.end())
|
||||
for (ClientlessTravelEdge const& edge : edges->second)
|
||||
{
|
||||
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge)))
|
||||
continue;
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
auto next = _nodes.find(edge.ToNode);
|
||||
if (next == _nodes.end())
|
||||
continue;
|
||||
best = std::min(best, edgeCost(edge) + lowerBoundToGoal(next->second));
|
||||
}
|
||||
return best;
|
||||
};
|
||||
// C8.20: a virtual entry is represented in the queue by its FAR
|
||||
// endpoint. Check exits BEFORE that endpoint independently: otherwise
|
||||
// an exit halfway along the road would pay for the entire remaining edge.
|
||||
@@ -1272,7 +1455,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (fromNodeItr != _edges.end())
|
||||
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
|
||||
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
|
||||
fullEdgeCost = edge.Cost;
|
||||
fullEdgeCost = edgeCost(edge);
|
||||
if (fullEdgeCost <= 0.0f)
|
||||
continue;
|
||||
float const partialCost = roadSectionCost(exit.EdgeId,
|
||||
@@ -1335,7 +1518,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
std::string const finishKey = "finish:" + candidateNode.Key;
|
||||
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
||||
? std::numeric_limits<float>::infinity() :
|
||||
std::min(direct<=locationAccessRadius ? direct : std::numeric_limits<float>::infinity(),air);
|
||||
std::min(direct<=locationAccessRadius ? groundCost(direct) : std::numeric_limits<float>::infinity(),air);
|
||||
if (current.Cost + finalCost < bestGoalCost)
|
||||
{
|
||||
bestGoalCost = current.Cost + finalCost;
|
||||
@@ -1379,11 +1562,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// ground transit. Once reached during a planned route, its only legal
|
||||
// continuation is the portal edge itself. Physical AreaTriggers may
|
||||
// fire even before the route executor advances to that edge.
|
||||
bool const isPortalSource =
|
||||
current.Value.Node.rfind("portal_at_", 0) == 0 &&
|
||||
current.Value.Node.size() >= 7 &&
|
||||
current.Value.Node.compare(current.Value.Node.size() - 7, 7, "_source") == 0;
|
||||
bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end();
|
||||
auto const sourceEdges = _edges.find(current.Value.Node);
|
||||
bool const isPortalSource = sourceEdges != _edges.end() &&
|
||||
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
|
||||
[](ClientlessTravelEdge const& edge)
|
||||
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
||||
// Root states are physical arrivals too: replanning while standing at
|
||||
// a portal must not turn its source into an indoor flight departure.
|
||||
// If the portal failed or is forbidden, retain its recorded escape roads.
|
||||
bool const portalArrival = isPortalSource &&
|
||||
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
|
||||
[&](ClientlessTravelEdge const& edge)
|
||||
{
|
||||
return (edge.Type == "portal" || edge.Type == "portal_use") &&
|
||||
RescueEdgeAllowed(edge) && sAdventureAreaMgr->AllowsEdge(player,edge) &&
|
||||
!(blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId));
|
||||
});
|
||||
|
||||
// Persistent TravelGraph edges always reset the bridge guard.
|
||||
auto edgeItr = _edges.find(current.Value.Node);
|
||||
@@ -1393,7 +1587,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
{
|
||||
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
||||
if (portalArrival && edge.Type != "portal")
|
||||
if (portalArrival && edge.Type != "portal" && edge.Type != "portal_use")
|
||||
continue;
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
@@ -1401,7 +1595,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
State next{ edge.ToNode, false };
|
||||
std::string const nextKey = stateKey(next);
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
float const candidate = current.Cost + edgeCost(edge);
|
||||
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && candidate >= old->second)
|
||||
@@ -1421,15 +1615,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// Dynamic flights connect real nodes; never the synthetic entry waypoints.
|
||||
// Include them before the local-bridge guard: a flight is a separate
|
||||
// macro travel mode with its own native departure and landing checks.
|
||||
if (flightEnabled && !portalArrival)
|
||||
if (flightEnabled && !portalArrival && trace.FlightCalls < MaxFlightEstimatesPerPlan)
|
||||
{
|
||||
auto const& origin=_nodes.at(current.Value.Node);
|
||||
for (std::string const& targetKey : flightConnectorNodes)
|
||||
std::vector<std::pair<float, std::string>> macroFlightCandidates;
|
||||
std::vector<std::pair<float, std::string>> flightCandidates;
|
||||
flightCandidates.reserve(_nodes.size());
|
||||
for (auto const& candidate : _nodes)
|
||||
{
|
||||
auto targetItr = _nodes.find(targetKey);
|
||||
if (targetItr == _nodes.end())
|
||||
continue;
|
||||
auto const& target=targetItr->second;
|
||||
auto const& target=candidate.second;
|
||||
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
|
||||
State next{target.Key,false};std::string key=stateKey(next);
|
||||
auto old=distance.find(key);
|
||||
@@ -1437,10 +1631,62 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// Even a preferred legal flight costs at least this much.
|
||||
// Skip expensive terrain sampling when it cannot improve the
|
||||
// route already found; this preserves the full graph search.
|
||||
float lowerBound=40.0f+std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
|
||||
float lowerBound=flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
|
||||
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
|
||||
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
||||
float const remainder = flightRemainderLowerBound(target);
|
||||
if (!std::isfinite(remainder))
|
||||
continue;
|
||||
bool macroConnector = target.Key == destinationNode;
|
||||
auto outgoing = _edges.find(target.Key);
|
||||
if (outgoing != _edges.end())
|
||||
for (ClientlessTravelEdge const& edge : outgoing->second)
|
||||
{
|
||||
auto next = _nodes.find(edge.ToNode);
|
||||
bool const changesMap = next != _nodes.end() &&
|
||||
next->second.MapId != target.MapId;
|
||||
bool const physical = edge.Type == "portal" ||
|
||||
edge.Type == "portal_use" || edge.Type == "ship" ||
|
||||
edge.Type == "zeppelin" || edge.Type == "tram" ||
|
||||
edge.Type == "transport";
|
||||
if (changesMap || physical)
|
||||
{
|
||||
macroConnector = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto& candidates = macroConnector ? macroFlightCandidates : flightCandidates;
|
||||
candidates.emplace_back(lowerBound + remainder, target.Key);
|
||||
}
|
||||
std::sort(macroFlightCandidates.begin(), macroFlightCandidates.end(),
|
||||
[&](auto const& left, auto const& right)
|
||||
{
|
||||
// Every newly reached portal/transport endpoint must get
|
||||
// one chance to finish the requested journey before the
|
||||
// bounded probe window is spent on further connectors.
|
||||
bool const leftIsDestination = left.second == destinationNode;
|
||||
bool const rightIsDestination = right.second == destinationNode;
|
||||
if (leftIsDestination != rightIsDestination)
|
||||
return leftIsDestination;
|
||||
return left < right;
|
||||
});
|
||||
std::sort(flightCandidates.begin(), flightCandidates.end());
|
||||
macroFlightCandidates.insert(macroFlightCandidates.end(),
|
||||
flightCandidates.begin(), flightCandidates.end());
|
||||
std::uint64_t const callsAtOrigin = trace.FlightCalls;
|
||||
for (auto const& ranked : macroFlightCandidates)
|
||||
{
|
||||
if (trace.FlightCalls >= MaxFlightEstimatesPerPlan ||
|
||||
trace.FlightCalls - callsAtOrigin >= MaxFlightEstimatesPerOrigin)
|
||||
break;
|
||||
auto const& target = _nodes.at(ranked.second);
|
||||
State next{target.Key,false};std::string key=stateKey(next);
|
||||
auto old=distance.find(key);
|
||||
auto const blockedFlight = blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key);
|
||||
if (blockedFlight)
|
||||
continue;
|
||||
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
|
||||
if (!std::isfinite(cost)) continue;
|
||||
float score=current.Cost+cost;
|
||||
@@ -1464,10 +1710,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// road at an MMAP-reachable waypoint. Only remaining portions of
|
||||
// stored, directed ground edges are admitted; no off-graph shortcut.
|
||||
// A portal source may not be treated as an ordinary intermediate node.
|
||||
bool const futurePortalSource =
|
||||
from.Key.rfind("portal_at_", 0) == 0 &&
|
||||
from.Key.size() >= 7 &&
|
||||
from.Key.compare(from.Key.size() - 7, 7, "_source") == 0;
|
||||
auto const futureEdges = _edges.find(from.Key);
|
||||
bool const futurePortalSource = futureEdges != _edges.end() &&
|
||||
std::any_of(futureEdges->second.begin(), futureEdges->second.end(),
|
||||
[](ClientlessTravelEdge const& edge)
|
||||
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
||||
if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 &&
|
||||
!(blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))
|
||||
@@ -1547,9 +1794,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (exit.EdgeId != entry.Edge->DatabaseId ||
|
||||
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
|
||||
continue;
|
||||
float const score = current.Cost + probe.PathLength + exit.LocalCost +
|
||||
float const score = current.Cost + groundCost(probe.PathLength) + exit.LocalCost +
|
||||
roadSectionCost(exit.EdgeId, exit.Reverse, entryIndex,
|
||||
exit.EndIndex, entry.Edge->Cost);
|
||||
exit.EndIndex, edgeCost(*entry.Edge));
|
||||
if (score >= bestGoalCost)
|
||||
continue;
|
||||
bestGoalCost = score;
|
||||
@@ -1561,9 +1808,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
std::string const nextKey = stateKey(next);
|
||||
uint32 const begin = entry.Edge->Reverse
|
||||
? entry.PointCount - 1 - entry.Index : entry.Index;
|
||||
float const cost = current.Cost + probe.PathLength +
|
||||
float const cost = current.Cost + groundCost(probe.PathLength) +
|
||||
roadSectionCost(entry.Edge->DatabaseId, entry.Edge->Reverse,
|
||||
begin, entry.PointCount - 1, entry.Edge->Cost);
|
||||
begin, entry.PointCount - 1, edgeCost(*entry.Edge));
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && cost >= old->second)
|
||||
continue;
|
||||
@@ -1677,7 +1924,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
|
||||
// ground connections, not penalized fallback routes.
|
||||
float const candidate = current.Cost + measuredLength;
|
||||
float const candidate = current.Cost + groundCost(measuredLength);
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && candidate >= old->second)
|
||||
continue;
|
||||
@@ -1692,6 +1939,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
}
|
||||
|
||||
if(bestGoalState.empty() && std::isfinite(directFlightCost))return directFlightRoute();
|
||||
|
||||
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
|
||||
!bestGoalState.empty())
|
||||
previous.erase(bestGoalState);
|
||||
@@ -2042,8 +2291,11 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& anchorNode,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds)
|
||||
std::unordered_set<uint32> const* blockedEdgeIds,
|
||||
ClientlessTravelCost::Profile const* journeyCosts)
|
||||
{
|
||||
ClientlessTravelCost::Profile const defaults;
|
||||
auto const& costs=journeyCosts?*journeyCosts:defaults;
|
||||
route.clear();
|
||||
anchorNode.clear();
|
||||
error.clear();
|
||||
@@ -2112,8 +2364,8 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
if (localStartDistance > startRadius)
|
||||
continue;
|
||||
|
||||
distance[node.Key] = localStartDistance;
|
||||
open.push({ localStartDistance, node.Key });
|
||||
distance[node.Key] = costs.Cost(ClientlessTravelCost::Ground,localStartDistance);
|
||||
open.push({ distance[node.Key], node.Key });
|
||||
++startCandidateCount;
|
||||
}
|
||||
|
||||
@@ -2143,7 +2395,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||
continue;
|
||||
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
|
||||
if (candidate >= distance[edge.ToNode])
|
||||
continue;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "Common.h"
|
||||
#include "ClientlessGraphProbeCache.h"
|
||||
#include "ClientlessTravelCost.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -114,13 +115,16 @@ public:
|
||||
bool Reload();
|
||||
bool EnsureLoaded();
|
||||
|
||||
ClientlessTravelCost::Profile NewJourneyCosts() const;
|
||||
|
||||
bool PlanRoute(
|
||||
Player const* player,
|
||||
std::string const& destinationNode,
|
||||
float startRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
|
||||
|
||||
bool PlanRouteToPosition(
|
||||
Player const* player,
|
||||
@@ -133,7 +137,8 @@ public:
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& anchorNode,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
|
||||
|
||||
// C8.13b: transient local bridges connect nearby graph islands for one
|
||||
// route plan. Bridges are never persisted and may never follow another
|
||||
@@ -151,7 +156,8 @@ public:
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||
std::unordered_set<std::string> const* blockedBridgeKeys = nullptr,
|
||||
ClientlessTravelLocationGoal const* locationGoal = nullptr);
|
||||
ClientlessTravelLocationGoal const* locationGoal = nullptr,
|
||||
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
|
||||
|
||||
bool FindNearestNodeToPosition(
|
||||
uint32 mapId,
|
||||
@@ -177,9 +183,12 @@ public:
|
||||
|
||||
static bool ResolvePortalUse(uint32 spawnId, ClientlessPortalUse& portal, std::string& error);
|
||||
ClientlessPortalUse const* GetPortalUse(uint32 edgeDatabaseId) const;
|
||||
bool PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
||||
bool AdventurePortalUnlockAllowed(Player const* player,uint32 edgeDatabaseId) const;
|
||||
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
||||
|
||||
private:
|
||||
void RebuildEdgeDistances();
|
||||
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
|
||||
|
||||
ClientlessGraphProbeCache _positionProbes;
|
||||
|
||||
@@ -540,6 +540,7 @@ bool BattlefieldWG::SetupBattlefield()
|
||||
|
||||
void BattlefieldWG::OnBattleStart()
|
||||
{
|
||||
sBattlefieldMgr->UpdateWintergraspFlight(m_Map);
|
||||
// Spawn titan relic
|
||||
if (GameObject* relic = SpawnGameObject(GO_WINTERGRASP_TITAN_S_RELIC, WintergraspRelicPos, WintergraspRelicRot))
|
||||
{
|
||||
@@ -628,6 +629,7 @@ void BattlefieldWG::UpdateCounterVehicle(bool init)
|
||||
|
||||
void BattlefieldWG::OnBattleEnd(bool endByTimer)
|
||||
{
|
||||
sBattlefieldMgr->UpdateWintergraspFlight(m_Map);
|
||||
// Remove relic
|
||||
if (m_titansRelicGUID)
|
||||
if (GameObject* relic = GetGameObject(m_titansRelicGUID))
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessWorldPointMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
@@ -2617,6 +2618,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
|
||||
} },
|
||||
{ "adventure", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureBehaviorMgr->Command(handler,args); }, "" },
|
||||
{ "groupmanager", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureGroupMgr->Command(handler,args); }, "" },
|
||||
{ "flight", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessFlight, "" },
|
||||
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
|
||||
@@ -193,8 +193,8 @@ bool OutdoorPvPZM::SetupOutdoorPvP()
|
||||
for (uint8 i = 0; i < OutdoorPvPZMBuffZonesNum; ++i)
|
||||
RegisterZone(OutdoorPvPZMBuffZones[i]);
|
||||
|
||||
m_Graveyard = std::make_unique<OPvPCapturePointZM_Graveyard>(this);
|
||||
AddCapturePoint(m_Graveyard.get()); // though the update function isn't used, the handleusego is!
|
||||
m_Graveyard = new OPvPCapturePointZM_Graveyard(this);
|
||||
AddCapturePoint(m_Graveyard); // though the update function isn't used, the handleusego is!
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ class OutdoorPvPZM : public OutdoorPvP
|
||||
void SetHordeTowersControlled(uint32 count);
|
||||
|
||||
private:
|
||||
std::unique_ptr<OPvPCapturePointZM_Graveyard> m_Graveyard;
|
||||
// Owned by OutdoorPvP::m_capturePoints after AddCapturePoint.
|
||||
OPvPCapturePointZM_Graveyard* m_Graveyard = nullptr;
|
||||
|
||||
uint32 m_AllianceTowersControlled;
|
||||
uint32 m_HordeTowersControlled;
|
||||
|
||||
@@ -1273,6 +1273,24 @@ DeletedCharacterTicketTrace = 0
|
||||
|
||||
DungeonFinder.OptionsMask = 1
|
||||
|
||||
#
|
||||
# DungeonFinder.LowPopulation.Enable
|
||||
# DungeonFinder.LowPopulation.FallbackDelay
|
||||
# DungeonFinder.LowPopulation.MinPlayers
|
||||
# DungeonFinder.LowPopulation.SoloGroupOutsideTimeout
|
||||
# Description: Allow the dungeon finder to create a dungeon group below
|
||||
# the normal group size after the fallback delay. The group
|
||||
# remains marked as low-population while inside the dungeon.
|
||||
# Default: Enable = 0
|
||||
# FallbackDelay = 60 seconds
|
||||
# MinPlayers = 1
|
||||
# SoloGroupOutsideTimeout = 300 seconds
|
||||
|
||||
DungeonFinder.LowPopulation.Enable = 0
|
||||
DungeonFinder.LowPopulation.FallbackDelay = 60
|
||||
DungeonFinder.LowPopulation.MinPlayers = 1
|
||||
DungeonFinder.LowPopulation.SoloGroupOutsideTimeout = 300
|
||||
|
||||
#
|
||||
# DungeonFinder.CallToArmsEnable
|
||||
# Description: Enable Call to Arms bonus mechanism for random Cataclysm dungeons
|
||||
@@ -3962,6 +3980,7 @@ Appender.Console=1,3,0
|
||||
Appender.Server=2,2,0,Server.log,w
|
||||
Appender.GM=2,2,15,gm/gm_%s.log
|
||||
Appender.DBErrors=2,2,0,DBErrors.log
|
||||
Appender.AdventureBotNavigation=2,2,7,adventurebot-navigation.log,a,67108864
|
||||
|
||||
# Logger config values: Given a logger "name"
|
||||
# Logger.name
|
||||
@@ -3987,6 +4006,7 @@ Logger.commands.gm=3,Console GM
|
||||
Logger.scripts.hotswap=3,Console Server
|
||||
Logger.sql.sql=5,Console DBErrors
|
||||
Logger.sql.updates=3,Console Server
|
||||
Logger.server.adventurebot.navigation=2,AdventureBotNavigation
|
||||
#Logger.mmaps=3,Server
|
||||
|
||||
#Logger.achievement=3,Console Server
|
||||
@@ -4376,3 +4396,18 @@ Clientless.Browser.Battleground.QueueEnabled = 1
|
||||
Clientless.Browser.Battleground.JoinEnabled = 0
|
||||
# Explicit isolated test override. Does not override disabled Queue/Join switches.
|
||||
Clientless.Browser.AllowBotOnlyTests = 0
|
||||
|
||||
# Clientless travel route scoring: measured path length (yards), not ticket
|
||||
# price or the legacy travel_edge.cost column. Higher factors discourage a
|
||||
# mode. Portal transitions have a unit base cost (no fictitious map distance).
|
||||
# Variation draws independent positive mode factors once per journey; recovery
|
||||
# retains the draw and group members inherit their leader's profile and route.
|
||||
Clientless.Travel.CostMultiplier.Ground = 1.0
|
||||
Clientless.Travel.CostMultiplier.Taxi = 1.0
|
||||
Clientless.Travel.CostMultiplier.Flight = 1.0
|
||||
Clientless.Travel.CostMultiplier.Ship = 1.0
|
||||
Clientless.Travel.CostMultiplier.Zeppelin = 1.0
|
||||
Clientless.Travel.CostMultiplier.Tram = 1.0
|
||||
Clientless.Travel.CostMultiplier.Portal = 1.0
|
||||
# 0 disables variation; 0.20 means a per-mode range of 80%..120% per journey.
|
||||
Clientless.Travel.CostVariation = 0.20
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
source = (root / "src/server/game/World/AdventureGroupMgr.cpp").read_text()
|
||||
|
||||
# The integration-heavy manager remains native C++, but these assertions keep
|
||||
# its safety boundaries visible when the surrounding group code is refactored.
|
||||
for required in (
|
||||
"ManagerWindow(plan.Deadline,now)",
|
||||
"ManagerLevelCompatible(member->getLevel(),present->getLevel())",
|
||||
"!BotLed(p)",
|
||||
"Complete(p)",
|
||||
"ProfessionCompatible(leader,member,Activity(targetPlan))",
|
||||
"CanInvite(leader,member)",
|
||||
"NeedsRole(leader,member)",
|
||||
"sourceSize>=targetGroup->GetMembersCount()",
|
||||
"Never hand forced leadership to a real player",
|
||||
"GROUP_MANAGER_TRANSFER",
|
||||
'op=="preview"',
|
||||
'op=="run"',
|
||||
):
|
||||
assert required in source, required
|
||||
|
||||
code = r'''
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include <cassert>
|
||||
int main()
|
||||
{
|
||||
using namespace AdventureGroupPolicy;
|
||||
assert(ManagerLevelCompatible(60, 55));
|
||||
assert(!ManagerLevelCompatible(60, 54));
|
||||
assert(ManagerLevelCompatible(84, 79));
|
||||
assert(!ManagerLevelCompatible(84, 78));
|
||||
assert(ManagerLevelCompatible(85, 85));
|
||||
assert(!ManagerLevelCompatible(85, 84));
|
||||
assert(!ManagerLevelCompatible(80, 85));
|
||||
assert(ManagerWindow(1600, 1000));
|
||||
assert(ManagerWindow(1300, 1000));
|
||||
assert(!ManagerWindow(1601, 1000));
|
||||
assert(!ManagerWindow(1000, 1000));
|
||||
assert(!ManagerWindow(999, 1000));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
directory = Path(directory)
|
||||
cpp = directory / "test.cpp"
|
||||
exe = directory / "test"
|
||||
cpp.write_text(code)
|
||||
subprocess.run([
|
||||
"c++", "-std=c++17", "-I" + str(root / "src/server/game/World"),
|
||||
str(cpp), "-o", str(exe)
|
||||
], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
print("Formation manager window, level-85 isolation and native safety gates passed.")
|
||||
@@ -23,6 +23,7 @@ code=r'''
|
||||
#include <set>
|
||||
using uint32=unsigned;
|
||||
#define TC_GAME_API
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
constexpr float INVALID_HEIGHT=-100000;
|
||||
constexpr int LINEOFSIGHT_ALL_CHECKS=0,CMSG_AREATRIGGER=0;
|
||||
namespace VMAP{enum class ModelIgnoreFlags{None};}
|
||||
@@ -100,6 +101,11 @@ int main(){
|
||||
travel.status.State=ClientlessTravelState::Arrived;leader.mounted=true;mgr.Follow(&a);assert(a.mounted);leader.mounted=false;a=Player(26); // retain a clean origin
|
||||
// A ground-only bot waits safely when the leader gains excessive height.
|
||||
leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0;
|
||||
// A shared/action-owned catch-up corridor survives a leader landing ahead.
|
||||
flight.status.State=ClientlessFlightState::Flying;flight.status.FollowingPlayer=false;
|
||||
mgr.Observe(&a,1);leader.flying=false;auto previousLands=flight.lands;
|
||||
assert(mgr.Follow(&a)&&flight.lands==previousLands);
|
||||
flight.status.State=ClientlessFlightState::Arrived;mgr.Follow(&a);
|
||||
// A native-capable follower uses flight, and safely lands after a kick.
|
||||
flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a);
|
||||
// A stale owned ground catch-up route cannot suppress a visible-phase
|
||||
|
||||
@@ -69,6 +69,7 @@ code+=r'''
|
||||
void AdventureGroupMgr::UpdateBrowser(Player*){}
|
||||
bool AdventureGroupMgr::Merge(Player*,AdventureGroupPlan const&){return false;}
|
||||
bool AdventureGroupMgr::NeedsRole(Player*,Player*){return true;}
|
||||
void AdventureGroupMgr::MaybeRunFormationManager(time_t){}
|
||||
bool AdventureGroupMgr::Complete(Player*p){auto group=p->GetGroup();if(!group)return false;auto plan=sAdventureBehaviorMgr->GroupPlan(p);return group->GetMembersCount()==(plan.Capacity?plan.Capacity:AdventureGroupPolicy::Capacity(plan.Forming?plan.NextIntent:plan.Intent));}
|
||||
'''
|
||||
code+=fun('float Sociability(')+'\n'+fun('bool ProfessionCompatible(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')+'\n'+fun('unsigned Capacity(')
|
||||
|
||||
@@ -22,6 +22,19 @@ assert "r.GroupOverride" in follower_hold
|
||||
assert "r.Dirty=false" in follower_hold
|
||||
assert "GROUP_MEMBER_FALLBACK_HOLD" in follower_hold
|
||||
|
||||
begin_start = source.index("void AdventureBehaviorMgr::Begin(")
|
||||
begin_end = source.index("void AdventureBehaviorMgr::Fallback(", begin_start)
|
||||
begin = source[begin_start:begin_end]
|
||||
assert '"portal_at_"+std::to_string(r.Instance.Entry)+"_source"' in begin
|
||||
assert 'Queue(player,r,"travel_to_node","node"' in begin
|
||||
|
||||
update_start = source.index("void AdventureBehaviorMgr::Update(Player * player")
|
||||
update_end = source.index("void AdventureBehaviorMgr::FinishInstance(", update_start)
|
||||
update = source[update_start:update_end]
|
||||
assert 'arrivalContract="native-area-trigger"' in update
|
||||
assert "player->IsInAreaTriggerRadius(entrance)" in update
|
||||
assert "INTENT_ARRIVAL_REJECT" in update
|
||||
|
||||
protect_start = source.index("bool AdventureBehaviorMgr::ProtectGroupLeave(")
|
||||
protect_end = source.index("void AdventureBehaviorMgr::InterruptForInteraction(", protect_start)
|
||||
protection = source[protect_start:protect_end]
|
||||
@@ -35,4 +48,9 @@ for invariant in (
|
||||
):
|
||||
assert invariant in protection
|
||||
|
||||
print("Instance visit retention, inherited group-target hold and formation leave protection passed.")
|
||||
flight = (root / "src/server/game/World/ClientlessFlightMgr.cpp").read_text()
|
||||
assert "OpenColumnFrom" in flight
|
||||
assert "overheadSurface" in flight
|
||||
assert "flight-departure-overhead-surface" in flight
|
||||
|
||||
print("Instance graph entrance/arrival contract, visit retention, flight overhead handling, group-target hold and formation leave protection passed.")
|
||||
@@ -58,7 +58,7 @@ unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,flo
|
||||
namespace ObjectAccessor {std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}}
|
||||
struct Profile {std::string Locale="deDE";std::unordered_map<std::string,float> Personality;};
|
||||
struct Lua {std::unordered_map<unsigned,Profile> profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua;
|
||||
struct Behavior {int interrupts=0;bool protectGroupLeave=false;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}bool ProtectGroupLeave(Player*)const{return protectGroupLeave;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
struct Behavior {int interrupts=0;bool protectGroupLeave=false,protectInstance=false;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}bool ProtectGroupLeave(Player*)const{return protectGroupLeave;}bool ProtectInstanceGroupLeave(Player*)const{return protectInstance;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat;
|
||||
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
|
||||
struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core;
|
||||
@@ -133,6 +133,17 @@ int main(){
|
||||
mgr._records.clear();behavior.protectGroupLeave=true;behavior.schedule="groupformation:meeting";leader.afk=true;
|
||||
tick(8000);behavior.schedule="instance:dungeon";tick(9000);assert(bot.group);
|
||||
behavior.protectGroupLeave=false;leader.afk=false;behavior.schedule="adventure:road";tick(9001);assert(!bot.group);bot.group=&group;
|
||||
// Instance travel/inside protects late arrivals from both AFK and distance.
|
||||
mgr._records.clear();behavior.schedule="raid:instance";behavior.protectInstance=true;
|
||||
leader.afk=true;groupTravel.separated=true;tick(10000);tick(12000);assert(bot.group);
|
||||
// Explicit intent changes retain their existing behavior.
|
||||
behavior.schedule="profession:city";tick(12001);assert(!bot.group);
|
||||
bot.group=&group;behavior.schedule="raid:instance";tick(13000);
|
||||
behavior.protectInstance=false;leader.afk=false;groupTravel.separated=false;
|
||||
tick(13599);assert(bot.group);tick(13600);assert(!bot.group);
|
||||
bot.group=&group;behavior.protectInstance=true;groupTravel.separated=true;tick(14000);
|
||||
behavior.protectInstance=false;tick(14001);assert(!bot.group);
|
||||
bot.group=&group;groupTravel.separated=false;
|
||||
// Explicit diagnostic leave uses ordinary safe-state guards.
|
||||
bot.combat=true;assert(!mgr.RequestLeave(&bot)&&bot.group);bot.combat=false;
|
||||
assert(mgr.RequestLeave(&bot)&&!bot.group);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Verify delayed dynamic bridge rejection blocks only its directed connection."""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
source = (root / "src/server/game/World/ClientlessActionExecutorMgr.cpp").read_text()
|
||||
start = source.index(' if (!failedEdgeDatabaseId && reason.rfind("route-local-", 0) == 0 &&')
|
||||
end = source.index(' // A failed initial local approach', start)
|
||||
code = r'''
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <cassert>
|
||||
struct Step {std::string NodeKey,EdgeType;};
|
||||
struct Record {unsigned RouteStep;std::vector<Step> Route;std::unordered_set<std::string> FailedBridgeKeys;};
|
||||
void reject(Record& record,unsigned failedEdgeDatabaseId,std::string reason){
|
||||
''' + source[start:end] + r'''
|
||||
}
|
||||
int main(){Record r{1,{{"taxi_2","start"},{"stormwind_tradecenter","local_bridge"}}, {}};
|
||||
reject(r,0,"route-local-PathRejected");assert(r.FailedBridgeKeys.count("taxi_2->stormwind_tradecenter"));
|
||||
assert(!r.FailedBridgeKeys.count("stormwind_tradecenter->taxi_2"));
|
||||
r.FailedBridgeKeys.clear();reject(r,15,"route-local-PathRejected");assert(r.FailedBridgeKeys.empty());
|
||||
r.RouteStep=0;reject(r,0,"route-local-PathRejected");assert(r.FailedBridgeKeys.empty());
|
||||
r.RouteStep=1;r.Route[1].EdgeType="free_flight";reject(r,0,"route-local-PathRejected");assert(r.FailedBridgeKeys.empty());}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-dynamic-bridge-") as directory:
|
||||
path=Path(directory)
|
||||
(path/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17",str(path/"test.cpp"),"-o",str(path/"test")],check=True)
|
||||
subprocess.run([str(path/"test")],check=True)
|
||||
print("Delayed bridge rejection, direction, persistent edges and non-bridge exclusions PASS")
|
||||
@@ -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;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));
|
||||
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)
|
||||
@@ -61,6 +61,7 @@ bool ClearRay(Player*,Point const&a,Point const&b){
|
||||
'''+column+r'''
|
||||
struct ClientlessFlightMgr{
|
||||
std::unordered_map<unsigned,Record>_records;Surface surface{0,300,true};int approached=0;bool seek=false;
|
||||
void Store(unsigned guid,Record&& record){_records[guid]=std::move(record);}
|
||||
Surface Sample(Player*,Record&,float,float){return surface;}
|
||||
bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&);
|
||||
bool Prepare(Player*player,Record&record,std::string&error){auto origin=surface;bool following=false;
|
||||
|
||||
@@ -10,6 +10,7 @@ b = source.index('bool ClientlessFlightMgr::Command(', a)
|
||||
update = source[a:b]
|
||||
harness = r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include "ClientlessFlightProgress.h"
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
@@ -24,7 +25,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;
|
||||
@@ -43,14 +44,18 @@ 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;
|
||||
ClientlessFlightProgress::Watch Progress;uint32 StallRecoveries=0;SharedJoin Join;
|
||||
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::make_shared<SharedTrail>();std::vector<Position> Breadcrumbs;
|
||||
};
|
||||
enum class ClientlessTravelState {Moving,Arrived,Stuck};
|
||||
struct Travel {struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;
|
||||
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,15 +69,24 @@ 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(){
|
||||
// Small real movements accumulate; oscillating distance does not reset best progress.
|
||||
ClientlessFlightProgress::Watch watch;watch.Begin(100,120000);
|
||||
for(int i=0;i<59;++i)watch.Observe(100+(i%2?0.1f:-0.1f),1000);
|
||||
assert(!watch.Stalled());watch.Observe(100,1000);assert(watch.Stalled());
|
||||
watch.Observe(99.4f,200);assert(!watch.Stalled()&&watch.IdleMs==0);
|
||||
// Vertical movement is measured with the same 3D distance as horizontal flight.
|
||||
watch.Begin(Distance({0,0,0},{0,0,200}),20000);
|
||||
watch.Observe(Distance({0,0,1},{0,0,200}),1000);assert(watch.IdleMs==0);
|
||||
ClientlessFlightMgr mgr;mgr._records[26]={};
|
||||
mgr._records[26].Progress.Begin(25,2000);
|
||||
// Begin replacement while the existing spline is still active: no stop.
|
||||
mgr.Update(50);assert(mgr.handoffs==1&&mgr.advances==1&&player.stops==0);
|
||||
// The 200ms collision cadence must not launch a replacement each tick.
|
||||
mgr.Update(50);assert(mgr.advances==1);
|
||||
mgr._records[26].CheckTimer=0;player.spline.remaining=900;
|
||||
mgr._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;
|
||||
@@ -87,14 +101,45 @@ int main(){
|
||||
assert(player.stops==1&&mgr.advances==2&&mgr.handoffs==1);
|
||||
// A completed spline continues via the normal non-handoff path.
|
||||
clear=true;r.CheckTimer=0;mgr.Update(50);assert(mgr.advances==3);
|
||||
// Long flights keep running when actual progress continues, even overdue.
|
||||
r.Returning=false;r.Moving=true;player.running=true;player.spline.remaining=3000;
|
||||
r.Progress.Begin(1000,20000);r.Segment={1007,0,60};
|
||||
for(int i=0;i<90;++i){player.x+=1;r.CheckTimer=0;mgr.Update(1000);}
|
||||
assert(mgr.failures==0&&r.Progress.Overdue()&&r.Progress.IdleMs==0);
|
||||
// A genuinely stuck spline survives 59s, then replans locally at 60s.
|
||||
r.Progress.Begin(Distance(PointOf(player),PointOf(r.Segment)),20000);r.StallRecoveries=0;
|
||||
int before=mgr.advances;
|
||||
for(int i=0;i<59;++i){r.CheckTimer=0;mgr.Update(1000);}
|
||||
assert(mgr.advances==before&&mgr.failures==0);
|
||||
r.CheckTimer=0;mgr.Update(1000);
|
||||
assert(mgr.advances==before+1&&r.StallRecoveries==1&&mgr.failures==0);
|
||||
player.fly=false;mgr.Update(50);assert(mgr.failures==1);
|
||||
// A ground departure approach must wait for actual arrival before mounting.
|
||||
r.Status.State=ClientlessFlightState::Mounting;r.ApproachingDeparture=true;
|
||||
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);
|
||||
// Repeated genuine stalls exhaust local recovery, without dropping the leader trail.
|
||||
ClientlessFlightMgr stuck;auto& sr=stuck._records[77];
|
||||
sr.Following=std::make_shared<int>(1);auto leaderTrail=sr.Following;
|
||||
sr.Segment={player.x+100,0,60};player.fly=true;player.spline.remaining=3000;
|
||||
for(int attempt=0;attempt<3;++attempt){
|
||||
sr.Moving=true;player.running=true;sr.Progress.Begin(100,4000);
|
||||
for(int i=0;i<60;++i){sr.CheckTimer=0;stuck.Update(1000);}
|
||||
assert(sr.Following==leaderTrail);
|
||||
assert(stuck.failures==(attempt==2?1:0));
|
||||
}
|
||||
assert(stuck.advances==2&&sr.StallRecoveries==2);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-flight-handoff-') as folder:
|
||||
@@ -103,4 +148,4 @@ with tempfile.TemporaryDirectory(prefix='vcore-flight-handoff-') as folder:
|
||||
cpp.write_text(harness)
|
||||
subprocess.run(['c++', '-std=c++17', '-I', str(root/'src/server/game/World'), str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print('Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.')
|
||||
print('Flight handoff, collision, 60s measured stall, jitter, vertical progress, long-flight overrun and local recovery passed.')
|
||||
@@ -51,6 +51,8 @@ struct Profile{bool CanFreeFly=true;std::vector<Mount> FlightMounts{{}};} profil
|
||||
struct Lua{auto GetAdventureProfile(uint32){return &profile;}} lua;
|
||||
struct Config{int reads=0;bool enabled=true;bool GetBoolDefault(const char*,bool){++reads;return enabled;}float GetFloatDefault(const char*,float v){++reads;return v;}std::string GetStringDefault(const char*,const char*v){++reads;return v;}} config;
|
||||
auto sClientlessLuaEngine=&lua;auto sConfigMgr=&config;
|
||||
struct DBCManager{static bool IsInArea(uint32 area,uint32 zone){return area==zone || (zone==4197&&area==4538);}};
|
||||
struct Battlefields{bool war=false;bool IsWintergraspFlightAllowed(uint32 map,uint32 area){return map==571&&DBCManager::IsInArea(area,4197)&&!war;}} battlefield;auto sBattlefieldMgr=&battlefield;
|
||||
struct SpellMgr{void* GetSpellInfo(uint32){return this;}} spells;auto sSpellMgr=&spells;
|
||||
uint32 FlightType(void*){return 1;}
|
||||
struct MapEntry{uint32 MapType=0;} entry;struct Maps{auto LookupEntry(uint32){return &entry;}} maps;auto& sMapStore=maps;
|
||||
@@ -123,6 +125,12 @@ int main(){
|
||||
profile.CanFreeFly=false;assert(!mgr.Enabled(&p));
|
||||
mgr.BeginPlanning();assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));
|
||||
profile.CanFreeFly=true;
|
||||
// Static Wintergrasp subareas lack flight flags, but peace permits flight.
|
||||
mgr.areaCode=4538;mgr.zoneCode=4197;area.fly=false;p.cold=true;
|
||||
assert(std::isfinite(mgr.Estimate(&p,571,a,b)));
|
||||
battlefield.war=true;assert(!std::isfinite(mgr.Estimate(&p,571,a,b)));
|
||||
battlefield.war=false;assert(std::isfinite(mgr.Estimate(&p,571,a,b)));
|
||||
mgr.areaCode=87;mgr.zoneCode=12;assert(!std::isfinite(mgr.Estimate(&p,571,a,b)));area.fly=true;
|
||||
int beforeInterrupts=p.interrupts;Record approach;approach.ApproachingDeparture=true;approach.Status.State=ClientlessFlightState::Mounting;mgr.Fail(&p,approach,"interrupted");assert(travel.cancels==1&&!approach.ApproachingDeparture&&approach.Status.State==ClientlessFlightState::Failed&&p.interrupts==beforeInterrupts);
|
||||
Record revoked;p.canFly=false;mgr.Fail(&p,revoked,"permission lost");assert(revoked.Status.State==ClientlessFlightState::Failed&&!revoked.Returning);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Replan during takeoff without prematurely demanding cruise clearance."""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
code = r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
using namespace ClientlessFlightPath;
|
||||
int main(){Settings c;Memory m;auto sample=[](float,float){return Surface{94.314f,550,true};};auto ray=[](Point const&,Point const&){return true;};Point goal{-8186,419,94.314f};auto n=Next({-8829,624,94.314f},goal,c,m,sample,ray);assert(n.Valid&&!m.Departed);n=Next({-8826.7f,622.306f,96.4122f},goal,c,m,sample,ray);assert(n.Valid&&!m.Departed);n=Next({-8800,610,104},goal,c,m,sample,ray);assert(n.Valid&&m.Departed);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-takeoff-handoff-") as directory:
|
||||
path = Path(directory)
|
||||
(path / "test.cpp").write_text(code)
|
||||
subprocess.run(["c++", "-std=c++17", "-I" + str(root / "src/server/game/World"), str(path / "test.cpp"), "-o", str(path / "test")], check=True)
|
||||
subprocess.run([str(path / "test")], check=True)
|
||||
print("Takeoff handoff at two yards and promotion at real cruise clearance PASS")
|
||||
@@ -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);
|
||||
|
||||
@@ -28,6 +28,15 @@ int main(){
|
||||
assert(touchdown.Valid&&touchdown.Landing&&Distance(touchdown.Next,{0,0,0})<0.01f);
|
||||
auto solidWall=[](Point const&,Point const&){return false;};
|
||||
assert(!Next({6,0,10},{0,0,0},Settings{},finalApproach,flat,solidWall).Valid);
|
||||
// Diagonal movement is blocked but the local air column is clear: take off
|
||||
// and touch down vertically instead of rejecting an otherwise safe flight.
|
||||
auto verticalOnly=[](Point const&a,Point const&b){return Horizontal(a,b)<0.1f;};
|
||||
Memory verticalTakeoff;
|
||||
auto ascent=Next({0,0,0},{100,0,0},Settings{},verticalTakeoff,flat,verticalOnly);
|
||||
assert(ascent.Valid&&Horizontal({0,0,0},ascent.Next)<0.1f&&ascent.Next.Z>10);
|
||||
Memory verticalLanding;verticalLanding.Departed=true;
|
||||
auto descent=Next({0,0,40},{0,0,0},Settings{},verticalLanding,flat,verticalOnly);
|
||||
assert(descent.Valid&&descent.Landing&&Distance(descent.Next,{0,0,0})<0.01f);
|
||||
auto normal=simulate({0,0,0},{800,0,0},flat,clear);
|
||||
assert(normal.Arrived&&normal.Peak>30&&normal.Peak<=60.1f);
|
||||
auto low=[](float,float){return Surface{0,42,true};};
|
||||
@@ -64,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);
|
||||
@@ -84,3 +93,10 @@ with tempfile.TemporaryDirectory(prefix='vcore-free-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)
|
||||
|
||||
flight=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
|
||||
executor=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
assert 'FLIGHT_AIRBORNE_REPLAN' in flight
|
||||
assert 'OwnsActiveFlight(guid)' in flight
|
||||
assert 'found->second.ActionOwned=actionOwned' in flight
|
||||
assert 'itr->second.Active && itr->second.FlightActive' in executor
|
||||
@@ -11,6 +11,7 @@ preamble=r'''
|
||||
#include "ClientlessGraphProbeCache.h"
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -34,12 +35,14 @@ struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious
|
||||
struct Travel {bool complete=true;void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);n.Complete=complete;}} travel;
|
||||
auto sClientlessTravelMgr=&travel;
|
||||
struct Flight {
|
||||
bool enabled=false;float minimumX=-10000;
|
||||
float closedFromX=1e9;bool rejectNearbyTargets=false;bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9;
|
||||
float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9;
|
||||
void BeginPlanning(Player const* = nullptr){}
|
||||
bool Enabled(Player const*){return enabled;}
|
||||
bool Preferred(Player const*,uint32,Position const&){return false;}
|
||||
float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){
|
||||
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits<float>::infinity();
|
||||
bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f;
|
||||
float d=dist(a,b);return enabled&&std::fabs(a.x-closedFromX)>0.1f&&!(rejectNearbyTargets&&b.x>1&&b.x<450)&&map==0&&a.x>=minimumX&&std::fabs(b.x-forbiddenTargetX)>0.1f&&!forbiddenPair&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits<float>::infinity();
|
||||
}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
@@ -162,6 +165,92 @@ travel.complete=true;route.clear();
|
||||
assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
|
||||
assert(route.front().NodeKey!="flight_departure");
|
||||
travel.complete=true;
|
||||
// Any allowed graph node can be a bounded, on-demand flight re-entry. The
|
||||
// planner must not require a portal/transport node type or an MMAP bridge.
|
||||
flight.forbiddenTargetX=2000;
|
||||
ClientlessTravelGraphMgr genericEntry;node(genericEntry,"A",0);node(genericEntry,"R",500);node(genericEntry,"B",2000);
|
||||
edge(genericEntry,"R","B",88,10,false,"ground");
|
||||
travel.complete=false;route.clear();
|
||||
assert(genericEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()==3&&route[0].NodeKey=="A"&&route[1].NodeKey=="R");
|
||||
assert(route[1].EdgeType=="free_flight"&&route[2].EdgeDatabaseId==88);
|
||||
flight.forbiddenTargetX=1e9;travel.complete=true;
|
||||
// Keep probe budget for a later graph state: fly to a generic portal source,
|
||||
// cross it, and then fly to the destination. A rejected direct flight must
|
||||
// not consume all estimates at the initial node.
|
||||
flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000;
|
||||
ClientlessTravelGraphMgr portalEntry;node(portalEntry,"A",0);node(portalEntry,"P",500);
|
||||
node(portalEntry,"Q",1500);node(portalEntry,"B",2000);
|
||||
edge(portalEntry,"P","Q",89,1,false,"portal_use");
|
||||
travel.complete=false;route.clear();
|
||||
assert(portalEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()==4&&route[1].NodeKey=="P"&&route[1].EdgeType=="free_flight");
|
||||
assert(route[2].NodeKey=="Q"&&route[2].EdgeType=="portal_use");
|
||||
assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight");
|
||||
flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true;
|
||||
|
||||
// Replanning AT an interactive portal must traverse it, even when flying
|
||||
// directly from the indoor source looks cheaper to the estimate.
|
||||
ClientlessTravelGraphMgr rootPortal;node(rootPortal,"P",500);node(rootPortal,"Q",-1000);node(rootPortal,"B",2000);
|
||||
edge(rootPortal,"P","Q",91,1,false,"portal_use");
|
||||
Player atPortal;atPortal.x=500;travel.complete=false;route.clear();
|
||||
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()>=3&&route[1].EdgeType=="portal_use"&&route[1].NodeKey=="Q");
|
||||
// A blocked portal does not trap the actor: its recorded escape road remains.
|
||||
edge(rootPortal,"P","B",92,400,false,"ground");
|
||||
std::unordered_set<uint32> failedPortal{91};flight.enabled=false;route.clear();
|
||||
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why,&failedPortal)&&done);
|
||||
assert(route.back().EdgeDatabaseId==92);
|
||||
// Many nearby inaccessible anchors must leave budget for an isolated portal
|
||||
// outside the local-entry radius and the flight from its destination.
|
||||
flight.enabled=true;flight.rejectNearbyTargets=true;
|
||||
flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000;
|
||||
ClientlessTravelGraphMgr crowded;node(crowded,"A",0);node(crowded,"P",500);node(crowded,"Q",1500);node(crowded,"B",2000);
|
||||
edge(crowded,"P","Q",93,1,false,"portal_use");
|
||||
for(unsigned i=1;i<=40;++i)node(crowded,"indoor"+std::to_string(i),float(i*10));
|
||||
route.clear();assert(crowded.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)&&done);
|
||||
bool usedIsolatedPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==93)usedIsolatedPortal=true;
|
||||
assert(usedIsolatedPortal);
|
||||
flight.rejectNearbyTargets=false;flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;
|
||||
|
||||
// Speculative road exits and an unusable initial departure must not starve a
|
||||
// nearby flight-only portal. The cheap complete macro route must beat the
|
||||
// long native fallback, not merely appear as a considered candidate.
|
||||
flight.enabled=true;flight.closedFromX=0;travel.complete=true;
|
||||
ClientlessTravelGraphMgr portalBudget;node(portalBudget,"A",0);node(portalBudget,"C",100);
|
||||
node(portalBudget,"P",500);node(portalBudget,"Q",1500);node(portalBudget,"B",2000);
|
||||
edge(portalBudget,"P","Q",94,1,false,"portal_use");
|
||||
edge(portalBudget,"A","B",95,10000,false,"taxi");
|
||||
for(unsigned i=0;i<4;++i){
|
||||
auto key="remote"+std::to_string(i);node(portalBudget,key,3000+float(i*100));
|
||||
edge(portalBudget,"A",key,100+i,10000);
|
||||
for(float x:{0.f,1000.f,2000.f,3000.f}){ClientlessTravelEdgePoint pt;pt.X=x;pt.Y=float(i*20);portalBudget._edgePoints[100+i].push_back(pt);}
|
||||
}
|
||||
route.clear();assert(portalBudget.PlanAdaptiveRoute(&start,"B",150,200,route,done,endKey,why)&&done);
|
||||
bool budgetPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==94)budgetPortal=true;
|
||||
assert(budgetPortal);flight.closedFromX=1e9;
|
||||
|
||||
// Apply modal factors to ALL persisted edges and keep a profile stable over
|
||||
// replanning. Independent journeys may choose different near-equal modes.
|
||||
flight.enabled=false;travel.complete=true;PathGenerator::available=false;
|
||||
ClientlessTravelGraphMgr modal;node(modal,"A",0);node(modal,"B",1000);
|
||||
edge(modal,"A","B",200,1000,false,"ground");edge(modal,"A","B",201,1050,false,"taxi");
|
||||
bool sawGround=false,sawTaxi=false;
|
||||
for(unsigned seed=1;seed<=100;++seed){
|
||||
auto profile=ClientlessTravelCost::Sample({},seed,.2f);
|
||||
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
|
||||
auto picked=route.back().EdgeDatabaseId;sawGround|=picked==200;sawTaxi|=picked==201;
|
||||
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
|
||||
assert(route.back().EdgeDatabaseId==picked);
|
||||
}assert(sawGround&&sawTaxi);
|
||||
// A same-map flight is a competing route, never an unconditional override.
|
||||
flight.enabled=true;ClientlessTravelCost::Profile expensiveFlight;expensiveFlight.Factors[ClientlessTravelCost::Flight]=100;
|
||||
ClientlessTravelLocationGoal sameMap;sameMap.X=1000;sameMap.AnchorRadius=5;
|
||||
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
|
||||
assert(route.back().NodeKey!="flight_location");
|
||||
expensiveFlight.Factors[ClientlessTravelCost::Flight]=.25f;
|
||||
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
|
||||
assert(route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight");
|
||||
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -18,6 +18,7 @@ def method(signature):
|
||||
return source[start:end]
|
||||
|
||||
preamble = r'''
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -77,7 +78,7 @@ struct Flight {
|
||||
bool enabled=false,starts=false,recovery=false;int launches=0;FlightStatus status;
|
||||
void BeginPlanning(){}
|
||||
float Estimate(Player*,uint32,Position const&,Position const&,bool fallback=false){recovery=fallback;return enabled?100:std::numeric_limits<float>::infinity();}
|
||||
bool Start(Player*,Position const&,std::string&){++launches;return starts;}
|
||||
bool Start(Player*,Position const&,std::string&,bool=false){++launches;return starts;}
|
||||
FlightStatus const* GetStatus(uint32){return &status;}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
@@ -95,8 +96,8 @@ struct Graph {
|
||||
} graph;
|
||||
auto sClientlessTravelGraphMgr=&graph;
|
||||
struct Areas{bool AllowsNode(Player*,ClientlessTravelNode const&){return true;}bool AllowsSegment(Player*,uint32,Position const&,Position const&){return true;}}areas;auto sAdventureAreaMgr=&areas;
|
||||
static int recoveries=0;
|
||||
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32,std::string const&){++recoveries;return false;}
|
||||
static int recoveries=0;static uint32 recoveryEdge=0;
|
||||
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32 edge,std::string const&){++recoveries;recoveryEdge=edge;return false;}
|
||||
'''
|
||||
methods = '\n'.join(method(sig) for sig in (
|
||||
'bool StartGroundConnection(',
|
||||
@@ -190,8 +191,10 @@ int main(){
|
||||
// Owner adapters can reject, wait, or shorten a recorded road without
|
||||
// losing the original index mapping or launching an unsafe spline.
|
||||
Record adapted;adapted.Active=true;adapted.EdgePath.resize(5);for(unsigned i=0;i<5;++i)adapted.EdgePath[i].X=150+i*10;p.x=140;
|
||||
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&){return false;};before=p.motion.launches;
|
||||
adapted.Route.resize(1);adapted.Route[0].EdgeDatabaseId=1885;
|
||||
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&){return false;};before=p.motion.launches;auto previousRecoveries=recoveries;
|
||||
assert(!mgr.StartRecordedEdgePath(&p,26,adapted) && p.motion.launches==before);
|
||||
assert(recoveries==previousRecoveries+1 && recoveryEdge==1885);
|
||||
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(1);return true;};
|
||||
assert(mgr.StartRecordedEdgePath(&p,26,adapted) && adapted.FormationWaiting && p.motion.launches==before);
|
||||
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(3);return true;};
|
||||
@@ -203,5 +206,5 @@ with tempfile.TemporaryDirectory(prefix='vcore-ground-exit-') as folder:
|
||||
cpp = Path(folder) / 'test.cpp'
|
||||
binary = Path(folder) / 'test'
|
||||
cpp.write_text(preamble + structs + cls + doubles + methods + tests)
|
||||
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run(['c++','-I'+str(root/'src/server/game/World'),'-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
@@ -26,9 +26,9 @@ namespace Movement{struct MoveSplineInit{Player*p;Point target;MoveSplineInit(Pl
|
||||
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
|
||||
struct Record{
|
||||
struct{ClientlessFlightState State=ClientlessFlightState::Flying;bool FollowingPlayer=true;Position Target,Landing;unsigned Segments=0;float Travelled=5000;std::string Detail;}Status;
|
||||
bool Returning=false,Moving=false;unsigned Elapsed=1000,TargetUpdated=0,SegmentElapsed=0;Position Segment;
|
||||
bool Returning=false,Moving=false;unsigned Elapsed=1000,TargetUpdated=0,SegmentElapsed=0;Position Segment,Start;unsigned SharedWaitSince=0;std::string AbortReason;
|
||||
ClientlessFlightPath::Settings Settings;ClientlessFlightPath::Memory Memory;
|
||||
std::shared_ptr<int>Following;std::vector<Position>Breadcrumbs;
|
||||
std::shared_ptr<SharedTrail>Following;std::shared_ptr<SharedTrail>Published=std::make_shared<SharedTrail>();std::vector<Position>Breadcrumbs;
|
||||
};
|
||||
bool clear=true,valid=true;float ceiling=300;
|
||||
bool ClearRay(Player*,Point const&,Point const&){return clear;}
|
||||
@@ -54,6 +54,9 @@ int main(){
|
||||
p.Relocate(110,20,30);r.Moving=true;assert(mgr.AdvanceFollowing(&p,r));assert(!r.Moving && p.stops==2); // holds in air, never dismounts at moving goal
|
||||
assert(mgr.LandFollowing(&p));assert(!r.Status.FollowingPlayer && r.Status.Landing.z==0 && r.Status.Travelled==0);
|
||||
assert(!mgr.Follow(&p,target,error)); // landing cannot be replaced by a new pursuit
|
||||
r.Status.FollowingPlayer=false;r.Following=std::make_shared<SharedTrail>();
|
||||
assert(mgr.LandFollowing(&p));assert(r.Returning&&!r.Following&&r.Start.z==0&&r.Breadcrumbs.empty());
|
||||
r.Returning=false;
|
||||
r.Status.FollowingPlayer=true;r.Status.State=ClientlessFlightState::Mounting;assert(mgr.LandFollowing(&p) && mgr.failures==1);
|
||||
r.Status.State=ClientlessFlightState::Flying;r.Status.FollowingPlayer=true;valid=false;assert(mgr.LandFollowing(&p) && mgr.failures==2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Run the production group-wide instance leave protection predicate."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/"src/server/game/World/AdventureBehaviorMgr.cpp").read_text()
|
||||
a=s.index("bool AdventureBehaviorMgr::ProtectInstanceGroupLeave(")
|
||||
b=s.index("void AdventureBehaviorMgr::InterruptForInteraction",a)
|
||||
code=r'''
|
||||
#include "AdventureInstancePolicy.h"
|
||||
#include <cassert>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
using uint32=unsigned;
|
||||
namespace GameTime{time_t GetGameTime(){return 1000;}}
|
||||
struct Group;
|
||||
struct Map{bool dungeon=false;bool IsDungeon()const{return dungeon;}};
|
||||
struct Guid{unsigned id;unsigned GetCounter()const{return id;}};
|
||||
struct Player{unsigned id;Group* group=nullptr;Map map;bool online=true;bool IsInWorld()const{return online;}Group* GetGroup(){return group;}Map* GetMap(){assert(online);return ↦}Guid GetGUID(){return {id};}};
|
||||
struct Reference{Player* player=nullptr;Reference* following=nullptr;Player* GetSource(){return player;}Reference* next(){return following;}};
|
||||
struct Group{Reference* first=nullptr;Reference* GetFirstMember(){return first;}};
|
||||
struct Location{unsigned InstanceMap=671;} location;
|
||||
struct Locations{Location const* Find(std::string const& key){return key=="instance"?&location:nullptr;}} locations;auto sAdventureLocationMgr=&locations;
|
||||
struct AdventureBehaviorMgr{struct Record{AdventureInstancePolicy::Visit Instance;std::string Intent,Location;time_t Deadline=0;};std::unordered_map<unsigned,Record> _records;bool ProtectInstanceGroupLeave(Player*)const;};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){AdventureBehaviorMgr m;Player late{1},leader{2};assert(!m.ProtectInstanceGroupLeave(&late));
|
||||
Reference second{&leader,nullptr},first{&late,&second};Group group{&first};late.group=leader.group=&group;
|
||||
assert(!m.ProtectInstanceGroupLeave(&late));leader.map.dungeon=true;
|
||||
assert(m.ProtectInstanceGroupLeave(&late)); // human leader inside, no behavior record
|
||||
leader.online=false;assert(!m.ProtectInstanceGroupLeave(&late));leader.online=true;leader.map.dungeon=false;
|
||||
auto& r=m._records[2];r.Instance.Active=true;
|
||||
for(auto phase:{AdventureInstancePolicy::Phase::Travel,AdventureInstancePolicy::Phase::Preparing,AdventureInstancePolicy::Phase::Entering,AdventureInstancePolicy::Phase::Inside,AdventureInstancePolicy::Phase::Returning}){
|
||||
r.Instance.Stage=phase;assert(m.ProtectInstanceGroupLeave(&late));}
|
||||
r.Instance.Stage=AdventureInstancePolicy::Phase::Rest;r.Intent="raid";r.Location="instance";r.Deadline=2000;
|
||||
assert(!m.ProtectInstanceGroupLeave(&late)); // finished visit must not retain protection
|
||||
r.Instance.Active=false;assert(m.ProtectInstanceGroupLeave(&late)); // pending retry/en-route
|
||||
r.Intent="instance";assert(m.ProtectInstanceGroupLeave(&late));
|
||||
r.Deadline=1000;assert(!m.ProtectInstanceGroupLeave(&late));
|
||||
r.Deadline=2000;r.Location="unknown";assert(!m.ProtectInstanceGroupLeave(&late));
|
||||
r.Location="instance";r.Intent="profession";assert(!m.ProtectInstanceGroupLeave(&late));
|
||||
first.following=nullptr;assert(!m.ProtectInstanceGroupLeave(&late));}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-instance-group-protection-") as d:
|
||||
p=Path(d);(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)
|
||||
print("Instance group protection: travel/preparing/entering/inside/return, human leader, offline, retry, expiry and finished visit PASS")
|
||||
@@ -68,6 +68,8 @@ struct Player {
|
||||
bool IsInPhase(GameObject*){return phase;}bool CanSeeOrDetect(GameObject*){return visible;}
|
||||
GameObject* GetGameObjectIfCanInteractWith(int guid){return inRange&&guid==go.guid?&go:nullptr;}
|
||||
float GetPositionX(){return x;}float GetPositionY(){return y;}float GetPositionZ(){return z;}
|
||||
unsigned getLevel(){return 85;}
|
||||
bool TeleportTo(uint32 map,float tx,float ty,float tz,float){pending=true;world=false;mapId=map;x=tx;y=ty;z=tz;return true;}
|
||||
std::string GetName(){return "Arthoran";}void StopMoving(){}
|
||||
};
|
||||
void Session::HandleGameObjectUseOpcode(WorldPacket& packet) {
|
||||
@@ -80,8 +82,11 @@ void Session::CompleteClientlessTeleport(){
|
||||
player->mapId=wrongTarget?1:0;player->x=-8998;player->y=861;player->z=29;
|
||||
}
|
||||
struct Node {std::string Key;uint32 MapId=530;};
|
||||
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;};
|
||||
struct Graph {bool prerequisites=true;bool PortalPrerequisites(Player*,uint32){return prerequisites;}bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
|
||||
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;struct{uint32 MapId=0;float X=-8998,Y=861,Z=29,O=0;}Target;};
|
||||
struct Graph {bool nativePrerequisites=true,adventureUnlock=false;
|
||||
bool PortalNativePrerequisites(Player*,uint32){return nativePrerequisites;}
|
||||
bool AdventurePortalUnlockAllowed(Player*,uint32){return adventureUnlock;}
|
||||
bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
|
||||
struct Travel {void CancelTravel(uint32){}} travel;
|
||||
struct Lua {void DismountGroundMount(Player*,const char*){}} lua;
|
||||
auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua;
|
||||
@@ -105,7 +110,11 @@ harness += methods
|
||||
harness += r'''
|
||||
int main(){
|
||||
ClientlessActionExecutorMgr mgr;
|
||||
{Player p;Record r;graph.prerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.prerequisites=true;}
|
||||
{Player p;Record r;graph.nativePrerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.nativePrerequisites=true;}
|
||||
{Player p;Record r;graph.nativePrerequisites=false;graph.adventureUnlock=true;
|
||||
assert(mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0&&p.pending);
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.sequence==1);
|
||||
graph.nativePrerequisites=true;graph.adventureUnlock=false;}
|
||||
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world);
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);}
|
||||
// The global lifecycle may have ACKed a delayed cast before our next update.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Run the production local-flight adapter; preserve macro route and native checks."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
src = (root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
a = src.index('bool ClientlessActionExecutorMgr::TryRouteApproachFlight(')
|
||||
b = src.index('bool ClientlessActionExecutorMgr::RecoverTravel(', a)
|
||||
method = src[a:b]
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
using uint32=unsigned;
|
||||
struct Position {float x=0,y=0,z=0;void Relocate(float a,float b,float c,float){x=a;y=b;z=c;}};
|
||||
struct Player {unsigned GetMapId(){return 0;}float GetExactDist(float x,float y,float z){return std::sqrt(x*x+y*y+z*z);}};
|
||||
struct Step {std::string NodeKey="portal",EdgeType="start";unsigned MapId=0,EdgeDatabaseId=0;float X=100,Y=0,Z=10,O=0;};
|
||||
enum class ClientlessActionExecutionState{Moving,Failed};
|
||||
struct Record {std::vector<Step> Route{Step{},Step{}};unsigned RouteStep=0;std::set<std::string> FailedBridgeKeys;bool DeferredStartActive=true,FlightActive=false,Active=false;std::vector<int> EdgePath{1,2};struct {ClientlessActionExecutionState State=ClientlessActionExecutionState::Failed;std::string Detail;}Status;};
|
||||
struct Flight {bool available=true;unsigned calls=0;Position last;bool Start(Player*,Position const&p,std::string&error,bool owned){assert(owned);++calls;last=p;if(!available)error="no safe landing";return available;}}flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
struct ClientlessActionExecutorMgr {bool TryRouteApproachFlight(Player*,uint32,Record&);};
|
||||
'''
|
||||
harness += method + r'''
|
||||
int main(){
|
||||
Player p;Record r;ClientlessActionExecutorMgr mgr;
|
||||
// Fly to the rejected connector, preserving the following portal edge.
|
||||
assert(mgr.TryRouteApproachFlight(&p,163,r));
|
||||
assert(r.FlightActive&&r.Active&&!r.DeferredStartActive&&r.EdgePath.empty());
|
||||
assert(r.RouteStep==0&&r.Route.size()==2&&r.Route[0].EdgeType=="start");
|
||||
assert(flight.last.x==100&&flight.last.z==10&&flight.calls==1);
|
||||
assert(!mgr.TryRouteApproachFlight(&p,163,r)&&flight.calls==1);
|
||||
// No flight permission/solid landing/ground connection means no fallback.
|
||||
r=Record{};flight.available=false;
|
||||
assert(!mgr.TryRouteApproachFlight(&p,163,r)&&!r.FlightActive&&r.Route.size()==2);
|
||||
r=Record{};flight.available=true;r.Route[0].MapId=1;
|
||||
assert(!mgr.TryRouteApproachFlight(&p,163,r));
|
||||
r=Record{};r.Route[0].EdgeDatabaseId=42;
|
||||
assert(!mgr.TryRouteApproachFlight(&p,163,r));
|
||||
for(auto type:{"portal_use","taxi","free_flight","ship"}){
|
||||
r=Record{};r.Route[0].EdgeType=type;assert(!mgr.TryRouteApproachFlight(&p,163,r));
|
||||
}
|
||||
r=Record{};r.Route[0].EdgeType="local_bridge";assert(mgr.TryRouteApproachFlight(&p,163,r));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-route-flight-') as folder:
|
||||
cpp=Path(folder)/'test.cpp'; exe=Path(folder)/'test'
|
||||
cpp.write_text(harness)
|
||||
subprocess.run(['c++','-std=c++17',str(cpp),'-o',str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
print('Route approach flight: original route retained, one attempt, native refusal and transport boundaries passed.')
|
||||
@@ -0,0 +1,88 @@
|
||||
"""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]
|
||||
a=source.index('void ClientlessFlightMgr::Store(')
|
||||
b=source.index('bool ClientlessFlightMgr::Begin(',a)
|
||||
store=source[a:b]
|
||||
code=r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
using uint32=unsigned;
|
||||
using namespace ClientlessFlightPath;
|
||||
struct Position{float x=0,y=0,z=0;void Relocate(float a,float b,float c){x=a;y=b;z=c;}float GetExactDist(Position const&p)const{return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);}};
|
||||
struct Record{std::shared_ptr<SharedTrail> Following;std::shared_ptr<SharedTrail> Published=std::make_shared<SharedTrail>();bool Returning=false;unsigned SharedWaitSince=0,Elapsed=0;std::size_t SharedIndex=0;SharedJoin Join;struct{unsigned MapId=0;Position Landing,Target;std::string Detail;}Status;};
|
||||
int landings=0;void* player=nullptr;void LandFollowing(void*){++landings;}
|
||||
bool Sync(Record& record){
|
||||
''' + sync + r'''
|
||||
return false;
|
||||
}
|
||||
struct ClientlessFlightMgr{std::unordered_map<unsigned,Record> _records;void Store(unsigned,Record&&);};
|
||||
''' + store + r'''
|
||||
int main(){
|
||||
ClientlessFlightMgr manager;Record original;original.Status.Target.x=100;
|
||||
auto previous=original.Published;manager.Store(1,std::move(original));
|
||||
Record updated;updated.Status.Target.x=100;updated.Published->LandingReady=true;
|
||||
auto successor=updated.Published;manager.Store(1,std::move(updated));
|
||||
assert(previous->Aborted&&previous->Replacement==successor);
|
||||
Record other;other.Status.Target.x=200;manager.Store(1,std::move(other));
|
||||
assert(successor->Aborted&&!successor->Replacement);
|
||||
|
||||
Record r;r.Following=std::make_shared<SharedTrail>();r.Status.Landing.z=777;
|
||||
assert(Sync(r)&&r.Status.Landing.z==777); // still preparing; no zero landing copied
|
||||
r.Following->Landing={12,34,827.765625f};r.Following->LandingReady=true;
|
||||
assert(!Sync(r)&&r.Status.Landing.x==12&&r.Status.Landing.y==34&&r.Status.Landing.z==827.765625f);
|
||||
assert(r.Published->LandingReady&&r.Published->Landing.Z==827.765625f);
|
||||
|
||||
auto replacement=r.Following->Replace({56,78,100});
|
||||
assert(!Sync(r)&&r.Following==replacement&&r.Status.Landing.z==100);
|
||||
replacement->Complete=true;assert(!Sync(r)); // completed valid trail remains usable
|
||||
Record waiting;waiting.Following=std::make_shared<SharedTrail>();
|
||||
assert(Sync(waiting));waiting.Elapsed=120000;assert(Sync(waiting)&&landings==1);
|
||||
waiting.Following->Aborted=true;assert(Sync(waiting)&&landings==2);
|
||||
Settings cfg;auto sample=[](float,float){return Surface{827.765625f,1000,true,0};};
|
||||
auto clear=[](Point const&,Point const&){return true;};
|
||||
// The same shared object first exists while its leader is walking to takeoff.
|
||||
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)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Join a leader trail from a later taxi stop instead of replaying its origin."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
code=r'''
|
||||
#include "ClientlessFlightPath.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
using namespace ClientlessFlightPath;
|
||||
int main(){Settings cfg;auto flat=[](float,float){return Surface{0,1000,true};};auto clear=[](Point const&,Point const&){return true;};
|
||||
SharedTrail trail;trail.LandingReady=true;trail.Complete=true;trail.Landing={1200,0,0};
|
||||
trail.Points={{0,0,20},{1025,0,60},{1050,0,60},{1100,0,60},{1200,0,0}};
|
||||
SharedJoin join;std::size_t index=0;Point current{1000,0,0};bool connected=false;
|
||||
for(unsigned i=0;i<100&&Distance(current,trail.Landing)>.1f;++i){
|
||||
auto result=FollowTrail(current,trail.Landing,cfg,trail,index,flat,clear,0,&join);
|
||||
assert(!result.Waiting&&result.Step.Valid);assert(result.Step.Next.X>=1000);
|
||||
assert(Corridor(current,result.Step.Next,trail.Landing,cfg,flat,clear,true,result.Step.UnderOverhang));
|
||||
current=result.Step.Next;if(!join.Active)connected=true;
|
||||
}
|
||||
assert(connected&&Distance(current,trail.Landing)<.1f&&index>=1);
|
||||
// Original Winienwi geometry: the old first point is 2,088 yards behind
|
||||
// the taxi stop and its direct segment intersects a raised surface.
|
||||
Point taxi{-4170.55f,-4748.93f,126.786f};SharedTrail actual;
|
||||
actual.LandingReady=true;actual.Landing={-4905.67f,-4232.75f,827.766f};
|
||||
actual.Points={{-4863.52f,-6715.61f,24.22f},{-4850,-4750,540},actual.Landing};
|
||||
auto hills=[](float x,float y){float ground=y< -6500?9.f:126.786f;
|
||||
if(std::fabs(x+4175.523f)<2.5f&&std::fabs(y+4763.045f)<2.5f)ground=129.25f;
|
||||
return Surface{ground,1000,true};};
|
||||
assert(!Corridor(taxi,actual.Points[0],actual.Landing,cfg,hills,clear,true));
|
||||
join={};index=0;auto connectedTaxi=FollowTrail(taxi,actual.Landing,cfg,actual,index,hills,clear,0,&join);
|
||||
assert(connectedTaxi.Step.Valid&&join.Target==1&&connectedTaxi.Step.Next.Z>taxi.Z);
|
||||
// Joining an airborne leg above a tower must still go around its platform.
|
||||
SharedTrail tower;tower.LandingReady=true;tower.Landing={12,0,200};
|
||||
tower.Points={{60,0,210},{12,0,210},tower.Landing};
|
||||
auto platform=[](float x,float y){return Surface{std::hypot(x,y)<20?200.f:0.f,500,true,0.f};};
|
||||
auto solid=[](Point const&a,Point const&b){for(unsigned i=0;i<=200;++i){auto p=Mix(a,b,i/200.f);if(std::hypot(p.X,p.Y)<20&&p.Z>=199.8f&&p.Z<=200.2f)return false;}return true;};
|
||||
current={90,0,80};join={};index=0;
|
||||
for(unsigned i=0;i<100&&Distance(current,tower.Landing)>.1f;++i){
|
||||
auto step=FollowTrail(current,tower.Landing,cfg,tower,index,platform,solid,0,&join);
|
||||
assert(step.Step.Valid&&!step.Waiting);current=step.Step.Next;
|
||||
}
|
||||
assert(Distance(current,tower.Landing)<.1f);
|
||||
// No unvalidated shortcut through a tower or missing terrain.
|
||||
join={};index=0;auto blocked=[](Point const&,Point const&){return false;};
|
||||
assert(!FollowTrail({1000,0,0},trail.Landing,cfg,trail,index,flat,blocked,0,&join).Step.Valid);
|
||||
auto missing=[](float,float){return Surface{};};join={};index=0;
|
||||
assert(!FollowTrail({1000,0,0},trail.Landing,cfg,trail,index,missing,clear,0,&join).Step.Valid);
|
||||
// A replacement publishes its verified destination immediately, even before
|
||||
// any points are flown. Old readers can resolve to this generation.
|
||||
auto old=std::make_shared<SharedTrail>();auto next=old->Replace({100,0,0});
|
||||
assert(old->Aborted&&old->Replacement==next&&next->LandingReady&&next->Landing.X==100);
|
||||
assert(next->Points.empty());next->Points={{25,0,20},{100,0,0}};next->Complete=true;
|
||||
join={};index=0;auto step=FollowTrail({0,0,0},next->Landing,cfg,*next,index,flat,clear,0,&join);
|
||||
assert(step.Step.Valid&&!step.Waiting);
|
||||
std::cout<<"Shared route rejoin after taxi, bounded climb, clear corridor, blocked/missing terrain, replacement publication PASS\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-shared-rejoin-") as directory:
|
||||
p=Path(directory);(p/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17","-I"+str(root/"src/server/game/World"),str(p/"test.cpp"),"-o",str(p/"test")],check=True)
|
||||
subprocess.run([str(p/"test")],check=True)
|
||||
@@ -5,6 +5,7 @@ root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(')
|
||||
code=r'''
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -20,9 +21,9 @@ struct Group{Guid leader{29};Guid GetLeaderGUID(){return leader;}}group;
|
||||
struct Player{bool grouped=true;unsigned map=0;Guid GetGUID(){return {31};}Group*GetGroup(){return grouped?&group:nullptr;}unsigned GetMapId(){return map;}float GetExactDist(Position const&p){return std::hypot(p.x,p.y);}};
|
||||
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
|
||||
struct ClientlessTravelRouteStep{std::string NodeKey,EdgeType;unsigned EdgeDatabaseId=0,MapId=0;float X=0,Y=0,Z=0;};
|
||||
struct Graph{bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector<ClientlessTravelRouteStep>&r,bool&complete,std::string&,std::string&){complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
struct Graph{ClientlessTravelCost::Profile observed;bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector<ClientlessTravelRouteStep>&r,bool&complete,std::string&,std::string&,void const* =nullptr,void const* =nullptr,void const* =nullptr,ClientlessTravelCost::Profile const*costs=nullptr){if(costs)observed=*costs;complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
struct ClientlessActionExecutorMgr{
|
||||
struct Record{std::vector<ClientlessTravelRouteStep>Route;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false;
|
||||
struct Record{ClientlessTravelCost::Profile JourneyCosts;std::string JourneyCostKey;bool HasJourneyCosts=false;std::vector<ClientlessTravelRouteStep>Route;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false;
|
||||
unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode;};
|
||||
std::unordered_map<unsigned,Record>_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;}
|
||||
bool AdoptRoute(Player*,ClientlessAction const&,Record&);
|
||||
@@ -31,14 +32,16 @@ struct ClientlessActionExecutorMgr{
|
||||
int main(){
|
||||
ClientlessActionExecutorMgr mgr;Player p;ClientlessAction a;a.Name="travel_to_location";a.Parameters={{"id","destination"},{"route_source","29"}};
|
||||
auto& lead=mgr._records[29];lead.Route={{"start","start",0,0,0,0,0},{"taxi_end","taxi",18,0,30,0,0},{"flight_end","free_flight",0,0,80,0,0}};
|
||||
lead.JourneyCosts=ClientlessTravelCost::Sample({},12345,0.2f);lead.JourneyCostKey="leader-trip";lead.HasJourneyCosts=true;
|
||||
lead.RouteStep=1;lead.FinalLocationId="destination";lead.FinalDatabaseId=77;lead.FinalX=90;lead.FinalLocalLegPending=true;
|
||||
ClientlessActionExecutorMgr::Record peer;
|
||||
assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==3&&peer.Route[1].EdgeType=="taxi"&&peer.Route[1].EdgeDatabaseId==18&&peer.Route[2].EdgeType=="free_flight");
|
||||
assert(peer.FinalLocationId=="destination"&&peer.FinalLocalLegPending&&peer.FinalX==90&&peer.FinalDatabaseId==77);
|
||||
assert(peer.HasJourneyCosts&&peer.JourneyCosts.Seed==12345&&peer.JourneyCostKey=="leader-trip"&&peer.JourneyCosts.Factors==lead.JourneyCosts.Factors);
|
||||
assert(peer.RouteStep==0&&peer.Active&&lead.RouteStep==1);peer.Route[1].NodeKey="changed";assert(lead.Route[1].NodeKey=="taxi_end");
|
||||
// Late arrival joins the active route at its previous reached node.
|
||||
lead.RouteStep=2;assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==2&&peer.Route[0].NodeKey=="taxi_end"&&peer.Route[0].EdgeType=="start"&&peer.Route[1].EdgeType=="free_flight");
|
||||
p.map=1;assert(mgr.AdoptRoute(&p,a,peer));graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0;
|
||||
p.map=1;assert(mgr.AdoptRoute(&p,a,peer));assert(graph.observed.Factors==lead.JourneyCosts.Factors&&graph.observed.Seed==12345);graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0;
|
||||
// No cross-group route copying and no stale destination reuse.
|
||||
group.leader={32};assert(!mgr.AdoptRoute(&p,a,peer));group.leader={29};p.grouped=false;assert(!mgr.AdoptRoute(&p,a,peer));p.grouped=true;
|
||||
a.Parameters["id"]="different";assert(!mgr.AdoptRoute(&p,a,peer));a.Parameters["id"]="destination";
|
||||
@@ -47,6 +50,6 @@ int main(){
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'route.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17',str(p/'route.cpp'),'-o',str(p/'route')],check=True)
|
||||
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'route.cpp'),'-o',str(p/'route')],check=True)
|
||||
subprocess.run([str(p/'route')],check=True)
|
||||
print('Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.')
|
||||
@@ -60,6 +60,7 @@ struct Position {
|
||||
float GetExactDist(Position const*p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));}
|
||||
};
|
||||
constexpr int MOVE_RUN=0,SPELL_AURA_MOUNTED=1;
|
||||
constexpr int GAMEOBJECT_TYPE_TRANSPORT=11;
|
||||
struct TransportDockState {uint32 Progress=0,Period=0,RemainingPause=0;bool Stopped=false;};
|
||||
class Player; class GameObject;
|
||||
class TransportBase {
|
||||
@@ -73,7 +74,8 @@ world = r'''
|
||||
};
|
||||
class GameObject:public Position {
|
||||
public:
|
||||
TransportBase base;uint32 map=0,entry=1,spawn=1;TransportDockState state{0,30000,10000,true};
|
||||
TransportBase base;uint32 map=0,entry=1,spawn=1;int type=15;TransportDockState state{0,30000,10000,true};
|
||||
int GetGoType()const{return type;}
|
||||
GameObject(){base.go=this;}
|
||||
bool IsInWorld()const{return true;}uint32 GetMapId()const{return map;}
|
||||
ObjectGuid GetGUID()const{return {spawn};}
|
||||
@@ -87,9 +89,9 @@ class Player:public Position {
|
||||
public:
|
||||
TransportBase* transport=nullptr;uint32 map=0;GameObject* object=nullptr;Motion motion{this};bool pending=false;
|
||||
struct {struct {Position pos;ObjectGuid guid;uint32 time=0;void Reset(){pos={};guid={};time=0;}}transport;}m_movementInfo;
|
||||
int teleports=0;bool falling=false;
|
||||
int teleports=0;bool falling=false,teleportAllowed=true;
|
||||
bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){}
|
||||
bool TeleportTo(uint32 m,float a,float b,float c,float d){++teleports;map=m;Relocate(a,b,c,d);falling=false;return true;}
|
||||
bool TeleportTo(uint32 m,float a,float b,float c,float d){if(!teleportAllowed)return false;++teleports;if(transport)transport->RemovePassenger(this);map=m;Relocate(a,b,c,d);falling=false;return true;}
|
||||
bool IsInCombat(){return false;}bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;}
|
||||
bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;}
|
||||
bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;}
|
||||
@@ -135,7 +137,8 @@ methods = '\n'.join(function(source, marker) for marker in [
|
||||
'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(',
|
||||
'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(',
|
||||
'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(',
|
||||
'void ClientlessTransportMgr::RecoverPassenger(', 'void ClientlessTransportMgr::UpdateRide('
|
||||
'void ClientlessTransportMgr::RecoverPassenger(', 'bool ClientlessTransportMgr::RecoverStuckElevator(',
|
||||
'void ClientlessTransportMgr::UpdateRide('
|
||||
])
|
||||
stubs = r'''
|
||||
void PublishPassengerState(Player*){}
|
||||
@@ -295,7 +298,56 @@ int main(){
|
||||
resumed.UpdateRide(&fallen,ride,100);
|
||||
assert(ride.Status.Recovering&&fallen.teleports==1&&fallen.x==0);
|
||||
}
|
||||
std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n";
|
||||
// Elevators need 500ms to begin boarding; exiting needs only the stopped dock.
|
||||
for(bool reverse:{false,true}){
|
||||
ClientlessTransportMgr lift;lift._stops["A"]=stop("A",5,0);lift._stops["B"]=stop("B",100,104);
|
||||
Player rider;GameObject platform;platform.type=11;rider.object=&platform;
|
||||
auto from=reverse?"B":"A",to=reverse?"A":"B";
|
||||
platform.x=reverse?100:5;rider.x=reverse?104:0;platform.state.RemainingPause=499;platform.state.Stopped=false;
|
||||
assert(lift.Start(&rider,from,to,error));auto& ride=lift._rides.at(26);
|
||||
run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport);
|
||||
platform.state.Stopped=true;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Waiting&&!rider.transport);
|
||||
platform.state.RemainingPause=500;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
||||
platform.x=50;rider.x=51;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
||||
platform.x=reverse?5:100;rider.x=platform.x+1;platform.state.Stopped=false;
|
||||
run(lift,rider,ride);assert(ride.Status.Phase==Phase::Riding&&rider.transport);
|
||||
platform.state.Stopped=true;platform.state.RemainingPause=1;run(lift,rider,ride);assert(ride.Status.Phase==Phase::Arrived&&!rider.transport&&rider.x==(reverse?0:104));
|
||||
// Actual passenger time, not waiting on land, gates the five-minute rescue.
|
||||
rider.x=reverse?104:0;platform.x=reverse?100:5;platform.state.RemainingPause=500;assert(lift.Start(&rider,from,to,error));run(lift,rider,lift._rides.at(26));
|
||||
auto& stuck=lift._rides.at(26);platform.x=50;rider.x=51;platform.state.Stopped=false;
|
||||
stuck.ElevatorPassengerMs=0;stuck.Elapsed=0;lift.UpdateRide(&rider,stuck,299999);
|
||||
assert(rider.teleports==0&&rider.transport&&stuck.ElevatorDestination==to&&!stuck.Abort);
|
||||
lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==1&&!rider.transport&&rider.x==(reverse?0:104));
|
||||
lift.UpdateRide(&rider,stuck,1);assert(stuck.Status.Phase==Phase::Arrived);
|
||||
lift.UpdateRide(&rider,stuck,300000);assert(rider.teleports==1);
|
||||
// A cancelled/retried exit cannot change the original recovery destination.
|
||||
stuck.Status.Phase=Phase::Riding;stuck.ElevatorFallback=false;stuck.Abort=true;
|
||||
stuck.To=stuck.From;stuck.RetryDelay=3000;platform.base.AddPassenger(&rider);
|
||||
lift.UpdateRide(&rider,stuck,1);assert(rider.teleports==2&&rider.x==(reverse?0:104));
|
||||
}
|
||||
// A ship, a missing/foreign attachment or a forbidden endpoint cannot use elevator rescue.
|
||||
ClientlessTransportMgr rescue;rescue._stops["A"]=stop("A",5,0);rescue._stops["B"]=stop("B",100,104);
|
||||
Player passenger;GameObject vehicle;passenger.object=&vehicle;vehicle.base.AddPassenger(&passenger);
|
||||
ClientlessTransportMgr::Ride stranded;stranded.From=rescue._stops["A"];stranded.To=rescue._stops["B"];
|
||||
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports);
|
||||
vehicle.type=11;vehicle.base.RemovePassenger(&passenger);
|
||||
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!stranded.ElevatorPassengerMs);
|
||||
vehicle.base.AddPassenger(&passenger);areas.denied="B";
|
||||
assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,300000)&&!passenger.teleports);areas.denied.clear();
|
||||
passenger.teleportAllowed=false;assert(!rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1)&&!stranded.ElevatorFallback);
|
||||
passenger.teleportAllowed=true;assert(rescue.RecoverStuckElevator(&passenger,&vehicle,stranded,1));
|
||||
// Departure during an elevator transition retains the live journey, whereas
|
||||
// walking the final land segment no longer depends on the departed platform.
|
||||
ClientlessTransportMgr quick;quick._stops["A"]=stop("A",5,0);quick._stops["B"]=stop("B",100,104);
|
||||
Player commuter;GameObject cabin;cabin.type=11;cabin.state.Stopped=false;commuter.object=&cabin;cabin.base.AddPassenger(&commuter);
|
||||
ClientlessTransportMgr::Ride interrupted;interrupted.From=quick._stops["A"];interrupted.To=quick._stops["B"];
|
||||
interrupted.Status.Phase=Phase::Boarding;interrupted.Point=2;
|
||||
quick.UpdateRide(&commuter,interrupted,100);
|
||||
assert(interrupted.Status.Phase==Phase::Riding&&!interrupted.Abort&&commuter.transport);
|
||||
cabin.base.RemovePassenger(&commuter);commuter.x=103;
|
||||
interrupted.Status.Phase=Phase::Leaving;interrupted.Point=1;interrupted.Moving=false;
|
||||
run(quick,commuter,interrupted);assert(interrupted.Status.Phase==Phase::Arrived&&commuter.x==104);
|
||||
std::cout<<"Transport dock, elevator pause/five-minute rescue, frame, recorder, cross-map copy and ride regressions passed\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-transport-test-') as directory:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Production native transport arclength, directed wraparound and static animation distance."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessTransportMgr.cpp').read_text()
|
||||
a=s.index('float ClientlessTransportMgr::ConnectionDistance(');b=s.index('bool ClientlessTransportMgr::ObserveJourney(',a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
using uint32=unsigned;
|
||||
struct Position{float x=0,y=0,z=0;Position(){}Position(float a,float b,float c):x(a),y(b),z(c){}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetExactDist(Position const&p)const{return std::sqrt((x-p.x)*(x-p.x)+(y-p.y)*(y-p.y)+(z-p.z)*(z-p.z));}};
|
||||
namespace G3D{struct Matrix{};struct Vector3{float x=0,y=0,z=0;Vector3(){}Vector3(float a,float b,float c):x(a),y(b),z(c){}Vector3 operator*(Matrix)const{return *this;}};struct Quat{Quat(float,float,float,float){}Matrix toRotationMatrix(){return {};}};}
|
||||
struct QuaternionData{float x=0,y=0,z=0,w=1;};
|
||||
struct Spline{double Length;float X,Span;double length()const{return Length;}void evaluate_percent(float t,G3D::Vector3&p)const{p={X+t*Span,0,0};}};
|
||||
struct Segment{double DistanceFromLegStartAtEnd;};
|
||||
struct Leg{unsigned MapId;std::shared_ptr<Spline>Spline;std::vector<Segment>Segments;};
|
||||
struct Native{std::vector<Leg>PathLegs;};
|
||||
struct AnimPoint{struct{float X,Y,Z;}Pos;};struct Animation{std::map<unsigned,AnimPoint const*>Path;};
|
||||
struct TransportManager{Native native;Animation animation;bool useNative=true;Native*GetTransportTemplate(unsigned){return useNative?&native:nullptr;}Animation*GetTransportAnimInfo(unsigned){return &animation;}}tm;auto sTransportMgr=&tm;
|
||||
struct Spawn{Position spawnPoint{100,0,0};}spawn;
|
||||
struct Addon{QuaternionData ParentRotation;}addon;
|
||||
struct Objects{Spawn*GetGameObjectData(unsigned){return &spawn;}Addon*GetGameObjectAddon(unsigned){return &addon;}}objects;auto sObjectMgr=&objects;
|
||||
struct ClientlessTransportStop{unsigned Entry=1,Spawn=5,Map=0;Position Dock;};
|
||||
struct ClientlessTransportMgr{std::unordered_map<std::string,ClientlessTransportStop>stops;auto GetStop(std::string const&n){return stops.count(n)?&stops[n]:nullptr;}float ConnectionDistance(std::string const&,std::string const&);};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
ClientlessTransportMgr m;m.stops["from"]={1,5,0,{10,0,0}};m.stops["to"]={1,5,1,{1000005,0,0}};
|
||||
tm.native.PathLegs={{0,std::make_shared<Spline>(Spline{100,0,10}),{{100}}},{1,std::make_shared<Spline>(Spline{300,1000000,20}),{{75},{300}}}};
|
||||
assert(m.ConnectionDistance("from","to")==75);assert(m.ConnectionDistance("to","from")==325);
|
||||
// Geometric jumps between maps contribute zero, native curved-leg length is retained.
|
||||
assert(m.ConnectionDistance("missing","to")==0);
|
||||
tm.useNative=false;AnimPoint p0{{0,0,0}},p1{{3,0,0}},p2{{10,0,0}};
|
||||
tm.animation.Path={{0,&p0},{1,&p1},{2,&p2},{3,&p0}};
|
||||
m.stops["from"].Map=m.stops["to"].Map=0;m.stops["from"].Dock={103,0,0};m.stops["to"].Dock={110,0,0};
|
||||
assert(m.ConnectionDistance("from","to")==7);assert(m.ConnectionDistance("to","from")==13);
|
||||
}
|
||||
'''
|
||||
# Avoid a type/member identifier collision in this small native-shaped fixture.
|
||||
code=code.replace('struct Spline{','struct NativeSpline{').replace('std::shared_ptr<Spline>Spline','std::shared_ptr<NativeSpline>Spline').replace('std::make_shared<Spline>(Spline{','std::make_shared<NativeSpline>(NativeSpline{')
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-transport-costs-') as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production native transport distances: spline arclength, cross-map discontinuities, directed cycle and static animation stops passed.')
|
||||
@@ -9,6 +9,7 @@ code=r'''
|
||||
using uint32=unsigned;using uint64=unsigned long long;
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
constexpr int SMSG_MOVE_UPDATE=1;
|
||||
constexpr int GAMEOBJECT_TYPE_TRANSPORT=11;
|
||||
struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}void Relocate(Position const&p){*this=p;}};
|
||||
struct Guid{unsigned GetCounter(){return 120;}};
|
||||
struct WorldPacket{explicit WorldPacket(int){}};
|
||||
@@ -29,7 +30,7 @@ struct Player:Position{bool alive=true,combat=false,teleport=false;int*world=&ma
|
||||
};
|
||||
void Transport::UpdatePassengerPosition(int*,Player*p,float x,float y,float z,float o,bool){++relocations;p->x=x;p->y=y;p->z=z;p->o=o;}
|
||||
struct TransportDockState{bool Stopped=true;unsigned RemainingPause=10000;};
|
||||
struct GameObject{TransportDockState dock;bool valid=true;Transport*ToTransportBase(){return valid?&transport:nullptr;}int*GetMap(){return ↦}bool GetTransportDockState(TransportDockState&d){d=dock;return true;}unsigned GetEntry(){return 176081;}}object;
|
||||
struct GameObject{TransportDockState dock;bool valid=true;int type=15;int GetGoType(){return type;}Transport*ToTransportBase(){return valid?&transport:nullptr;}int*GetMap(){return ↦}bool GetTransportDockState(TransportDockState&d){d=dock;return true;}unsigned GetEntry(){return 176081;}}object;
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
Player p;Position anchor{2,-3,-10,0};
|
||||
@@ -45,6 +46,11 @@ int main(){
|
||||
p.attached=nullptr;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));p.attached=&transport;
|
||||
anchor.x=101;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));anchor.x=NAN;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));
|
||||
assert(transport.relocations==1&&p.updates==1); // rejected operations never move or detach
|
||||
// Elevators need a real stop, but no time reserve even for stand recovery.
|
||||
anchor.x=2;object.type=11;object.dock.RemainingPause=1;
|
||||
assert(ReturnToRecordedStand(&p,&object,anchor,5000));
|
||||
object.dock.Stopped=false;assert(!ReturnToRecordedStand(&p,&object,anchor,5000));
|
||||
assert(transport.relocations==2&&p.updates==2);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory()as d:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Exercise production executor journey initialization, retry stability and leader inheritance."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
a=s.index('bool ClientlessActionExecutorMgr::StartAction(')
|
||||
b=s.index(' record.StopAtNode = false;',a)
|
||||
method=s[a:b]+' return true;\n}\n'
|
||||
code=r'''
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <unordered_map>
|
||||
using uint32=unsigned;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
struct Guid{unsigned id;unsigned GetCounter()const{return id;}};
|
||||
struct Group{unsigned leader=29;Guid GetLeaderGUID(){return {leader};}}group;
|
||||
struct Player{unsigned id=29;Guid GetGUID(){return {id};}Group*GetGroup(){return &group;}};
|
||||
struct ClientlessAction{std::string Name="travel_to_node";std::unordered_map<std::string,std::string>Parameters{{"node","destination"}};};
|
||||
enum class ClientlessActionExecutionState{Idle,Completed,Failed,SelectingTarget};
|
||||
struct Graph{unsigned draws=0;auto NewJourneyCosts(){return ClientlessTravelCost::Sample({},++draws,.2f);}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
struct ClientlessActionExecutorMgr{
|
||||
struct Record{ClientlessTravelCost::Profile JourneyCosts;std::string JourneyCostKey;bool HasJourneyCosts=false;struct{ClientlessActionExecutionState State=ClientlessActionExecutionState::Idle;}Status;};
|
||||
std::unordered_map<unsigned,Record>_records;
|
||||
bool StartAction(Player*,ClientlessAction const&,Record&);
|
||||
};
|
||||
'''+method+r'''
|
||||
int main(){
|
||||
ClientlessActionExecutorMgr m;Player leader;ClientlessAction action;auto& r=m._records[29];
|
||||
m.StartAction(&leader,action,r);auto original=r.JourneyCosts;assert(graph.draws==1);
|
||||
r.Status.State=ClientlessActionExecutionState::Failed;m.StartAction(&leader,action,r);
|
||||
assert(graph.draws==1&&r.JourneyCosts.Factors==original.Factors);
|
||||
r.Status.State=ClientlessActionExecutionState::SelectingTarget;m.StartAction(&leader,action,r);assert(graph.draws==1);
|
||||
action.Parameters["node"]="another-destination";m.StartAction(&leader,action,r);assert(graph.draws==2);
|
||||
r.Status.State=ClientlessActionExecutionState::Completed;m.StartAction(&leader,action,r);assert(graph.draws==3);
|
||||
Player follower;follower.id=31;action.Parameters["route_source"]="29";auto& peer=m._records[31];
|
||||
// Preferences are already inherited before a shared route is available.
|
||||
m.StartAction(&follower,action,peer);assert(graph.draws==3&&peer.JourneyCosts.Factors==r.JourneyCosts.Factors);
|
||||
action.Parameters.erase("route_source");peer.Status.State=ClientlessActionExecutionState::SelectingTarget;
|
||||
m.StartAction(&follower,action,peer);assert(graph.draws==3&&peer.JourneyCosts.Seed==r.JourneyCosts.Seed);
|
||||
// A forged/stale source from another group is not inherited.
|
||||
group.leader=99;action.Parameters["route_source"]="29";peer.Status.State=ClientlessActionExecutionState::Completed;
|
||||
m.StartAction(&follower,action,peer);assert(graph.draws==4&&peer.JourneyCosts.Seed!=r.JourneyCosts.Seed);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-journey-costs-') as d:
|
||||
p=Path(d);(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)
|
||||
print('Production journey initialization: new trip/new draw, stable retries, delayed leader route and group-safe modal inheritance passed.')
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Measured production edge lengths and stable per-journey modal preferences."""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
|
||||
a=s.index('void ClientlessTravelGraphMgr::RebuildEdgeDistances(')
|
||||
b=s.index('\nstd::string ClientlessTravelGraphMgr::FindNearestStartNode(',a)
|
||||
code=r'''
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
using uint32=unsigned;
|
||||
#define TC_LOG_DEBUG(...) ((void)0)
|
||||
struct Node{unsigned MapId=0;float X=0,Y=0,Z=0;};
|
||||
struct Point:Node{float O=0;};
|
||||
struct Edge{unsigned DatabaseId=0;std::string FromNode,ToNode,Type;float Cost=99999;bool Reverse=false;};
|
||||
struct NativePoint{unsigned ContinentID;struct{float X,Y,Z;}Loc;};
|
||||
using TaxiPathNodeList=std::vector<NativePoint const*>;
|
||||
std::vector<TaxiPathNodeList>sTaxiPathNodesByPath(2);
|
||||
struct Objects{void GetTaxiPath(unsigned,unsigned,unsigned&p,unsigned&fare){p=1;fare=98765;}}objects;auto sObjectMgr=&objects;
|
||||
struct Transports{static bool IsTransportEdge(std::string const&t){return t=="ship"||t=="tram";}float ConnectionDistance(std::string const&,std::string const&){return 777;}}transports;
|
||||
using ClientlessTransportMgr=Transports;auto sClientlessTransportMgr=&transports;
|
||||
struct ClientlessTravelGraphMgr{
|
||||
std::unordered_map<std::string,Node>_nodes;std::unordered_map<std::string,std::vector<Edge>>_edges;
|
||||
std::unordered_map<unsigned,std::vector<Point>>points;
|
||||
auto GetEdgePoints(unsigned id){return points.count(id)?&points[id]:nullptr;}
|
||||
void RebuildEdgeDistances();
|
||||
};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
using namespace ClientlessTravelCost;
|
||||
Profile neutral;
|
||||
auto first=Sample(neutral,1234,.2f),same=Sample(neutral,1234,.2f),other=Sample(neutral,9876,.2f);
|
||||
assert(first.Factors==same.Factors&&first.Factors!=other.Factors);
|
||||
for(float factor:first.Factors)assert(factor>=.8f&&factor<=1.2f);
|
||||
assert(Sample(neutral,1234,0).Factors==neutral.Factors);
|
||||
bool ground=false,taxi=false;
|
||||
for(unsigned seed=1;seed<=200;++seed){auto p=Sample(neutral,seed,.2f);if(p.Cost(Ground,1000)<p.Cost(Taxi,1050))ground=true;else taxi=true;}
|
||||
assert(ground&&taxi); // Repeated journeys can choose different near-equal modes.
|
||||
neutral.Factors[Flight]=2;assert(neutral.Cost(Flight,100)==200);
|
||||
assert(neutral.Cost(Portal,1000000)==1); // Cross-map coordinates never act as travelled distance.
|
||||
ClientlessTravelGraphMgr g;g._nodes["A"]={0,0,0,0};g._nodes["B"]={0,10,0,0};
|
||||
g._nodes["taxi_1"]={0,0,0,0};g._nodes["taxi_2"]={1,100000,0,0};
|
||||
g._edges["A"]={{1,"A","B","ground",99999,false},{2,"A","taxi_2","portal_use",99999,false},
|
||||
{3,"A","B","ship",100,false},{4,"A","taxi_2","ground",1,false}};
|
||||
g._edges["B"]={{1,"B","A","ground",1,true}};
|
||||
g.points[1]={{{0,0,0,0}},{{0,0,10,0}},{{0,10,10,0}},{{0,10,0,0}}};
|
||||
g._edges["taxi_1"]={{5,"taxi_1","taxi_2","taxi",1,false}};
|
||||
NativePoint a{0,{0,0,0}},b{0,{3,4,0}},c{0,{3,4,12}},d{1,{100000,0,0}},e{1,{100003,4,0}};
|
||||
sTaxiPathNodesByPath[1]={&a,&b,&c,&d,&e};g.RebuildEdgeDistances();
|
||||
assert(g._edges["A"][0].Cost==30&&g._edges["B"][0].Cost==30);
|
||||
assert(g._edges["A"][1].Cost==1&&g._edges["A"][2].Cost==777);
|
||||
assert(!std::isfinite(g._edges["A"][3].Cost));
|
||||
assert(g._edges["taxi_1"][0].Cost==22); // Native geometry, not ticket price; no cross-map jump.
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-travel-costs-') as d:
|
||||
p=Path(d);(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)
|
||||
print('Production measured roads/native taxi paths/portal transitions/transport distance and reproducible per-mode journey variation passed.')
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run production VMAP tile lifecycle methods against temporary files and a model cache.
|
||||
|
||||
--baseline <git-ref> reproduces both crash prerequisites using the specified revision.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASELINE = sys.argv[sys.argv.index('--baseline') + 1] if '--baseline' in sys.argv else None
|
||||
|
||||
def read(relative):
|
||||
if BASELINE:
|
||||
return subprocess.check_output(['git', 'show', BASELINE + ':' + relative], cwd=ROOT, text=True)
|
||||
return (ROOT / relative).read_text()
|
||||
|
||||
def function(source, marker):
|
||||
start = source.index(marker)
|
||||
opening = source.index('{', start)
|
||||
depth, end = 1, opening + 1
|
||||
while depth:
|
||||
depth += (source[end] == '{') - (source[end] == '}')
|
||||
end += 1
|
||||
return source[start:end]
|
||||
|
||||
source = read('src/common/Collision/Maps/MapTree.cpp')
|
||||
header = read('src/common/Collision/Maps/MapTree.h')
|
||||
header = '\n'.join(line for line in header.splitlines() if not line.startswith('#'))
|
||||
header = header.replace('private:', 'public:').replace('TC_COMMON_API ', '')
|
||||
methods = '\n'.join(function(source, marker) for marker in [
|
||||
'std::string StaticMapTree::getTileFileName(',
|
||||
'StaticMapTree::StaticMapTree(', 'StaticMapTree::~StaticMapTree(',
|
||||
'StaticMapTree::TileFileOpenResult StaticMapTree::OpenMapTileFile(',
|
||||
'LoadResult StaticMapTree::CanLoadMap(', 'void StaticMapTree::UnloadMap(',
|
||||
'LoadResult StaticMapTree::LoadMapTile(', 'void StaticMapTree::UnloadMapTile('
|
||||
])
|
||||
preamble = r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
using uint8=uint8_t;using uint32=uint32_t;using int32=int32_t;
|
||||
#define TC_LOG_DEBUG(...) ((void)0)
|
||||
#define TC_LOG_ERROR(...) ((void)0)
|
||||
#define VMAP_ERROR_LOG(...) ((void)0)
|
||||
#define TC_METRIC_EVENT(...) ((void)0)
|
||||
namespace G3D{struct Vector3{};struct Ray{};float finf(){return 1e30f;}}
|
||||
namespace VMAP{
|
||||
constexpr char VMAP_MAGIC[]="VMAPtest";
|
||||
bool readChunk(FILE*f,char*buffer,char const*magic,int length){return int(fread(buffer,1,length,f))==length&&!std::memcmp(buffer,magic,length);}
|
||||
enum class LoadResult:uint8{Success,FileNotFound,VersionMismatch,ReadFromFileFailed,DisabledInConfig};
|
||||
struct BIH{};class GroupModel{};
|
||||
class WorldModel{public:std::string name;std::string const&GetName()const{return name;}};
|
||||
struct ModelSpawn{
|
||||
uint32 ID=0,flags=0;std::string name;
|
||||
static bool readFromFile(FILE*f,ModelSpawn&s){uint32 n;if(fread(&s.ID,4,1,f)!=1||fread(&n,4,1,f)!=1||n>10000)return false;s.name.resize(n);return fread(s.name.data(),1,n,f)==n;}
|
||||
};
|
||||
class ModelInstance{
|
||||
WorldModel*model=nullptr;
|
||||
public:ModelInstance()=default;ModelInstance(ModelSpawn const&,WorldModel*m):model(m){};
|
||||
WorldModel*getWorldModel(){return model;}void setUnloaded(){model=nullptr;}
|
||||
};
|
||||
class VMapManager2{
|
||||
public:
|
||||
struct Cached{WorldModel*model;int refs;};std::map<std::string,Cached>cache;
|
||||
int failures=0,acquires=0,releases=0,badReleases=0;
|
||||
~VMapManager2(){for(auto&pair:cache)delete pair.second.model;}
|
||||
int32 getParentMapId(uint32 map){return map==1?0:-1;}
|
||||
static std::string getMapFileName(uint32 map){std::ostringstream out;out<<std::setw(3)<<std::setfill('0')<<map<<".vmtree";return out.str();}
|
||||
WorldModel*acquireModelInstance(std::string const&,std::string const&name,uint32=0){++acquires;if(failures){--failures;return nullptr;}auto it=cache.find(name);if(it==cache.end())it=cache.emplace(name,Cached{new WorldModel{name},0}).first;++it->second.refs;return it->second.model;}
|
||||
void releaseModelInstance(std::string const&name){++releases;auto it=cache.find(name);if(it==cache.end()){++badReleases;return;}if(!--it->second.refs){delete it->second.model;cache.erase(it);}}
|
||||
int refs(std::string const&name){auto it=cache.find(name);return it==cache.end()?0:it->second.refs;}
|
||||
};
|
||||
}
|
||||
'''
|
||||
tests = r'''
|
||||
namespace VMAP{
|
||||
void init(StaticMapTree&t){t.iNTreeValues=2;t.iTreeValues=new ModelInstance[2];t.iSpawnIndices={{7,0},{8,1}};}
|
||||
void writeTile(std::string const&base,uint32 map,uint32 x,uint32 y,std::vector<std::pair<uint32,std::string>> const&models,uint32 declared=0){
|
||||
auto path=base+StaticMapTree::getTileFileName(map,x,y);FILE*f=fopen(path.c_str(),"wb");assert(f);fwrite(VMAP_MAGIC,1,8,f);uint32 count=declared?declared:uint32(models.size());fwrite(&count,4,1,f);
|
||||
for(auto const&model:models){uint32 id=model.first,n=uint32(model.second.size());fwrite(&id,4,1,f);fwrite(&n,4,1,f);fwrite(model.second.data(),1,n,f);}fclose(f);
|
||||
}
|
||||
size_t fds(){size_t count=0;for(auto const&entry:std::filesystem::directory_iterator("/proc/self/fd")){(void)entry;++count;}return count;}
|
||||
void probe(std::string const&base){
|
||||
{std::ofstream f(base+"000.vmtree",std::ios::binary);f.write(VMAP_MAGIC,8);}writeTile(base,0,2,3,{{7,"crystal"}});
|
||||
VMapManager2 vm;auto before=fds();for(int i=0;i<128;++i)assert(StaticMapTree::CanLoadMap(base,0,2,3,&vm)==LoadResult::Success);assert(fds()==before);
|
||||
// A real parent-map fallback must validate the already opened parent tile.
|
||||
{std::ofstream f(base+"001.vmtree",std::ios::binary);f.write(VMAP_MAGIC,8);}
|
||||
assert(StaticMapTree::CanLoadMap(base,1,2,3,&vm)==LoadResult::Success);assert(fds()==before);
|
||||
{std::ofstream f(base+StaticMapTree::getTileFileName(0,2,3),std::ios::binary);f<<"badmagic";}
|
||||
assert(StaticMapTree::CanLoadMap(base,0,2,3,&vm)==LoadResult::VersionMismatch);assert(fds()==before);
|
||||
}
|
||||
void lifetime(std::string const&base){
|
||||
VMapManager2 vm;StaticMapTree tree(0,base);init(tree);writeTile(base,0,4,5,{{7,"crystal"}});
|
||||
// Transient failure while a tile stream consumes the last file descriptor.
|
||||
vm.failures=1;tree.LoadMapTile(4,5,&vm);
|
||||
// Later the stream closes, letting a dynamic GameObject load this same model.
|
||||
auto dynamic=vm.acquireModelInstance(base,"crystal");assert(dynamic&&vm.refs("crystal")==1);
|
||||
tree.UnloadMapTile(4,5,&vm);assert(vm.refs("crystal")==1&&vm.releases==0);vm.releaseModelInstance("crystal");
|
||||
}
|
||||
void shared(std::string const&base){
|
||||
VMapManager2 vm;StaticMapTree tree(0,base);init(tree);auto dynamic=vm.acquireModelInstance(base,"shared");assert(dynamic);
|
||||
writeTile(base,0,2,3,{{7,"shared"}});writeTile(base,0,4,5,{{7,"shared"}});
|
||||
tree.LoadMapTile(2,3,&vm);tree.LoadMapTile(4,5,&vm);assert(vm.refs("shared")==3);
|
||||
tree.LoadMapTile(2,3,&vm);assert(vm.refs("shared")==3); // repeated checks do not acquire twice
|
||||
std::filesystem::remove(base+StaticMapTree::getTileFileName(0,2,3)); // unloading cannot depend on reopening data
|
||||
tree.UnloadMapTile(2,3,&vm);assert(vm.refs("shared")==2&&tree.iTreeValues[0].getWorldModel());
|
||||
tree.UnloadMap(&vm);assert(vm.refs("shared")==1&&!tree.iTreeValues[0].getWorldModel()&&!tree.numLoadedTiles());
|
||||
tree.UnloadMap(&vm);assert(vm.refs("shared")==1&&vm.badReleases==0);vm.releaseModelInstance("shared");
|
||||
// Truncated tile: release exactly the successfully read prefix, even if the file is removed.
|
||||
writeTile(base,0,6,7,{{7,"prefix"}},2);assert(tree.LoadMapTile(6,7,&vm)==LoadResult::ReadFromFileFailed);
|
||||
assert(vm.refs("prefix")==1);std::filesystem::remove(base+StaticMapTree::getTileFileName(0,6,7));tree.UnloadMap(&vm);assert(!vm.refs("prefix")&&!vm.badReleases);
|
||||
// Missing data is tracked as an empty tile without acquiring/releasing any models.
|
||||
assert(tree.LoadMapTile(30,31,&vm)==LoadResult::FileNotFound);tree.UnloadMap(&vm);assert(vm.badReleases==0);
|
||||
// Overlapping parent spawns absent from the child's index are skipped without leaking references.
|
||||
StaticMapTree child(1,base);init(child);writeTile(base,0,8,9,{{99,"parent-only"},{7,"indexed"}});
|
||||
assert(child.LoadMapTile(8,9,&vm)==LoadResult::Success);assert(!vm.refs("parent-only")&&vm.refs("indexed")==1);child.UnloadMap(&vm);assert(!vm.refs("indexed")&&!vm.badReleases);
|
||||
}
|
||||
}
|
||||
int main(int argc,char**argv){assert(argc==3);std::string base=std::string(argv[2])+"/";std::string test=argv[1];if(test=="fd")VMAP::probe(base);else if(test=="lifetime")VMAP::lifetime(base);else VMAP::shared(base);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-vmap-lifetime-') as folder:
|
||||
folder = Path(folder)
|
||||
cpp = folder / 'test.cpp'
|
||||
cpp.write_text(preamble + header + '\nnamespace VMAP{\n' + methods + '\n}\n' + tests)
|
||||
exe = folder / 'test'
|
||||
subprocess.run(['g++', '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', str(cpp), '-o', str(exe)], check=True)
|
||||
for case in (['fd', 'lifetime'] if BASELINE else ['fd', 'lifetime', 'shared']):
|
||||
data = folder / case
|
||||
data.mkdir()
|
||||
result = subprocess.run([str(exe), case, str(data)], capture_output=True, text=True)
|
||||
if BASELINE:
|
||||
assert result.returncode != 0, (case, 'baseline unexpectedly passed')
|
||||
print('Baseline reproduced:', case, 'exit', result.returncode, result.stderr.strip())
|
||||
else:
|
||||
assert result.returncode == 0, (case, result.stdout, result.stderr)
|
||||
print('Passed:', case)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Run production WG client permission synchronization and battle refresh."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/"src/server/game/Battlefield/BattlefieldMgr.cpp").read_text()
|
||||
a=s.index("void BattlefieldMgr::UpdateWintergraspFlight(Player*")
|
||||
b=s.index("\nBattlefield* BattlefieldMgr::GetBattlefieldToZoneId",a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
using uint32=unsigned;
|
||||
constexpr unsigned SPELL_AURA_MOUNT_RESTRICTIONS=336;
|
||||
enum class AreaMountFlags:unsigned{AllowFlyingMounts=2};
|
||||
struct AuraEffect {unsigned id,misc;unsigned GetId()const{return id;}int GetMiscValue()const{return misc;}};
|
||||
struct Player {unsigned zone=4197,area=4197,map=571;int casts=0,updates=0;std::set<unsigned> auras;std::vector<AuraEffect const*> effects;
|
||||
unsigned GetMapId()const{return map;}unsigned GetAreaId()const{return area;}unsigned GetZoneId()const{return zone;}
|
||||
auto const& GetAuraEffectsByType(unsigned)const{return effects;}bool HasAura(unsigned id)const{return auras.count(id);}
|
||||
void AddAura(unsigned id,Player*){auras.insert(id);++casts;}void RemoveAurasDueToSpell(unsigned id){auras.erase(id);}void UpdateMountCapability(){++updates;}};
|
||||
struct Reference {Player* player;Player* GetSource()const{return player;}};
|
||||
struct Map {std::vector<Reference> players;auto const& GetPlayers()const{return players;}};
|
||||
struct BattlefieldMgr {bool permitted=true;bool IsWintergraspFlightAllowed(unsigned map,unsigned area)const{return permitted&&map==571&&(area==4197||area==4538);}void UpdateWintergraspFlight(Player*)const;void UpdateWintergraspFlight(Map*)const;};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){BattlefieldMgr m;Player p,out;out.zone=210;out.area=210;Map map{{{&p},{&out}}};
|
||||
m.UpdateWintergraspFlight(&map);assert(p.HasAura(91262)&&p.casts==1&&p.updates==1&&!out.HasAura(91262));
|
||||
AuraEffect own{91262,15};p.effects={&own};m.UpdateWintergraspFlight(&p);assert(p.casts==1&&p.HasAura(91262));
|
||||
m.permitted=false;m.UpdateWintergraspFlight(&map);assert(!p.HasAura(91262)&&p.updates==2);
|
||||
m.permitted=true;m.UpdateWintergraspFlight(&map);assert(p.HasAura(91262)&&p.casts==2);
|
||||
p.area=4538;m.UpdateWintergraspFlight(&p);assert(p.HasAura(91262));
|
||||
p.area=210;m.UpdateWintergraspFlight(&p);assert(!p.HasAura(91262));
|
||||
p.area=4197;AuraEffect restriction{91124,13};p.effects={&restriction};m.UpdateWintergraspFlight(&p);assert(!p.HasAura(91262));
|
||||
AuraEffect permission{86973,15};p.effects={&permission};m.UpdateWintergraspFlight(&p);assert(p.HasAura(91262));
|
||||
p.effects={&own,&restriction};m.UpdateWintergraspFlight(&p);assert(!p.HasAura(91262));}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-wg-client-permission-") as d:
|
||||
p=Path(d);(p/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17",str(p/"test.cpp"),"-o",str(p/"test")],check=True)
|
||||
subprocess.run([str(p/"test")],check=True)
|
||||
print("WG permission: entry/subarea/idempotence/battle start/end/exit/aura precedence PASS")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Exercise shared Wintergrasp mount permission with actual manager method."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/"src/server/game/Battlefield/BattlefieldMgr.cpp").read_text()
|
||||
a=s.index("bool BattlefieldMgr::IsWintergraspFlightAllowed(")
|
||||
b=s.index("\nvoid BattlefieldMgr::UpdateWintergraspFlight",a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
using uint32=unsigned;
|
||||
struct DBCManager{static bool IsInArea(unsigned a,unsigned z){return a==z||(z==4197&&a==4538);}};
|
||||
struct Map{unsigned id=571;unsigned GetId()const{return id;}};
|
||||
struct Battlefield{bool enabled=true,war=false;unsigned zone=4197;unsigned GetZoneId()const{return zone;}bool IsEnabled()const{return enabled;}bool CanFlyIn()const{return !war;}};
|
||||
struct BattlefieldMgr{std::map<Map const*,std::vector<std::unique_ptr<Battlefield>>> _battlefieldsByMap;bool IsWintergraspFlightAllowed(uint32,uint32)const;};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){BattlefieldMgr m;Map map;assert(m.IsWintergraspFlightAllowed(571,4197));assert(m.IsWintergraspFlightAllowed(571,4538));assert(!m.IsWintergraspFlightAllowed(0,4197));assert(!m.IsWintergraspFlightAllowed(571,210));auto bf=std::make_unique<Battlefield>();auto p=bf.get();m._battlefieldsByMap[&map].push_back(std::move(bf));assert(m.IsWintergraspFlightAllowed(571,4538));p->war=true;assert(!m.IsWintergraspFlightAllowed(571,4197));assert(!m.IsWintergraspFlightAllowed(571,4538));p->enabled=false;assert(m.IsWintergraspFlightAllowed(571,4538));p->enabled=true;p->war=false;assert(m.IsWintergraspFlightAllowed(571,4197));}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-wg-flight-") as d:
|
||||
p=Path(d);(p/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17",str(p/"test.cpp"),"-o",str(p/"test")],check=True)
|
||||
subprocess.run([str(p/"test")],check=True)
|
||||
print("WG main/subarea: absent, disabled, peace, war, end and unrelated areas PASS")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Run the production flying-area cast check, including explicit aura restrictions."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/"src/server/game/Spells/SpellInfo.cpp").read_text()
|
||||
a=s.index(" // continent limitation (virtual continent)",s.index("SpellInfo::CheckLocation"))
|
||||
b=s.index(" // raid instance limitation",a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
using uint32=unsigned;
|
||||
enum class AreaMountFlags{None=0,AllowFlyingMounts=2};
|
||||
template<class T>struct EnumFlag{int bits;EnumFlag(T v):bits(int(v)){}void operator|=(T v){bits|=int(v);}bool HasFlag(T v){return bits&int(v);}};
|
||||
constexpr int SPELL_AURA_MOUNT_RESTRICTIONS=1,SPELL_ATTR4_ONLY_FLYING_AREAS=2,SPELL_FAILED_INCORRECT_AREA=3;
|
||||
struct AuraEffect{int mask;int GetMiscValue()const{return mask;}};
|
||||
struct Player{bool restrict=false;std::vector<AuraEffect const*> auras;bool HasAuraType(int)const{return restrict;}auto const& GetAuraEffectsByType(int)const{return auras;}};
|
||||
struct AreaTableEntry{int flags=13;EnumFlag<AreaMountFlags> GetMountFlags()const{return AreaMountFlags(flags);}} area;
|
||||
struct Areas{AreaTableEntry const* LookupEntry(unsigned){return &area;}} sAreaTableStore;
|
||||
struct Battlefields{bool war=false;bool IsWintergraspFlightAllowed(unsigned map,unsigned id){return map==571&&(id==4197||id==4538)&&!war;}} battlefield;auto sBattlefieldMgr=&battlefield;
|
||||
struct SpellInfo{bool HasAttribute(int)const{return true;}int CheckLocation(unsigned map_id,unsigned area_id,Player const* player){
|
||||
'''+s[a:b]+r'''
|
||||
return 0;}};
|
||||
int main(){SpellInfo s;Player p;assert(s.CheckLocation(571,4197,&p)==0);assert(s.CheckLocation(571,4538,&p)==0);battlefield.war=true;assert(s.CheckLocation(571,4538,&p)==SPELL_FAILED_INCORRECT_AREA);battlefield.war=false;assert(s.CheckLocation(571,4538,&p)==0);assert(s.CheckLocation(571,210,&p)==SPELL_FAILED_INCORRECT_AREA);p.restrict=true;AuraEffect noFlight{1};p.auras={&noFlight};assert(s.CheckLocation(571,4197,&p)==SPELL_FAILED_INCORRECT_AREA);AuraEffect flight{3};p.auras={&flight};assert(s.CheckLocation(571,4197,&p)==0);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-wg-mount-cast-") as directory:
|
||||
p=Path(directory);(p/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17",str(p/"test.cpp"),"-o",str(p/"test")],check=True)
|
||||
subprocess.run([str(p/"test")],check=True)
|
||||
print("Production mount cast: WG/main/subarea peace/war/end, unrelated zone, aura precedence PASS")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Exercise ZM graveyard ownership with production registration and member setup."""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
header = (root / "src/server/scripts/OutdoorPvP/OutdoorPvPZM.h").read_text()
|
||||
script = (root / "src/server/scripts/OutdoorPvP/OutdoorPvPZM.cpp").read_text()
|
||||
source = (root / "src/server/game/OutdoorPvP/OutdoorPvP.cpp").read_text()
|
||||
member = next(line for line in header.splitlines() if " m_Graveyard" in line and "State" not in line)
|
||||
a = script.index(" m_Graveyard =", script.index("bool OutdoorPvPZM::SetupOutdoorPvP"))
|
||||
b = script.index("\n", script.index("AddCapturePoint(",a))
|
||||
setup = script[a:b]
|
||||
a = source.index("void OutdoorPvP::AddCapturePoint(")
|
||||
b = source.index("\nOPvPCapturePoint* OutdoorPvP::GetCapturePoint",a)
|
||||
registration = source[a:b]
|
||||
code = r'''
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
#define TC_LOG_ERROR(...) ((void)0)
|
||||
struct OutdoorPvP;
|
||||
int destroyed=0;
|
||||
struct OPvPCapturePoint {unsigned m_capturePointSpawnId=0; virtual ~OPvPCapturePoint(){++destroyed;}};
|
||||
struct OPvPCapturePointZM_Graveyard:OPvPCapturePoint {explicit OPvPCapturePointZM_Graveyard(OutdoorPvP*){}};
|
||||
struct OutdoorPvP {
|
||||
using OPvPCapturePointMap=std::map<unsigned,std::unique_ptr<OPvPCapturePoint>>;
|
||||
OPvPCapturePointMap m_capturePoints;
|
||||
virtual ~OutdoorPvP()=default;
|
||||
void AddCapturePoint(OPvPCapturePoint* cp);
|
||||
};
|
||||
''' + registration + "struct OutdoorPvPZM: OutdoorPvP {\n" + member + "\nvoid setup(){\n" + setup + r'''
|
||||
}
|
||||
};
|
||||
int main(){for(int i=0;i<100;++i){auto p=std::make_unique<OutdoorPvPZM>();p->setup();assert(p->m_capturePoints.size()==1);assert(p->m_capturePoints.begin()->second.get()==p->m_Graveyard);}assert(destroyed==100);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-zm-ownership-") as directory:
|
||||
path=Path(directory)
|
||||
(path/"test.cpp").write_text(code)
|
||||
subprocess.run(["c++","-std=c++17","-fsanitize=address","-fno-omit-frame-pointer",str(path/"test.cpp"),"-o",str(path/"test")],check=True)
|
||||
subprocess.run([str(path/"test")],check=True)
|
||||
print("ZM graveyard: 100 lifecycles, exactly one destruction each, AddressSanitizer PASS")
|
||||
Reference in new issue
Block a user