Use journey-stable distance costs for clientless travel routes
This commit is contained in:
1 parent
3c361e435a
commit
2778cf9bdd
19 files changed
+1375
-95
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.
|
||||||
@@ -1,3 +1,265 @@
|
|||||||
|
# Aktueller Stand 02.11.2026, 06:21 MEZ
|
||||||
|
|
||||||
|
Das Entfernungskostenmodell ist gebaut und installiert. Acht gezielte Tests sind
|
||||||
|
bestanden; Build und Installation exit 0. Details und Logs:
|
||||||
|
`checkpoints/2026-11-02-travel-costs/`. Kein Commit oder Push.
|
||||||
|
|
||||||
|
Testserver nach Wiederaufnahme frisch gestartet: PID 219333, ready nachgewiesen,
|
||||||
|
Ports 8085/8087 offen. Loader wieder aktiv; 110 Bots online. Die vorherigen 15
|
||||||
|
Recovery-Bots wurden offline nativ in ihre Fraktionshauptstadt versetzt und wieder
|
||||||
|
geladen. Die Unterbrechung und der ausgefallene ursprüngliche Test bleiben unten
|
||||||
|
dokumentiert; keine Auswertung um 03:00 behaupten.
|
||||||
|
|
||||||
|
Vom Nutzer neu bestätigt: 06:00 Gruppenbildung, 07:00 Raid/Dungeon. 92 Bots ab
|
||||||
|
Level 15 haben beide Slots verifiziert; missing=0. Allianz Goldene Rose,
|
||||||
|
Horde Broken Tusk (poi_npc_287214). 18 aktuell online unter Level 15 wurden von
|
||||||
|
Teständerungen ausgenommen. Alle zehn bekannten 85er haben 07:00 raid mit
|
||||||
|
instance_visit_at_6354_d0 (Bastion normal10). Aktuelle Gruppe 5, Leader Renren159:
|
||||||
|
159,160,161,162,163,164,165,167,176. Corric166 ist noch solo, schedule=1,
|
||||||
|
Gruppenbildungs-Fallback poi_npc_313808 statt eigentlichem Treffpunkt beobachtet.
|
||||||
|
Elanora168 wurde aus der Testgruppe entfernt; externe Allianz85 35/168 bekommen
|
||||||
|
07:00 Dungeon statt Raid, damit sie nicht die zehnte Position belegen. Einmaliger
|
||||||
|
nativer groupmanager run führte Corric bisher nicht zu. Nicht behaupten, alle
|
||||||
|
zehn seien bereits gemeinsam. Im Managerlauf außerdem wiederholte Transfers
|
||||||
|
Arthoran26 zwischen zwei Dungeon-Gruppen beobachtet; Log erhalten, kein Fix.
|
||||||
|
|
||||||
|
Read-only Beobachter und Berichtstimer sind implementiert und aktiv:
|
||||||
|
`~/.config/systemd/user/vcore-adventure-observer-20261102.service`
|
||||||
|
`~/.config/systemd/user/vcore-adventure-report-20261102.{service,timer}`.
|
||||||
|
Timer nachgewiesen für 02.11.2026 08:00 Europe/Berlin, Persistent=true.
|
||||||
|
Skripte, Manifest, Rohdaten: `checkpoints/2026-11-02-0600-group-test/`.
|
||||||
|
Erste vollständige Beobachtung ca. 06:13; frühere Lücke im Bericht ausweisen.
|
||||||
|
Bericht wird lokal nach `doc/clientless/2026-11-02-0800-test-report.md` geschrieben.
|
||||||
|
Bis dahin ist die Datei ausdrücklich Platzhalter. Keine automatische Chatnachricht
|
||||||
|
möglich/zugesagt. Bericht nach 08:00 auf erfolgreiche Erstellung prüfen und Nutzer
|
||||||
|
über Ergebnisse informieren. Beobachter repariert keine Bots während des Tests.
|
||||||
|
|
||||||
|
Noch offen: tatsächliche Ankunft/Instanzeintritt und vollständige Zehnergruppe;
|
||||||
|
ursprüngliche MMAP-Höhenvalidator-Ablehnung, Treffpunkt-Fallback und Kolonnenbildung
|
||||||
|
sind nicht durch das Kostenmodell behoben. Gameplay-Abnahme steht aus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Fortsetzung06:04MEZ, Ersatztest06/07/08 vomNutzerfreigegeben
|
||||||
|
|
||||||
|
Reisekostenmodell kompiliert/installiert (build-installed.log exit0, install.log
|
||||||
|
exit0), ZweiterStart00:30 readyPID214613 Ports8085/8087. NativeTaxi15=6457.25yd,
|
||||||
|
Transportgeometrien positiv. SiebengezielteTestsgrün: Kosten/NativeTransport/
|
||||||
|
JourneyLifecycle/SharedRoute/Planner/GroundExit/AreaNode, RescueGraphzusätzlich.
|
||||||
|
NeueQuellen ClientlessTravelCost.h undProfile-/Geometrieintegration inGraph/
|
||||||
|
Executor/Transport; ConfigCostMultiplier alle7Modi1, Variation0.20. NativeRNG
|
||||||
|
proReise, RecoverybehältProfil, FollowerkopierenLeaderRoute+Faktoren.
|
||||||
|
NochkeinevollständigeLive-Abnahme/keinCommit/Push.
|
||||||
|
|
||||||
|
SitzungvorAbschlussunterbrochen: Loaderblieb0, alte03Uhr-TesteinrichtungNICHT
|
||||||
|
abgeschlossen, KEINE03Uhr-Auswertung/keinTimer eingerichtet. NICHTanderesbehaupten.
|
||||||
|
BeiFortsetzung06:02 Serveroffline, keinvcore-worldserver-tmux/Prozess. Logsenden
|
||||||
|
04:03:45ohnesauberesShutdown; keinaktuellerCore (nurhistorischer01.11.Core).
|
||||||
|
Ursacheungeklärt. VorigesServerloggesichertimKostencheckpoint.
|
||||||
|
|
||||||
|
NutzerneuerTest: JETZT06:00Gruppenbildung,07:00Dungeon/Raid; Auswertungentsprechend
|
||||||
|
08MEZ (bisheralsrelativeFortsetzungkommuniziert). AlleAllianz:GoldeneRoseSW,
|
||||||
|
Horde:BrokenTuskORG. Unter15ausnehmen undnormalePläneunverändert (explizitbeantwortet).
|
||||||
|
Bekannte85er159..167/176 auf10normalBastion6354_d0für07. Alle10wiederladen.
|
||||||
|
Testcheckpoint checkpoints/2026-11-02-0600-group-test/.
|
||||||
|
Serverjetztrecovern, alte15Restbotsnochprüfen/bergensoweitnötig, Loader1restaurieren.
|
||||||
|
NativeeditplanallnurOnline: erst07instance-or-raidauto,06formationmitFraktions-
|
||||||
|
location. FürCohort07raid6354_d0 explizit. Unter15unverändert.
|
||||||
|
|
||||||
|
Plan fürlokaleZeitsteuerung: UserSystemd funktioniert, keineChat-Automationstools
|
||||||
|
verfügbar. NochNICHTimplementiert: read-onlyObserver mitNativeStatus/DBGroup-
|
||||||
|
Snapshots+Logdelta, einmaligerTimer08MEZ schreibtBericht. Keineautomatische
|
||||||
|
Chatnachrichtzusagen. KeineCLI-Subagentenstarten (Delegationnichtbeauftragt).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Routenalternativenfix final installiert, Restbetrieb restauriert
|
||||||
|
|
||||||
|
Details: checkpoints/2026-11-01-route-alternatives/README.md.
|
||||||
|
FinalerBuild-j10/InstallPASS, freshreadyPID200529/Ports8085+8087.
|
||||||
|
ViergezielteRegressionenPASS inklPortalroot, PortalblockEscape, verworfeneRoad-
|
||||||
|
Kante1885 gehtanRecovery, konkurrierendeBudgetverbraucher+isolierterPortalflug.
|
||||||
|
NegativeKontrolle ohnePortalverbindungs-Pass fällt erwartbarfehl.
|
||||||
|
LiveIFPortalnutzungRenren23:32:04->06/arrivalError0.00 bestätigt.
|
||||||
|
LiveSW23:42:21: vollständigeFlug->Portal200627->Flug-Verbindungwirdjetzt
|
||||||
|
berücksichtigt, gewinntabernicht gegenTaxiroute. NeuerbelegterKostenbefund:
|
||||||
|
Taxikante15(SW->IF) cost1, aufgezeichneteStraße478.308, FlügeDistanzkosten.
|
||||||
|
Nichtbehaupten, SWPortal werdejetztgewählt; konsistenteMakroreisekosten offen.
|
||||||
|
SourceänderungenPlanner/Executor, zweiTests; keinKolonnen- oderHöhenvalidator-
|
||||||
|
Bypass. ZweiBuilds/Restarts mitLoader0/SafeLogout undje5Minuten-Regel; Recovery-
|
||||||
|
Listen22/3 undBergungeninHauptstädten dokumentiert. RenrenScheduleonrestauriert,
|
||||||
|
Loaderconfig1/nativeReloadrestauriert, normalerBetrieb läuftwiederan.
|
||||||
|
UrsprünglicheZehnergruppewarbeimSafeLogout imaltenRuntimebereitsinTeilgruppen
|
||||||
|
zerfallen;kein vollständigerRaid/Ankunftbestätigt. KeinCommit/Push.
|
||||||
|
NächsteArbeit: echteTransportzeitkosten stattTaxipauschale1, genaueGrundweg-
|
||||||
|
Ablehnungsdiagnostik, Kolonnenintegrationwievorheruntersucht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: finaler Portal-Flugverbindungsfix gebaut, zweiterRestart ausstehend
|
||||||
|
|
||||||
|
ErsterFix installiert/frischready PID195658 Ports8085/8087. LiveRenren
|
||||||
|
benutzteIFPortal217855 nativ23:32:04->23:32:06, arrivalError0. SW->Bastion
|
||||||
|
wählteaberweiterIF. Deshalb zweiterPlannerfix: vorRoadExitBudget begrenzt
|
||||||
|
vollständige nahePortal-Flugverbindungen root->source(freeflight)->portal->
|
||||||
|
target->destination(freeflight) aufGesamtkosten prüfen, nativeRechte/Sperren
|
||||||
|
behalten, maximal8Kandidaten/6neueEstimates imbestehenden16erBudget.
|
||||||
|
KeineBodenkanten zuSWPortalen hinzugefügt. NeueRegression konkurrierendeExits+
|
||||||
|
unbrauchbarerStart schlägt OHNE dieseErgänzung erwartbarfehl, MIT ihrPASS.
|
||||||
|
FinalerCMakeBuild-j10 EXIT0 (build-final.log), nochNICHTinstalliert.
|
||||||
|
ZweiterSafeLogout23:35:20MEZ, Loader weiterhin0; FünfMinutengrenze23:40:20.
|
||||||
|
Zuletzt4Restbots73/92/96/132,96bereitsdanachsicherausgeloggt. NativeDiagnose,
|
||||||
|
Recoveryliste recovery-before-final-install.tsv unter route-alternatives.
|
||||||
|
VorShutdown aktuelleRestliste/Flags/Saveallaktualisieren, shutdown10, Prozessende
|
||||||
|
prüfen, finalinstall,startready/ports, Restbotsprüfen/ggfHauptstädte.
|
||||||
|
Loader1 zumAbschlussrestaurieren. Renren159 wurdefürPortaltests manuellgestoppt,
|
||||||
|
abschließend schedule159on wiederherstellen. AndereBotpläneunverändert.
|
||||||
|
KeinCommit/Push. ÄnderungenPlanner+Executor+zweiRegressionen. KeineKolonnen-
|
||||||
|
oderGrundvalidatorreparatur. DerenBefunde bleibenoffen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Routenalternativen-Fix gebaut, Installation steht unmittelbar aus
|
||||||
|
|
||||||
|
Autorisiert durchNutzerkontext: trotz gescheitertemFlugstart Graphalternativen
|
||||||
|
nutzen; SWCataclysmPortale nurdurchFlug erreichbar lassen. ZweiSourcefixes:
|
||||||
|
PortalGuard auchStartwurzel, bei gesperrtemPortal weiterhinStraßen zulassen;
|
||||||
|
GruppenRoadAdapter-Reject führt überRecoverTravel mitgesperrterKante weiter.
|
||||||
|
StartFlightProbes max4 reserviertPlanbudget für isolierteFluganschlüsse.
|
||||||
|
Plannerregression erweitert: Portalroot/PortalblockEscape/40unerreichbareRoots+
|
||||||
|
isolierterPortalflug; PASS. GroundExit-Test verifiziertRecovery1885; PASS.
|
||||||
|
PortalPrerequisites/PortalUse PASS. CMakeDebugBuild-j10 EXIT0, nochNICHTinstalliert.
|
||||||
|
Loaderconfig1->0, tatsächlichenabled0/busy0. SafeLogout23:24:15MEZ angefordert;
|
||||||
|
22Restbots gegen23:28 inFlug/Kampf. FünfMinuten-Grenze23:29:15.
|
||||||
|
Belege/Recoveryliste/Flags/NativeStatus unter
|
||||||
|
checkpoints/2026-11-01-route-alternatives/. VorShutdown nochmalsStatus sichern;
|
||||||
|
sauberShutdown10, Prozessende VOR cmake--install. Dannfreshready+Ports prüfen,
|
||||||
|
22Recoverybots laden/Transitstatesprüfen und nötigenfallsAllianceSW/HordeORG.
|
||||||
|
Loader1 undnormalenBetrieb danachwiederherstellen. Gruppe warbereits imalten
|
||||||
|
Runtime beimSafeLogout inmehrereTeilgruppen zerfallen; keinevollständigeGruppe
|
||||||
|
behaupten. KeinCommit/Push. KeinKolonnenumbau/keinGrundvalidatorBypass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Nutzerfragen Portale/Kolonne untersucht, nurDiagnose
|
||||||
|
|
||||||
|
Zusatz imBericht doc/clientless/2026-11-01-raid-test-investigation.md.
|
||||||
|
IFPortal wird NICHT benutzt: LeaderRoute enthält an217855Quelle free_flight
|
||||||
|
zurBastion; Start indoor/no reachable open takeoff point; Recovery safetyAdapter
|
||||||
|
reject. Quellknoten alsStartwurzel umgehtPortalGuard(previous fehlt).
|
||||||
|
SW200627Portal vorhanden/benutzbarhistorisch, aber ohneeingehendefesteKante.
|
||||||
|
Exakter Grund der Nichtauswahl UNGEKLÄRT: dynamischeFlugverbindung/budget/Estimate
|
||||||
|
nicht kandidatenbezogen geloggt. Ungefiltert geometrisch sogarRang1 vonSWtrade/
|
||||||
|
taxi2; nicht behaupten, es wäre nur zuweit/Questgesperrt.
|
||||||
|
Kolonne: BotLed umgehtallgemeinesFollow; gemeinsameExecutorRouten statt dauernder
|
||||||
|
Positionsformation. Adapter nuraufgezeichneteStraßen, keineZuläufe/nativeTaxi-
|
||||||
|
Versätze; gemeinsameEndpunkte undside0Fallback ermöglichenÜberlagerung.
|
||||||
|
Winienwi nachNutzerTeleport bisIF gereist, dort23:13:45 und23:14:25 gleiche
|
||||||
|
safetyAdapterAblehnung bei(-4606.44,-928.997,501.07). Einzelbeobachtung auf
|
||||||
|
Nutzerhinweis beendet: gesamteGruppebetroffen. KeinEingriff/Build/Restart.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Winienwis Blockade live genauer eingegrenzt,23:07
|
||||||
|
|
||||||
|
Nutzer bat besonders Winienwi163 zu beobachten, virtueller Einstieg vorGasthaus.
|
||||||
|
Nur Diagnose, keine Code-/Botänderung. Wiederholte Versuche23:06:45 und23:07:17:
|
||||||
|
virtueller Einstieg WIRD gewählt: (-8843.320,667.142,97.829), Start
|
||||||
|
(-8863.633,676.073,97.903), vollständiger MMAP10Punkte/35.30yd/22.19Luftlinie,
|
||||||
|
maxZDelta0.60, finalDistance0.00. Native travelinfo belegt eindeutig
|
||||||
|
Path.Reason='owner ground-safety validator rejected spline'. Gruppenvalidator
|
||||||
|
ValidateGroundPath (AdventureGroupTravelMgr) wird bei Observe auch fürLeader
|
||||||
|
installiert; prüft alle2yd Höhe/Steigung sowieLOS proSegment. Welcher innere
|
||||||
|
Zweig/Probe ablehnt, wird noch nicht geloggt. Nicht mehr bloß Vermutung über
|
||||||
|
Gebietspflicht: bei Winienwi ist OWNER VALIDATOR nachgewiesen.
|
||||||
|
Alternative echteKnoten und5Recoveries scheitern ebenso; Flugfallback scheitert
|
||||||
|
am Bodenweg zum Freiluft-Startplatz. Statische Flight-probe liest am aktuellen
|
||||||
|
XY roof115.617, nearFeet97.903 (korrekter Innenboden); beim Ziel97.830.
|
||||||
|
Dies beweist mehrere Höhenlagen, aber NICHT, dass der Gruppenvalidator die
|
||||||
|
Dachhöhe ausgewählt hat. Keine pauschale Ground-/LOS-Abschaltung vornehmen.
|
||||||
|
Belege winienwi-live-diagnostics.log/winienwi-navigation.log unter
|
||||||
|
checkpoints/2026-11-01-raid-2200/. Nächster sinnvoller Schritt: gezielte genaue
|
||||||
|
Ablehnungsdiagnostik in ValidateGroundPath, an derselbenPosition reproduzieren.
|
||||||
|
Kein Build/Install/Restart, kein Teleport.10erRaidintents bleiben gesetzt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Alle10 Raidintents auf10normal korrigiert, ca.23:05
|
||||||
|
|
||||||
|
Nutzer autorisierte Anpassung aller zehn aktuellen Raidintents. Native editplan
|
||||||
|
preview/apply für159..167/176: aktueller Slot23 raid instance_visit_at_6354_d0.
|
||||||
|
DB bestätigt alle10 persistent. GroupPlan Leader159 jetzt capacity10; native
|
||||||
|
INSTANCE_PLAN mode0/map671/entry6354. NeunBots in erster Nachkontrolle Moving/
|
||||||
|
Travel zurBastion, Winienwi163 lokaler Anreiseversuch Failed und30sRetry zum
|
||||||
|
selben Ziel (ground approach interrupted). Keine Ankunft/Gameplay bestätigt.
|
||||||
|
Keine Sourceänderung/Build/Restart. Gruppe bleibt zehnköpfig. Nachweise:
|
||||||
|
checkpoints/2026-11-01-raid-2200/10man-applied.tsv und10man-console.log.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Fehlgeschlagenen Raidtest untersucht (nur Diagnose)
|
||||||
|
|
||||||
|
Bericht: doc/clientless/2026-11-01-raid-test-investigation.md.
|
||||||
|
Bestätigt: Basisschlüssel6354 wählt25normal/Difficulty1, intern fälschlich20;
|
||||||
|
10er-Normal heißt6354_d0 (nativeEligibility erlaubt). Completefalse verhindert
|
||||||
|
frühe und23Uhr-Abreise. Vorbereitung hätte explizite10er-Variante wählen müssen.
|
||||||
|
Zerstreuung:22:00:02 vollständiger9.38yd-MMAP-Weg zuAllison bei Ausführung
|
||||||
|
PathRejected, Recoveries erschöpft; Leader-Fallback wechselt37x Treffpunkt bis
|
||||||
|
andere Kontinente. Genauer erster Höhen/LOS/Gebiet-Ablehnungszweig noch offen.
|
||||||
|
164/165 nur vorübergehend im Reisehandover; inzwischen alle10 raid/WaitingGroup.
|
||||||
|
Kein Codefix/Build/Restart/Botplanänderung bei dieser Diagnose.
|
||||||
|
Belege unter checkpoints/2026-11-01-raid-2200/initial-navigation-failure.log und
|
||||||
|
investigation-console.log. Vorschläge/Regressionsumfang im Bericht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Raidtest-Reisereset am 01.11.2026 ca.22:53 MEZ
|
||||||
|
|
||||||
|
Auf ausdrücklichen Nutzerauftrag alle10 Raidbots159..167/176 zurück zur
|
||||||
|
Goldenen Rose gesetzt und alte Reisepläne entfernt. Vorherige Diagnose22:47:
|
||||||
|
Gruppe2/Leader159 hat exakt10 online, aber Plan meldet capacity20/Anzeige25;
|
||||||
|
Formation-Fallback wechselte wiederholt Hauptstädte/Kontinente. Kein Codefix.
|
||||||
|
Für vollständigen Reset gezielter Logout der10 (ohne Serverrestart), Offline-
|
||||||
|
Status ALLER10 vor Positionsänderung geprüft. Offlineposition genau auf Allison
|
||||||
|
poi_npc_314039: map0 -8867.79/673.673/97.9864/o5.20108, zone1519,
|
||||||
|
instance0, taxi_path leer. Wieder eingeloggt: alte Executor-/Travel-/Flight-
|
||||||
|
und Behavior-RAM-Records durch Logout verworfen. Aktuellen Formationplan aller10
|
||||||
|
nativ erneut auf poi_npc_314039 gesetzt, Schedule1. 23-Raidplan unverändert.
|
||||||
|
Alle10 native area5148/map0 bestätigt, jeweils kein freier Flugzustand.
|
||||||
|
Loader weiter normal1; kein Build/Install/Serverrestart. Gruppe bleibt bestehen.
|
||||||
|
Nachweise checkpoints/2026-11-01-raid-2200/{before-reset-positions.tsv,reset-console.log}.
|
||||||
|
Reset behebt nicht den zugrunde liegenden Treffpunkt-/Raidkapazitätsfehler;
|
||||||
|
weiteren Verlauf nicht als erfolgreich abgenommen darstellen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Raidtest am 01.11.2026 um 22/23 Uhr vorbereitet
|
||||||
|
|
||||||
|
Nutzerauftrag: zehn Allianz-AdventureBots Stufe85 in Sturmwind, 22:00
|
||||||
|
Gruppenbildung, 23:00 Raid Bastion des Zwielichts. Roster: Tanks Ragnorli167
|
||||||
|
und Ashaelri176; Heiler Marnda161 (Schamane), Aruendas162 (Priester);
|
||||||
|
Damage Renren159, Marndra160, Winienwi163, Taliaarnora164, Brynda165, Corric166.
|
||||||
|
Rollen aus generierten Profilen geprüft: 2T/2H/6D. Alle zehn eingeloggt.
|
||||||
|
Tagesplan für 20261101 persistent über native editplan-Befehle gesetzt:
|
||||||
|
21 rest, 22 groupformation, beide poi_npc_314039 (Allison, Gilded Rose, SW);
|
||||||
|
23 raid instance_visit_at_6354 (Twilight Bastion Entrance, NICHT3529/MoltenCore).
|
||||||
|
Tagesplansteuerung an. Bestehende Teilgruppen bleiben dem normalen Gruppenmanager
|
||||||
|
überlassen; noch keine vollständige Zehnergruppe oder Raid-Ankunft behauptet.
|
||||||
|
Zunächst poi_guard_22 vom Editor abgelehnt, deshalb auf gültigen gemeinsamen
|
||||||
|
NPC-Treffpunkt korrigiert. Aktive Altanreise einiger Bots brauchte sicheren
|
||||||
|
Handover; 163/164/166 BehaviorStop, erneuter nativer SW-Teleport und ScheduleOn.
|
||||||
|
|
||||||
|
Testserver war zu Beginn offline (kein Prozess/tmux). Ohne Build gestartet,
|
||||||
|
frische ready-Meldung/PID180309 und Ports8085/8087 geprüft. Normalen Loaderbetrieb
|
||||||
|
beibehalten. Kein Build, keine Installation, kein Push/Commit.
|
||||||
|
Nachweise: checkpoints/2026-11-01-raid-2200/console.log und verified-db.tsv.
|
||||||
|
Final: alle10 nach Saveall online1/Level85/map0/zone1519 und taxi_path leer;
|
||||||
|
Corric nach abgeschlossenem Routen-Handover erneut SW gesetzt, native area5148
|
||||||
|
und schedule1/rest bestätigt. Alle10 DB-Slots22/23 geprüft.
|
||||||
|
Die tatsächliche Gruppenbildung/Abreise/Gameplay ab22/23 bleibt live zu prüfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Aktuell: Quest-unabhängige Portalnutzung für AdventureBots ab Stufe 80
|
# Aktuell: Quest-unabhängige Portalnutzung für AdventureBots ab Stufe 80
|
||||||
|
|
||||||
AdventureBots können keinen Questfortschritt erwerben. Graphgebundene öffentliche
|
AdventureBots können keinen Questfortschritt erwerben. Graphgebundene öffentliche
|
||||||
|
|||||||
@@ -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,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/`.
|
||||||
@@ -214,6 +214,35 @@ bool ClientlessActionExecutorMgr::OwnsActiveFlight(uint32 characterGuid) const
|
|||||||
|
|
||||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
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.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
|
||||||
record.TransportActive = false;
|
record.TransportActive = false;
|
||||||
record.FlightActive = false;
|
record.FlightActive = false;
|
||||||
@@ -635,7 +664,7 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
|
|||||||
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||||
player, destinationNode, startRadius, record.LocalBridgeRadius,
|
player, destinationNode, startRadius, record.LocalBridgeRadius,
|
||||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys))
|
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts))
|
||||||
{
|
{
|
||||||
TC_LOG_WARN("server.adventurebot.navigation",
|
TC_LOG_WARN("server.adventurebot.navigation",
|
||||||
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_node' destination='%s' reason='%s'",
|
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_node' destination='%s' reason='%s'",
|
||||||
@@ -827,7 +856,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
|||||||
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||||
player, anchorNode, startRadius, record.LocalBridgeRadius,
|
player, anchorNode, startRadius, record.LocalBridgeRadius,
|
||||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
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",
|
TC_LOG_WARN("server.adventurebot.navigation",
|
||||||
"C820_LOCATION_PLAN_FAILED location='%s' reason='%s'",
|
"C820_LOCATION_PLAN_FAILED location='%s' reason='%s'",
|
||||||
@@ -1854,8 +1883,11 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
|
|||||||
size_t original=path.size();
|
size_t original=path.size();
|
||||||
if(!adapter->second(player,path))
|
if(!adapter->second(player,path))
|
||||||
{
|
{
|
||||||
player->StopMoving();record.Active=false;record.Status.State=ClientlessActionExecutionState::Failed;
|
player->StopMoving();
|
||||||
record.Status.Detail="recorded group road rejected by safety adapter";return false;
|
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(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;}
|
{if(player->IsSplineEnabled())player->StopMoving();record.FormationWaiting=true;record.Status.State=ClientlessActionExecutionState::Moving;record.Status.Detail="waiting for leading formation row";return true;}
|
||||||
@@ -2335,7 +2367,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
|||||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||||
player, anchorNode, radius, record.LocalBridgeRadius,
|
player, anchorNode, radius, record.LocalBridgeRadius,
|
||||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal);
|
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
error = "no graph node exists on destination map";
|
error = "no graph node exists on destination map";
|
||||||
@@ -2351,7 +2383,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
|||||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||||
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
|
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
|
||||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys);
|
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts);
|
||||||
|
|
||||||
if (planned)
|
if (planned)
|
||||||
TC_LOG_WARN("server.adventurebot.navigation",
|
TC_LOG_WARN("server.adventurebot.navigation",
|
||||||
@@ -3034,6 +3066,11 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
|||||||
auto const snapshot=source->second;
|
auto const snapshot=source->second;
|
||||||
auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node");
|
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;
|
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 start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1);
|
||||||
auto anchor=route[start];
|
auto anchor=route[start];
|
||||||
std::vector<ClientlessTravelRouteStep> joined;
|
std::vector<ClientlessTravelRouteStep> joined;
|
||||||
@@ -3043,7 +3080,7 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
bool complete=false;std::string reached,error;
|
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());
|
joined.insert(joined.end(),route.begin()+start+1,route.end());
|
||||||
record.Route=std::move(joined);record.RouteStep=0;
|
record.Route=std::move(joined);record.RouteStep=0;
|
||||||
@@ -3053,6 +3090,6 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
|
|||||||
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
|
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
|
||||||
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
|
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
|
||||||
record.HardDestination=snapshot.HardDestination;record.Active=true;
|
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);
|
return StartRouteStep(player,player->GetGUID().GetCounter(),record);
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
struct Record
|
struct Record
|
||||||
{
|
{
|
||||||
|
ClientlessTravelCost::Profile JourneyCosts;
|
||||||
|
std::string JourneyCostKey;
|
||||||
|
bool HasJourneyCosts = false;
|
||||||
ClientlessActionExecutionStatus Status;
|
ClientlessActionExecutionStatus Status;
|
||||||
bool Active = false;
|
bool Active = false;
|
||||||
std::shared_ptr<ClientlessAction> PendingShared;
|
std::shared_ptr<ClientlessAction> PendingShared;
|
||||||
|
|||||||
@@ -22,8 +22,10 @@
|
|||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "Transport.h"
|
#include "Transport.h"
|
||||||
#include "TransportMgr.h"
|
#include "TransportMgr.h"
|
||||||
|
#include "Spline.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
#include "WorldSession.h"
|
#include "WorldSession.h"
|
||||||
|
#include <G3D/Quat.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -326,6 +328,63 @@ ClientlessTransportStop const* ClientlessTransportMgr::GetStop(std::string const
|
|||||||
if(!EnsureLoaded())return nullptr;
|
if(!EnsureLoaded())return nullptr;
|
||||||
auto found=_stops.find(node);return found==_stops.end()?nullptr:&found->second;
|
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)
|
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;
|
from.clear();to.clear();if(!player->GetTransport() || !EnsureLoaded())return false;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public:
|
|||||||
static ClientlessTransportMgr* instance();
|
static ClientlessTransportMgr* instance();
|
||||||
bool Reload();
|
bool Reload();
|
||||||
bool HasConnection(std::string const& from, std::string const& to);
|
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 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);
|
bool ArrivalNode(Player* player, std::string const& from, std::string& to);
|
||||||
ClientlessTransportStop const* GetStop(std::string const& node);
|
ClientlessTransportStop const* GetStop(std::string const& node);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
#include "ClientlessLuaEngine.h"
|
#include "ClientlessLuaEngine.h"
|
||||||
|
|
||||||
#include "DatabaseEnv.h"
|
#include "DatabaseEnv.h"
|
||||||
|
#include "DBCStores.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Random.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "ClientlessTravelMgr.h"
|
#include "ClientlessTravelMgr.h"
|
||||||
@@ -317,6 +320,7 @@ bool ClientlessTravelGraphMgr::Reload()
|
|||||||
while (pointResult->NextRow());
|
while (pointResult->NextRow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RebuildEdgeDistances();
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
|
|
||||||
TC_LOG_INFO("server.adventurebot",
|
TC_LOG_INFO("server.adventurebot",
|
||||||
@@ -326,6 +330,61 @@ bool ClientlessTravelGraphMgr::Reload()
|
|||||||
return !_nodes.empty();
|
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(
|
std::string ClientlessTravelGraphMgr::FindNearestStartNode(
|
||||||
Player const* player,
|
Player const* player,
|
||||||
float maxDistance) const
|
float maxDistance) const
|
||||||
@@ -364,8 +423,11 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
|||||||
float startRadius,
|
float startRadius,
|
||||||
std::vector<ClientlessTravelRouteStep>& route,
|
std::vector<ClientlessTravelRouteStep>& route,
|
||||||
std::string& error,
|
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();
|
route.clear();
|
||||||
error.clear();
|
error.clear();
|
||||||
|
|
||||||
@@ -448,7 +510,7 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
|||||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float const candidate = current.Cost + edge.Cost;
|
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
|
||||||
if (candidate >= distance[edge.ToNode])
|
if (candidate >= distance[edge.ToNode])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -575,7 +637,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
std::string& error,
|
std::string& error,
|
||||||
std::unordered_set<uint32> const* blockedEdgeIds,
|
std::unordered_set<uint32> const* blockedEdgeIds,
|
||||||
std::unordered_set<std::string> const* blockedBridgeKeys,
|
std::unordered_set<std::string> const* blockedBridgeKeys,
|
||||||
ClientlessTravelLocationGoal const* locationGoal)
|
ClientlessTravelLocationGoal const* locationGoal,
|
||||||
|
ClientlessTravelCost::Profile const* journeyCosts)
|
||||||
{
|
{
|
||||||
struct Trace
|
struct Trace
|
||||||
{
|
{
|
||||||
@@ -639,6 +702,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
|
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
|
||||||
{ error="destination location denied by area catalog"; return false; }
|
{ 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);
|
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||||
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
|
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
|
||||||
// Preserve part of the global budget for nodes reached later in the
|
// Preserve part of the global budget for nodes reached later in the
|
||||||
@@ -666,12 +734,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
return std::numeric_limits<float>::infinity();
|
return std::numeric_limits<float>::infinity();
|
||||||
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
||||||
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
||||||
if(access && std::isfinite(result))
|
if(std::isfinite(result))
|
||||||
{
|
{
|
||||||
float distance=a.GetExactDist(&b);
|
float length=a.GetExactDist(&b);
|
||||||
if(distance<20)result=std::numeric_limits<float>::infinity();
|
result=access && length<20?std::numeric_limits<float>::infinity():flightDistanceCost(length);
|
||||||
else result=(sClientlessFlightMgr->Preferred(player,map,a) || sClientlessFlightMgr->Preferred(player,map,b))?
|
|
||||||
40+distance*0.4f:180+distance*1.5f;
|
|
||||||
}
|
}
|
||||||
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
||||||
flightEstimates.emplace(estimateKey, result);
|
flightEstimates.emplace(estimateKey, result);
|
||||||
@@ -687,45 +753,38 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
bridgeRadius = MaxVirtualNodeBridge;
|
bridgeRadius = MaxVirtualNodeBridge;
|
||||||
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
|
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
|
||||||
|
|
||||||
// The overwhelmingly common free-flight case is a same-map Location. Do
|
float directFlightCost=std::numeric_limits<float>::infinity();
|
||||||
// not construct a macro graph first: Estimate already enforces licenses,
|
auto directFlightRoute=[&]()
|
||||||
// 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())
|
|
||||||
{
|
{
|
||||||
ClientlessTravelNode departureNode;
|
ClientlessTravelRouteStep departure;
|
||||||
departureNode.Key = "flight_departure";
|
departure.NodeKey = "flight_departure";
|
||||||
departureNode.MapId = player->GetMapId();
|
departure.NodeType = "ground";
|
||||||
departureNode.X = player->GetPositionX();
|
departure.EdgeType = "start";
|
||||||
departureNode.Y = player->GetPositionY();
|
departure.MapId = player->GetMapId();
|
||||||
departureNode.Z = player->GetPositionZ();
|
departure.X = player->GetPositionX();
|
||||||
float const directFlight = flightCost(departureNode, locationGoal->MapId,
|
departure.Y = player->GetPositionY();
|
||||||
locationGoal->X, locationGoal->Y, locationGoal->Z);
|
departure.Z = player->GetPositionZ();
|
||||||
if (std::isfinite(directFlight))
|
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;
|
ClientlessTravelRouteStep flight;
|
||||||
flight.NodeKey = "flight_location";
|
flight.NodeKey = "flight_location";
|
||||||
flight.NodeType = "location";
|
flight.NodeType = "location";
|
||||||
flight.EdgeType = "free_flight";
|
flight.EdgeType = "free_flight";
|
||||||
flight.MapId = locationGoal->MapId;
|
flight.MapId = locationGoal->MapId;
|
||||||
flight.X = locationGoal->X;
|
flight.X = locationGoal->X;
|
||||||
flight.Y = locationGoal->Y;
|
flight.Y = locationGoal->Y;
|
||||||
flight.Z = locationGoal->Z;
|
flight.Z = locationGoal->Z;
|
||||||
route.push_back(std::move(flight));
|
route.push_back(std::move(flight));
|
||||||
complete = true;
|
complete = true;
|
||||||
reachedNode = "flight_location";
|
reachedNode = "flight_location";
|
||||||
return true;
|
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
|
struct State
|
||||||
@@ -807,6 +866,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
|
|
||||||
std::unordered_set<std::string> flightRoots;
|
std::unordered_set<std::string> flightRoots;
|
||||||
uint32 startCount = 0;
|
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)
|
for (auto const& pair : _nodes)
|
||||||
{
|
{
|
||||||
ClientlessTravelNode const& node = pair.second;
|
ClientlessTravelNode const& node = pair.second;
|
||||||
@@ -820,7 +883,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
if (localDistance > startRadius)
|
if (localDistance > startRadius)
|
||||||
continue;
|
continue;
|
||||||
float startCost = localDistance;
|
float startCost = groundCost(localDistance);
|
||||||
|
|
||||||
// VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node.
|
// VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node.
|
||||||
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key))
|
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key))
|
||||||
@@ -842,9 +905,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
// the recovery/re-entry mode for nodes that local navigation cannot
|
// the recovery/re-entry mode for nodes that local navigation cannot
|
||||||
// reach, not a shortcut racing every nearby pavement node.
|
// reach, not a shortcut racing every nearby pavement node.
|
||||||
float air = std::numeric_limits<float>::infinity();
|
float air = std::numeric_limits<float>::infinity();
|
||||||
if (!reachable && !(blockedBridgeKeys &&
|
if (!reachable && startFlightProbes < MaxStartFlightProbes && !(blockedBridgeKeys &&
|
||||||
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
|
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
|
||||||
|
{
|
||||||
|
++startFlightProbes;
|
||||||
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
|
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
|
||||||
|
}
|
||||||
if(std::isfinite(air) && !reachable)
|
if(std::isfinite(air) && !reachable)
|
||||||
{flightRoots.insert(node.Key);startCost=air;}
|
{flightRoots.insert(node.Key);startCost=air;}
|
||||||
else
|
else
|
||||||
@@ -870,7 +936,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
|
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
startCost = entryNav.PathLength;
|
startCost = groundCost(entryNav.PathLength);
|
||||||
if (!reachable)
|
if (!reachable)
|
||||||
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
|
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
|
||||||
}
|
}
|
||||||
@@ -988,9 +1054,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
continue;
|
continue;
|
||||||
uint32 const startIndex = candidate.Edge.Reverse
|
uint32 const startIndex = candidate.Edge.Reverse
|
||||||
? candidate.Count - 1 - candidate.Index : candidate.Index;
|
? 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,
|
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);
|
auto old = distance.find(key);
|
||||||
if (old != distance.end() && old->second <= newCost)
|
if (old != distance.end() && old->second <= newCost)
|
||||||
continue;
|
continue;
|
||||||
@@ -1000,7 +1066,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
previous.erase(key);
|
previous.erase(key);
|
||||||
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
|
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
|
||||||
candidate.Edge.Reverse, candidate.Point,
|
candidate.Edge.Reverse, candidate.Point,
|
||||||
alreadyThere ? 0.0f : nav.PathLength };
|
alreadyThere ? 0.0f : groundCost(nav.PathLength) };
|
||||||
open.push({ newCost, state });
|
open.push({ newCost, state });
|
||||||
++accepted;
|
++accepted;
|
||||||
}
|
}
|
||||||
@@ -1011,10 +1077,92 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
|
|
||||||
if (!startCount && !accepted)
|
if (!startCount && !accepted)
|
||||||
{
|
{
|
||||||
|
if(std::isfinite(directFlightCost))return directFlightRoute();
|
||||||
error = "no reachable travel node or recorded ground edge near current position";
|
error = "no reachable travel node or recorded ground edge near current position";
|
||||||
return false;
|
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
|
// C8.20: virtual node bridges are equal-cost ground alternatives and
|
||||||
// participate even when stored graph routes also exist.
|
// participate even when stored graph routes also exist.
|
||||||
bool const repairRequired = true;
|
bool const repairRequired = true;
|
||||||
@@ -1163,13 +1311,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
candidate.EdgeId = edge.DatabaseId;
|
candidate.EdgeId = edge.DatabaseId;
|
||||||
candidate.Reverse = edge.Reverse;
|
candidate.Reverse = edge.Reverse;
|
||||||
candidate.EndIndex = i;
|
candidate.EndIndex = i;
|
||||||
candidate.RoadCost = edge.Cost * (travelled / totalLength);
|
candidate.RoadCost = edgeCost(edge) * (travelled / totalLength);
|
||||||
candidate.DistanceSq = dsq;
|
candidate.DistanceSq = dsq;
|
||||||
candidate.Point = point;
|
candidate.Point = point;
|
||||||
bool const groundExitExhausted = blockedBridgeKeys &&
|
bool const groundExitExhausted = blockedBridgeKeys &&
|
||||||
blockedBridgeKeys->count("ground_exit_edge:" + identity);
|
blockedBridgeKeys->count("ground_exit_edge:" + identity);
|
||||||
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
|
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
|
// A recorded edge can contain hundreds of samples. Calling
|
||||||
// FlightMgr::Estimate for every sample made the synchronous
|
// FlightMgr::Estimate for every sample made the synchronous
|
||||||
@@ -1186,10 +1334,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
|
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
|
||||||
{
|
{
|
||||||
float const direct = std::sqrt(dsq);
|
float const direct = std::sqrt(dsq);
|
||||||
// Forty plus 0.4 * distance is the cheapest possible
|
// Use this journey's flight factor for both candidate
|
||||||
// free-flight cost. It is an admissible ordering key;
|
// ordering and execution cost, including expensive modes.
|
||||||
// rejected area segments simply advance to the next item.
|
rankedFlightExits.push_back({ candidate.RoadCost + flightDistanceCost(direct),
|
||||||
rankedFlightExits.push_back({ candidate.RoadCost + 40.0f + direct * 0.4f,
|
|
||||||
std::move(candidate) });
|
std::move(candidate) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1245,7 +1392,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
uint32 StartIndex = 0;
|
uint32 StartIndex = 0;
|
||||||
ClientlessTravelEdgePoint EntryPoint;
|
ClientlessTravelEdgePoint EntryPoint;
|
||||||
} bestExit;
|
} bestExit;
|
||||||
float bestGoalCost = std::numeric_limits<float>::infinity();
|
float bestGoalCost = directFlightCost;
|
||||||
std::string bestGoalState;
|
std::string bestGoalState;
|
||||||
bool bestFinalFlight = false;
|
bool bestFinalFlight = false;
|
||||||
// Every permitted node is a potential way back into the persistent graph.
|
// Every permitted node is a potential way back into the persistent graph.
|
||||||
@@ -1261,7 +1408,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
if (node.MapId != destination.MapId)
|
if (node.MapId != destination.MapId)
|
||||||
return std::numeric_limits<float>::infinity();
|
return std::numeric_limits<float>::infinity();
|
||||||
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
|
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
|
||||||
return 40.0f + std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
|
return flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
|
||||||
};
|
};
|
||||||
float best = lowerBoundToGoal(candidate);
|
float best = lowerBoundToGoal(candidate);
|
||||||
auto edges = _edges.find(candidate.Key);
|
auto edges = _edges.find(candidate.Key);
|
||||||
@@ -1275,7 +1422,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
auto next = _nodes.find(edge.ToNode);
|
auto next = _nodes.find(edge.ToNode);
|
||||||
if (next == _nodes.end())
|
if (next == _nodes.end())
|
||||||
continue;
|
continue;
|
||||||
best = std::min(best, edge.Cost + lowerBoundToGoal(next->second));
|
best = std::min(best, edgeCost(edge) + lowerBoundToGoal(next->second));
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
};
|
};
|
||||||
@@ -1308,7 +1455,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
if (fromNodeItr != _edges.end())
|
if (fromNodeItr != _edges.end())
|
||||||
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
|
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
|
||||||
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
|
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
|
||||||
fullEdgeCost = edge.Cost;
|
fullEdgeCost = edgeCost(edge);
|
||||||
if (fullEdgeCost <= 0.0f)
|
if (fullEdgeCost <= 0.0f)
|
||||||
continue;
|
continue;
|
||||||
float const partialCost = roadSectionCost(exit.EdgeId,
|
float const partialCost = roadSectionCost(exit.EdgeId,
|
||||||
@@ -1371,7 +1518,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
std::string const finishKey = "finish:" + candidateNode.Key;
|
std::string const finishKey = "finish:" + candidateNode.Key;
|
||||||
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
||||||
? std::numeric_limits<float>::infinity() :
|
? 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)
|
if (current.Cost + finalCost < bestGoalCost)
|
||||||
{
|
{
|
||||||
bestGoalCost = current.Cost + finalCost;
|
bestGoalCost = current.Cost + finalCost;
|
||||||
@@ -1420,7 +1567,17 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
|
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
|
||||||
[](ClientlessTravelEdge const& edge)
|
[](ClientlessTravelEdge const& edge)
|
||||||
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
||||||
bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end();
|
// 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.
|
// Persistent TravelGraph edges always reset the bridge guard.
|
||||||
auto edgeItr = _edges.find(current.Value.Node);
|
auto edgeItr = _edges.find(current.Value.Node);
|
||||||
@@ -1438,7 +1595,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
|
|
||||||
State next{ edge.ToNode, false };
|
State next{ edge.ToNode, false };
|
||||||
std::string const nextKey = stateKey(next);
|
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);
|
auto old = distance.find(nextKey);
|
||||||
if (old != distance.end() && candidate >= old->second)
|
if (old != distance.end() && candidate >= old->second)
|
||||||
@@ -1474,7 +1631,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
// Even a preferred legal flight costs at least this much.
|
// Even a preferred legal flight costs at least this much.
|
||||||
// Skip expensive terrain sampling when it cannot improve the
|
// Skip expensive terrain sampling when it cannot improve the
|
||||||
// route already found; this preserves the full graph search.
|
// 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(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
|
||||||
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
|
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
|
||||||
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
||||||
@@ -1637,9 +1794,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
if (exit.EdgeId != entry.Edge->DatabaseId ||
|
if (exit.EdgeId != entry.Edge->DatabaseId ||
|
||||||
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
|
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
|
||||||
continue;
|
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,
|
roadSectionCost(exit.EdgeId, exit.Reverse, entryIndex,
|
||||||
exit.EndIndex, entry.Edge->Cost);
|
exit.EndIndex, edgeCost(*entry.Edge));
|
||||||
if (score >= bestGoalCost)
|
if (score >= bestGoalCost)
|
||||||
continue;
|
continue;
|
||||||
bestGoalCost = score;
|
bestGoalCost = score;
|
||||||
@@ -1651,9 +1808,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
std::string const nextKey = stateKey(next);
|
std::string const nextKey = stateKey(next);
|
||||||
uint32 const begin = entry.Edge->Reverse
|
uint32 const begin = entry.Edge->Reverse
|
||||||
? entry.PointCount - 1 - entry.Index : entry.Index;
|
? 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,
|
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);
|
auto old = distance.find(nextKey);
|
||||||
if (old != distance.end() && cost >= old->second)
|
if (old != distance.end() && cost >= old->second)
|
||||||
continue;
|
continue;
|
||||||
@@ -1767,7 +1924,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
|
|
||||||
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
|
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
|
||||||
// ground connections, not penalized fallback routes.
|
// ground connections, not penalized fallback routes.
|
||||||
float const candidate = current.Cost + measuredLength;
|
float const candidate = current.Cost + groundCost(measuredLength);
|
||||||
auto old = distance.find(nextKey);
|
auto old = distance.find(nextKey);
|
||||||
if (old != distance.end() && candidate >= old->second)
|
if (old != distance.end() && candidate >= old->second)
|
||||||
continue;
|
continue;
|
||||||
@@ -1782,6 +1939,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(bestGoalState.empty() && std::isfinite(directFlightCost))return directFlightRoute();
|
||||||
|
|
||||||
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
|
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
|
||||||
!bestGoalState.empty())
|
!bestGoalState.empty())
|
||||||
previous.erase(bestGoalState);
|
previous.erase(bestGoalState);
|
||||||
@@ -2132,8 +2291,11 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
|||||||
std::vector<ClientlessTravelRouteStep>& route,
|
std::vector<ClientlessTravelRouteStep>& route,
|
||||||
std::string& anchorNode,
|
std::string& anchorNode,
|
||||||
std::string& error,
|
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();
|
route.clear();
|
||||||
anchorNode.clear();
|
anchorNode.clear();
|
||||||
error.clear();
|
error.clear();
|
||||||
@@ -2202,8 +2364,8 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
|||||||
if (localStartDistance > startRadius)
|
if (localStartDistance > startRadius)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
distance[node.Key] = localStartDistance;
|
distance[node.Key] = costs.Cost(ClientlessTravelCost::Ground,localStartDistance);
|
||||||
open.push({ localStartDistance, node.Key });
|
open.push({ distance[node.Key], node.Key });
|
||||||
++startCandidateCount;
|
++startCandidateCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2233,7 +2395,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
|||||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float const candidate = current.Cost + edge.Cost;
|
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
|
||||||
if (candidate >= distance[edge.ToNode])
|
if (candidate >= distance[edge.ToNode])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "ClientlessGraphProbeCache.h"
|
#include "ClientlessGraphProbeCache.h"
|
||||||
|
#include "ClientlessTravelCost.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -114,13 +115,16 @@ public:
|
|||||||
bool Reload();
|
bool Reload();
|
||||||
bool EnsureLoaded();
|
bool EnsureLoaded();
|
||||||
|
|
||||||
|
ClientlessTravelCost::Profile NewJourneyCosts() const;
|
||||||
|
|
||||||
bool PlanRoute(
|
bool PlanRoute(
|
||||||
Player const* player,
|
Player const* player,
|
||||||
std::string const& destinationNode,
|
std::string const& destinationNode,
|
||||||
float startRadius,
|
float startRadius,
|
||||||
std::vector<ClientlessTravelRouteStep>& route,
|
std::vector<ClientlessTravelRouteStep>& route,
|
||||||
std::string& error,
|
std::string& error,
|
||||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
|
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||||
|
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
|
||||||
|
|
||||||
bool PlanRouteToPosition(
|
bool PlanRouteToPosition(
|
||||||
Player const* player,
|
Player const* player,
|
||||||
@@ -133,7 +137,8 @@ public:
|
|||||||
std::vector<ClientlessTravelRouteStep>& route,
|
std::vector<ClientlessTravelRouteStep>& route,
|
||||||
std::string& anchorNode,
|
std::string& anchorNode,
|
||||||
std::string& error,
|
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
|
// C8.13b: transient local bridges connect nearby graph islands for one
|
||||||
// route plan. Bridges are never persisted and may never follow another
|
// route plan. Bridges are never persisted and may never follow another
|
||||||
@@ -151,7 +156,8 @@ public:
|
|||||||
std::string& error,
|
std::string& error,
|
||||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||||
std::unordered_set<std::string> const* blockedBridgeKeys = nullptr,
|
std::unordered_set<std::string> const* blockedBridgeKeys = nullptr,
|
||||||
ClientlessTravelLocationGoal const* locationGoal = nullptr);
|
ClientlessTravelLocationGoal const* locationGoal = nullptr,
|
||||||
|
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
|
||||||
|
|
||||||
bool FindNearestNodeToPosition(
|
bool FindNearestNodeToPosition(
|
||||||
uint32 mapId,
|
uint32 mapId,
|
||||||
@@ -182,6 +188,7 @@ public:
|
|||||||
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void RebuildEdgeDistances();
|
||||||
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
|
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
|
||||||
|
|
||||||
ClientlessGraphProbeCache _positionProbes;
|
ClientlessGraphProbeCache _positionProbes;
|
||||||
|
|||||||
@@ -4396,3 +4396,18 @@ Clientless.Browser.Battleground.QueueEnabled = 1
|
|||||||
Clientless.Browser.Battleground.JoinEnabled = 0
|
Clientless.Browser.Battleground.JoinEnabled = 0
|
||||||
# Explicit isolated test override. Does not override disabled Queue/Join switches.
|
# Explicit isolated test override. Does not override disabled Queue/Join switches.
|
||||||
Clientless.Browser.AllowBotOnlyTests = 0
|
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
|
||||||
@@ -11,6 +11,7 @@ preamble=r'''
|
|||||||
#include "ClientlessGraphProbeCache.h"
|
#include "ClientlessGraphProbeCache.h"
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include "ClientlessTravelCost.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -34,14 +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;
|
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;
|
auto sClientlessTravelMgr=&travel;
|
||||||
struct Flight {
|
struct Flight {
|
||||||
bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9;
|
float closedFromX=1e9;bool rejectNearbyTargets=false;bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9;
|
||||||
float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9;
|
float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9;
|
||||||
void BeginPlanning(Player const* = nullptr){}
|
void BeginPlanning(Player const* = nullptr){}
|
||||||
bool Enabled(Player const*){return enabled;}
|
bool Enabled(Player const*){return enabled;}
|
||||||
bool Preferred(Player const*,uint32,Position const&){return false;}
|
bool Preferred(Player const*,uint32,Position const&){return false;}
|
||||||
float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){
|
float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){
|
||||||
bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f;
|
bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f;
|
||||||
float d=dist(a,b);return enabled&&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();
|
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;
|
} flight;
|
||||||
auto sClientlessFlightMgr=&flight;
|
auto sClientlessFlightMgr=&flight;
|
||||||
@@ -187,6 +188,69 @@ 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[2].NodeKey=="Q"&&route[2].EdgeType=="portal_use");
|
||||||
assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight");
|
assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight");
|
||||||
flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true;
|
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";
|
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]
|
return source[start:end]
|
||||||
|
|
||||||
preamble = r'''
|
preamble = r'''
|
||||||
|
#include "ClientlessTravelCost.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -95,8 +96,8 @@ struct Graph {
|
|||||||
} graph;
|
} graph;
|
||||||
auto sClientlessTravelGraphMgr=&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;
|
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;
|
static int recoveries=0;static uint32 recoveryEdge=0;
|
||||||
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32,std::string const&){++recoveries;return false;}
|
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32 edge,std::string const&){++recoveries;recoveryEdge=edge;return false;}
|
||||||
'''
|
'''
|
||||||
methods = '\n'.join(method(sig) for sig in (
|
methods = '\n'.join(method(sig) for sig in (
|
||||||
'bool StartGroundConnection(',
|
'bool StartGroundConnection(',
|
||||||
@@ -190,8 +191,10 @@ int main(){
|
|||||||
// Owner adapters can reject, wait, or shorten a recorded road without
|
// Owner adapters can reject, wait, or shorten a recorded road without
|
||||||
// losing the original index mapping or launching an unsafe spline.
|
// 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;
|
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(!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;};
|
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);
|
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;};
|
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'
|
cpp = Path(folder) / 'test.cpp'
|
||||||
binary = Path(folder) / 'test'
|
binary = Path(folder) / 'test'
|
||||||
cpp.write_text(preamble + structs + cls + doubles + methods + tests)
|
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)
|
subprocess.run([str(binary)], check=True)
|
||||||
@@ -5,6 +5,7 @@ root=Path(__file__).resolve().parents[2]
|
|||||||
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||||
a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(')
|
a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(')
|
||||||
code=r'''
|
code=r'''
|
||||||
|
#include "ClientlessTravelCost.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#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 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 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 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 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;};
|
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;}
|
std::unordered_map<unsigned,Record>_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;}
|
||||||
bool AdoptRoute(Player*,ClientlessAction const&,Record&);
|
bool AdoptRoute(Player*,ClientlessAction const&,Record&);
|
||||||
@@ -31,14 +32,16 @@ struct ClientlessActionExecutorMgr{
|
|||||||
int main(){
|
int main(){
|
||||||
ClientlessActionExecutorMgr mgr;Player p;ClientlessAction a;a.Name="travel_to_location";a.Parameters={{"id","destination"},{"route_source","29"}};
|
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}};
|
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;
|
lead.RouteStep=1;lead.FinalLocationId="destination";lead.FinalDatabaseId=77;lead.FinalX=90;lead.FinalLocalLegPending=true;
|
||||||
ClientlessActionExecutorMgr::Record peer;
|
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(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.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");
|
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.
|
// 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");
|
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.
|
// 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;
|
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";
|
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:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
p=Path(d);(p/'route.cpp').write_text(code)
|
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)
|
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.')
|
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.')
|
||||||
@@ -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.')
|
||||||
@@ -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.')
|
||||||
Reference in new issue
Block a user