Use journey-stable distance costs for clientless travel routes

This commit is contained in:
Nico committed 2026-11-02 06:26:58 +01:00
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.
+262
View File
@@ -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
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))
+36
View File
@@ -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)
{
if(action.Name=="travel_to_node" || action.Name=="travel_to_location")
{
auto goal=action.Parameters.find(action.Name=="travel_to_node"?"node":"id");
std::string key=action.Name+":"+(goal==action.Parameters.end()?std::string():goal->second);
// Inherit preferences even while the leader's route is still being
// published. The shared-route timeout must not roll a follower's own
// modal preference before a local catch-up plan.
ClientlessTravelCost::Profile const* inherited=nullptr;
auto requested=action.Parameters.find("route_source");
auto group=player->GetGroup();
if(requested!=action.Parameters.end() && group)
{
uint32 sourceGuid=uint32(std::strtoul(requested->second.c_str(),nullptr,10));
auto source=_records.find(sourceGuid);
if(group->GetLeaderGUID().GetCounter()==sourceGuid && sourceGuid!=player->GetGUID().GetCounter() &&
source!=_records.end() && source->second.HasJourneyCosts && source->second.JourneyCostKey==key)
inherited=&source->second.JourneyCosts;
}
if(inherited || !record.HasJourneyCosts || record.JourneyCostKey!=key || record.Status.State==ClientlessActionExecutionState::Completed)
{
record.JourneyCosts=inherited?*inherited:sClientlessTravelGraphMgr->NewJourneyCosts();
record.JourneyCostKey=key;record.HasJourneyCosts=true;
TC_LOG_INFO("server.adventurebot.navigation",
"TRAVEL_JOURNEY_COSTS guid=%u seed=%u goal='%s' ground=%.3f taxi=%.3f flight=%.3f ship=%.3f zeppelin=%.3f tram=%.3f portal=%.3f",
player->GetGUID().GetCounter(),record.JourneyCosts.Seed,key.c_str(),
record.JourneyCosts.Factors[0],record.JourneyCosts.Factors[1],record.JourneyCosts.Factors[2],
record.JourneyCosts.Factors[3],record.JourneyCosts.Factors[4],record.JourneyCosts.Factors[5],record.JourneyCosts.Factors[6]);
}
}
record.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
record.TransportActive = false;
record.FlightActive = false;
@@ -635,7 +664,7 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
player, destinationNode, startRadius, record.LocalBridgeRadius,
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys))
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts))
{
TC_LOG_WARN("server.adventurebot.navigation",
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_node' destination='%s' reason='%s'",
@@ -827,7 +856,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
player, anchorNode, startRadius, record.LocalBridgeRadius,
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal))
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts))
{
TC_LOG_WARN("server.adventurebot.navigation",
"C820_LOCATION_PLAN_FAILED location='%s' reason='%s'",
@@ -1854,8 +1883,11 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
size_t original=path.size();
if(!adapter->second(player,path))
{
player->StopMoving();record.Active=false;record.Status.State=ClientlessActionExecutionState::Failed;
record.Status.Detail="recorded group road rejected by safety adapter";return false;
player->StopMoving();
uint32 const failedEdge = record.RouteStep < record.Route.size()
? record.Route[record.RouteStep].EdgeDatabaseId : 0;
return RecoverTravel(player,characterGuid,record,failedEdge,
"recorded group road rejected by safety adapter");
}
if(path.size()<2)
{if(player->IsSplineEnabled())player->StopMoving();record.FormationWaiting=true;record.Status.State=ClientlessActionExecutionState::Moving;record.Status.Detail="waiting for leading formation row";return true;}
@@ -2335,7 +2367,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
player, anchorNode, radius, record.LocalBridgeRadius,
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal);
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts);
}
else
error = "no graph node exists on destination map";
@@ -2351,7 +2383,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys);
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, nullptr, &record.JourneyCosts);
if (planned)
TC_LOG_WARN("server.adventurebot.navigation",
@@ -3034,6 +3066,11 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
auto const snapshot=source->second;
auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node");
if(goal==action.Parameters.end() || goal->second!=(action.Name=="travel_to_location"?snapshot.FinalLocationId:snapshot.PlannedDestinationNode))return false;
// Catch-up planning and later recoveries share the leader's sampled modal
// costs. A follower must never choose a new transport preference here.
record.JourneyCosts=snapshot.JourneyCosts;
record.JourneyCostKey=snapshot.JourneyCostKey;
record.HasJourneyCosts=snapshot.HasJourneyCosts;
auto start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1);
auto anchor=route[start];
std::vector<ClientlessTravelRouteStep> joined;
@@ -3043,7 +3080,7 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
else
{
bool complete=false;std::string reached,error;
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error) || !complete)return false;
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error,nullptr,nullptr,nullptr,&record.JourneyCosts) || !complete)return false;
}
joined.insert(joined.end(),route.begin()+start+1,route.end());
record.Route=std::move(joined);record.RouteStep=0;
@@ -3053,6 +3090,6 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
record.HardDestination=snapshot.HardDestination;record.Active=true;
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()));
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u journeySeed=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()),record.JourneyCosts.Seed);
return StartRouteStep(player,player->GetGUID().GetCounter(),record);
}
@@ -57,6 +57,9 @@ public:
private:
struct Record
{
ClientlessTravelCost::Profile JourneyCosts;
std::string JourneyCostKey;
bool HasJourneyCosts = false;
ClientlessActionExecutionStatus Status;
bool Active = false;
std::shared_ptr<ClientlessAction> PendingShared;
@@ -22,8 +22,10 @@
#include "Player.h"
#include "Transport.h"
#include "TransportMgr.h"
#include "Spline.h"
#include "World.h"
#include "WorldSession.h"
#include <G3D/Quat.h>
#include <algorithm>
#include <cmath>
#include <limits>
@@ -326,6 +328,63 @@ ClientlessTransportStop const* ClientlessTransportMgr::GetStop(std::string const
if(!EnsureLoaded())return nullptr;
auto found=_stops.find(node);return found==_stops.end()?nullptr:&found->second;
}
float ClientlessTransportMgr::ConnectionDistance(std::string const& from,std::string const& to)
{
auto source=GetStop(from),target=GetStop(to);
if(!source || !target || source->Entry!=target->Entry)return 0;
struct Sample { uint32 Map;Position Pos;double Arc; };
std::vector<Sample> samples;
double totalLength=0;
if(auto native=sTransportMgr->GetTransportTemplate(source->Entry))
{
// Native splines already expose cumulative arclength at each stop.
// Sum their legs, never geographic jumps between maps/teleports.
for(auto const& leg:native->PathLegs)
{
if(!leg.Spline || leg.Spline->length()<=0)continue;
G3D::Vector3 point;
leg.Spline->evaluate_percent(0,point);
samples.push_back({leg.MapId,Position(point.x,point.y,point.z),totalLength});
for(auto const& segment:leg.Segments)
{
leg.Spline->evaluate_percent(float(segment.DistanceFromLegStartAtEnd/leg.Spline->length()),point);
samples.push_back({leg.MapId,Position(point.x,point.y,point.z),totalLength+segment.DistanceFromLegStartAtEnd});
}
totalLength+=leg.Spline->length();
}
}
else if(auto animation=sTransportMgr->GetTransportAnimInfo(source->Entry))
{
auto spawn=sObjectMgr->GetGameObjectData(source->Spawn);
if(!spawn)return 0;
QuaternionData parent;
if(auto addon=sObjectMgr->GetGameObjectAddon(source->Spawn))parent=addon->ParentRotation;
auto rotation=G3D::Quat(parent.x,parent.y,parent.z,parent.w).toRotationMatrix();
for(auto const& node:animation->Path)
{
auto const& p=node.second->Pos;
G3D::Vector3 world=G3D::Vector3(p.X,p.Y,p.Z)*rotation;
Position position(spawn->spawnPoint.GetPositionX()+world.x,spawn->spawnPoint.GetPositionY()+world.y,spawn->spawnPoint.GetPositionZ()+world.z);
if(!samples.empty())totalLength+=samples.back().Pos.GetExactDist(position);
samples.push_back({source->Map,position,totalLength});
}
if(samples.size()>1)totalLength+=samples.back().Pos.GetExactDist(samples.front().Pos);
}
if(samples.size()<2 || totalLength<=0)return 0;
auto nearest=[&](ClientlessTransportStop const& stop)
{
size_t result=samples.size();float best=std::numeric_limits<float>::infinity();
for(size_t i=0;i<samples.size();++i)if(samples[i].Map==stop.Map)
{float distance=samples[i].Pos.GetExactDist(stop.Dock);if(distance<best){best=distance;result=i;}}
return result;
};
auto begin=nearest(*source),end=nearest(*target);
if(begin>=samples.size() || end>=samples.size() || begin==end)return 0;
double length=samples[end].Arc-samples[begin].Arc;
if(length<0)length+=totalLength;
return float(length);
}
bool ClientlessTransportMgr::ObserveJourney(Player* player,uint32 lastMap,Position const& ground,std::string& from,std::string& to)
{
from.clear();to.clear();if(!player->GetTransport() || !EnsureLoaded())return false;
@@ -50,6 +50,7 @@ public:
static ClientlessTransportMgr* instance();
bool Reload();
bool HasConnection(std::string const& from, std::string const& to);
float ConnectionDistance(std::string const& from, std::string const& to);
bool ObserveJourney(Player* passenger, uint32 lastMap, Position const& lastGround, std::string& from, std::string& to);
bool ArrivalNode(Player* player, std::string const& from, std::string& to);
ClientlessTransportStop const* GetStop(std::string const& node);
@@ -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 "DatabaseEnv.h"
#include "DBCStores.h"
#include "Config.h"
#include "Random.h"
#include "Log.h"
#include "Player.h"
#include "ClientlessTravelMgr.h"
@@ -317,6 +320,7 @@ bool ClientlessTravelGraphMgr::Reload()
while (pointResult->NextRow());
}
RebuildEdgeDistances();
_loaded = true;
TC_LOG_INFO("server.adventurebot",
@@ -326,6 +330,61 @@ bool ClientlessTravelGraphMgr::Reload()
return !_nodes.empty();
}
ClientlessTravelCost::Profile ClientlessTravelGraphMgr::NewJourneyCosts() const
{
ClientlessTravelCost::Profile base;
for(unsigned mode=0;mode<ClientlessTravelCost::Count;++mode)
base.Factors[mode]=sConfigMgr->GetFloatDefault(
std::string("Clientless.Travel.CostMultiplier.")+ClientlessTravelCost::Name(ClientlessTravelCost::Mode(mode)),1.0f);
return ClientlessTravelCost::Sample(base,urand(1,0xffffffffu),
sConfigMgr->GetFloatDefault("Clientless.Travel.CostVariation",0.20f));
}
void ClientlessTravelGraphMgr::RebuildEdgeDistances()
{
for(auto& outgoing:_edges)for(auto& edge:outgoing.second)
{
auto const& from=_nodes.at(edge.FromNode);auto const& to=_nodes.at(edge.ToNode);
float length=0;
auto segment=[&](uint32 map,float x,float y,float z,uint32 otherMap,float a,float b,float c)
{if(map==otherMap)length+=std::hypot(std::hypot(x-a,y-b),z-c);};
if(edge.Type=="portal" || edge.Type=="portal_use")length=1;
else if(edge.Type=="taxi")
{
if(edge.FromNode.rfind("taxi_",0)==0 && edge.ToNode.rfind("taxi_",0)==0)
{
uint32 path=0,fare=0;
sObjectMgr->GetTaxiPath(uint32(std::strtoul(edge.FromNode.c_str()+5,nullptr,10)),
uint32(std::strtoul(edge.ToNode.c_str()+5,nullptr,10)),path,fare);
if(path<sTaxiPathNodesByPath.size())
{
auto const& nodes=sTaxiPathNodesByPath[path];
for(size_t i=1;i<nodes.size();++i)
segment(nodes[i-1]->ContinentID,nodes[i-1]->Loc.X,nodes[i-1]->Loc.Y,nodes[i-1]->Loc.Z,
nodes[i]->ContinentID,nodes[i]->Loc.X,nodes[i]->Loc.Y,nodes[i]->Loc.Z);
}
}
}
else if(ClientlessTransportMgr::IsTransportEdge(edge.Type))
length=sClientlessTransportMgr->ConnectionDistance(edge.FromNode,edge.ToNode);
else if(auto points=GetEdgePoints(edge.DatabaseId);points && !points->empty())
{
auto const& first=edge.Reverse?points->back():points->front();
auto const& last=edge.Reverse?points->front():points->back();
segment(from.MapId,from.X,from.Y,from.Z,first.MapId,first.X,first.Y,first.Z);
for(size_t i=1;i<points->size();++i)
segment((*points)[i-1].MapId,(*points)[i-1].X,(*points)[i-1].Y,(*points)[i-1].Z,
(*points)[i].MapId,(*points)[i].X,(*points)[i].Y,(*points)[i].Z);
segment(last.MapId,last.X,last.Y,last.Z,to.MapId,to.X,to.Y,to.Z);
}
if(length<=0 && from.MapId==to.MapId)
segment(from.MapId,from.X,from.Y,from.Z,to.MapId,to.X,to.Y,to.Z);
edge.Cost=std::isfinite(length)&&length>0?length:std::numeric_limits<float>::infinity();
TC_LOG_DEBUG("server.adventurebot.navigation","TRAVEL_EDGE_DISTANCE edge=%u type='%s' reverse=%u distance=%.2f",
edge.DatabaseId,edge.Type.c_str(),edge.Reverse?1u:0u,edge.Cost);
}
}
std::string ClientlessTravelGraphMgr::FindNearestStartNode(
Player const* player,
float maxDistance) const
@@ -364,8 +423,11 @@ bool ClientlessTravelGraphMgr::PlanRoute(
float startRadius,
std::vector<ClientlessTravelRouteStep>& route,
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds)
std::unordered_set<uint32> const* blockedEdgeIds,
ClientlessTravelCost::Profile const* journeyCosts)
{
ClientlessTravelCost::Profile const defaults;
auto const& costs=journeyCosts?*journeyCosts:defaults;
route.clear();
error.clear();
@@ -448,7 +510,7 @@ bool ClientlessTravelGraphMgr::PlanRoute(
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
continue;
float const candidate = current.Cost + edge.Cost;
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
if (candidate >= distance[edge.ToNode])
continue;
@@ -575,7 +637,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds,
std::unordered_set<std::string> const* blockedBridgeKeys,
ClientlessTravelLocationGoal const* locationGoal)
ClientlessTravelLocationGoal const* locationGoal,
ClientlessTravelCost::Profile const* journeyCosts)
{
struct Trace
{
@@ -639,6 +702,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
{ error="destination location denied by area catalog"; return false; }
}
ClientlessTravelCost::Profile const defaults;
auto const& costs=journeyCosts?*journeyCosts:defaults;
auto groundCost=[&](float length){return costs.Cost(ClientlessTravelCost::Ground,length);};
auto flightDistanceCost=[&](float length){return costs.Cost(ClientlessTravelCost::Flight,length);};
auto edgeCost=[&](ClientlessTravelEdge const& edge){return costs.Cost(edge.Type,edge.Cost);};
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
// Preserve part of the global budget for nodes reached later in the
@@ -666,12 +734,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
return std::numeric_limits<float>::infinity();
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
if(access && std::isfinite(result))
if(std::isfinite(result))
{
float distance=a.GetExactDist(&b);
if(distance<20)result=std::numeric_limits<float>::infinity();
else result=(sClientlessFlightMgr->Preferred(player,map,a) || sClientlessFlightMgr->Preferred(player,map,b))?
40+distance*0.4f:180+distance*1.5f;
float length=a.GetExactDist(&b);
result=access && length<20?std::numeric_limits<float>::infinity():flightDistanceCost(length);
}
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
flightEstimates.emplace(estimateKey, result);
@@ -687,45 +753,38 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
bridgeRadius = MaxVirtualNodeBridge;
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
// The overwhelmingly common free-flight case is a same-map Location. Do
// not construct a macro graph first: Estimate already enforces licenses,
// mount flags, area policy, distance and the complete catalog segment.
// Native departure/landing validation still happens in FlightMgr::Start.
if (locationGoal && locationGoal->MapId == player->GetMapId())
float directFlightCost=std::numeric_limits<float>::infinity();
auto directFlightRoute=[&]()
{
ClientlessTravelNode departureNode;
departureNode.Key = "flight_departure";
departureNode.MapId = player->GetMapId();
departureNode.X = player->GetPositionX();
departureNode.Y = player->GetPositionY();
departureNode.Z = player->GetPositionZ();
float const directFlight = flightCost(departureNode, locationGoal->MapId,
locationGoal->X, locationGoal->Y, locationGoal->Z);
if (std::isfinite(directFlight))
{
ClientlessTravelRouteStep departure;
departure.NodeKey = "flight_departure";
departure.NodeType = "ground";
departure.EdgeType = "start";
departure.MapId = player->GetMapId();
departure.X = player->GetPositionX();
departure.Y = player->GetPositionY();
departure.Z = player->GetPositionZ();
route.push_back(std::move(departure));
ClientlessTravelRouteStep departure;
departure.NodeKey = "flight_departure";
departure.NodeType = "ground";
departure.EdgeType = "start";
departure.MapId = player->GetMapId();
departure.X = player->GetPositionX();
departure.Y = player->GetPositionY();
departure.Z = player->GetPositionZ();
route.push_back(std::move(departure));
ClientlessTravelRouteStep flight;
flight.NodeKey = "flight_location";
flight.NodeType = "location";
flight.EdgeType = "free_flight";
flight.MapId = locationGoal->MapId;
flight.X = locationGoal->X;
flight.Y = locationGoal->Y;
flight.Z = locationGoal->Z;
route.push_back(std::move(flight));
complete = true;
reachedNode = "flight_location";
return true;
}
ClientlessTravelRouteStep flight;
flight.NodeKey = "flight_location";
flight.NodeType = "location";
flight.EdgeType = "free_flight";
flight.MapId = locationGoal->MapId;
flight.X = locationGoal->X;
flight.Y = locationGoal->Y;
flight.Z = locationGoal->Z;
route.push_back(std::move(flight));
complete = true;
reachedNode = "flight_location";
return true;
};
if(locationGoal && locationGoal->MapId==player->GetMapId())
{
ClientlessTravelNode departure;
departure.Key="flight_departure";departure.MapId=player->GetMapId();
departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
directFlightCost=flightCost(departure,locationGoal->MapId,locationGoal->X,locationGoal->Y,locationGoal->Z);
}
struct State
@@ -807,6 +866,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::unordered_set<std::string> flightRoots;
uint32 startCount = 0;
// Unreachable nearby pavement/indoor anchors must not consume the entire
// flight budget before graph search can consider flight-only connections.
std::uint64_t startFlightProbes = 0;
constexpr std::uint64_t MaxStartFlightProbes = 4;
for (auto const& pair : _nodes)
{
ClientlessTravelNode const& node = pair.second;
@@ -820,7 +883,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
if (localDistance > startRadius)
continue;
float startCost = localDistance;
float startCost = groundCost(localDistance);
// VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node.
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key))
@@ -842,9 +905,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// the recovery/re-entry mode for nodes that local navigation cannot
// reach, not a shortcut racing every nearby pavement node.
float air = std::numeric_limits<float>::infinity();
if (!reachable && !(blockedBridgeKeys &&
if (!reachable && startFlightProbes < MaxStartFlightProbes && !(blockedBridgeKeys &&
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
{
++startFlightProbes;
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
}
if(std::isfinite(air) && !reachable)
{flightRoots.insert(node.Key);startCost=air;}
else
@@ -870,7 +936,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
continue;
}
startCost = entryNav.PathLength;
startCost = groundCost(entryNav.PathLength);
if (!reachable)
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
}
@@ -988,9 +1054,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
continue;
uint32 const startIndex = candidate.Edge.Reverse
? candidate.Count - 1 - candidate.Index : candidate.Index;
float newCost = (alreadyThere ? 0.0f : nav.PathLength) +
float newCost = (alreadyThere ? 0.0f : groundCost(nav.PathLength)) +
roadSectionCost(candidate.Edge.DatabaseId, candidate.Edge.Reverse,
startIndex, candidate.Count - 1, candidate.Edge.Cost);
startIndex, candidate.Count - 1, edgeCost(candidate.Edge));
auto old = distance.find(key);
if (old != distance.end() && old->second <= newCost)
continue;
@@ -1000,7 +1066,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
previous.erase(key);
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
candidate.Edge.Reverse, candidate.Point,
alreadyThere ? 0.0f : nav.PathLength };
alreadyThere ? 0.0f : groundCost(nav.PathLength) };
open.push({ newCost, state });
++accepted;
}
@@ -1011,10 +1077,92 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (!startCount && !accepted)
{
if(std::isfinite(directFlightCost))return directFlightRoute();
error = "no reachable travel node or recorded ground edge near current position";
return false;
}
// Reserve a bounded part of the flight budget for complete local portal
// connections before speculative road exits spend it. Nearby ground roots
// are entry options, not a reason to overlook flight-only portal islands.
if (flightEnabled && !locationGoal)
{
struct PortalConnection
{
float Rank;
std::string Root;
ClientlessTravelEdge Edge;
};
std::vector<PortalConnection> connections;
auto const& goal = destinationItr->second;
for (auto const& outgoing : _edges)
for (auto const& edge : outgoing.second)
{
if (edge.Type != "portal" && edge.Type != "portal_use") continue;
if (!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge) ||
(blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))) continue;
auto const& source = _nodes.at(edge.FromNode);
auto const& target = _nodes.at(edge.ToNode);
if (target.MapId != goal.MapId) continue;
float dx=goal.X-target.X,dy=goal.Y-target.Y,dz=goal.Z-target.Z;
float finish = flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
for (auto const& root : states)
{
auto const& origin = _nodes.at(root.second.Node);
if (origin.MapId != source.MapId || origin.Key == source.Key) continue;
auto originEdges = _edges.find(origin.Key);
if (originEdges != _edges.end() && std::any_of(originEdges->second.begin(),originEdges->second.end(),
[](ClientlessTravelEdge const& e){return e.Type=="portal" || e.Type=="portal_use";})) continue;
dx=source.X-origin.X;dy=source.Y-origin.Y;dz=source.Z-origin.Z;
float direct=std::sqrt(dx*dx+dy*dy+dz*dz);
if (direct > 1500.0f) continue;
connections.push_back({distance.at(root.first)+flightDistanceCost(direct)+edgeCost(edge)+finish,root.first,edge});
}
}
std::sort(connections.begin(),connections.end(),[](PortalConnection const& a,PortalConnection const& b)
{
if(a.Rank!=b.Rank)return a.Rank<b.Rank;
if(a.Edge.DatabaseId!=b.Edge.DatabaseId)return a.Edge.DatabaseId<b.Edge.DatabaseId;
return a.Root<b.Root;
});
auto const callsBefore = trace.FlightCalls;
unsigned attempts=0;
for (auto const& candidate : connections)
{
if (++attempts>8 || trace.FlightCalls-callsBefore>=6) break;
auto const edge=candidate.Edge;
auto const& origin=_nodes.at(states.at(candidate.Root).Node);
auto const& source=_nodes.at(edge.FromNode);
auto const& target=_nodes.at(edge.ToNode);
if (blockedBridgeKeys && (blockedBridgeKeys->count("flight:"+origin.Key+"->"+source.Key) ||
blockedBridgeKeys->count("flight:"+target.Key+"->"+goal.Key))) continue;
float entry=flightCost(origin,source.MapId,source.X,source.Y,source.Z,true);
if (!std::isfinite(entry)) continue;
float finish=target.Key==goal.Key?0.0f:flightCost(target,goal.MapId,goal.X,goal.Y,goal.Z,true);
if (!std::isfinite(finish)) continue;
auto relax=[&](std::string const& from,ClientlessTravelNode const& to,
std::string const& type,uint32 id,bool reverse,float cost)
{
State next{to.Key,false};auto key=stateKey(next);
float score=distance.at(from)+cost;
auto old=distance.find(key);
if(old==distance.end() || score<old->second)
{
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[from]+1;
previous[key]={from,states.at(from).Node,type,id,reverse};
open.push({score,next});
}
return key;
};
auto sourceKey=relax(candidate.Root,source,"free_flight",0,false,entry);
auto targetKey=relax(sourceKey,target,edge.Type,edge.DatabaseId,edge.Reverse,edgeCost(edge));
if(target.Key!=goal.Key)relax(targetKey,goal,"free_flight",0,false,finish);
TC_LOG_INFO("server.adventurebot.navigation",
"PORTAL_FLIGHT_CONNECTION from='%s' source='%s' target='%s' goal='%s' entry=%.1f finish=%.1f",
origin.Key.c_str(),source.Key.c_str(),target.Key.c_str(),goal.Key.c_str(),entry,finish);
}
}
// C8.20: virtual node bridges are equal-cost ground alternatives and
// participate even when stored graph routes also exist.
bool const repairRequired = true;
@@ -1163,13 +1311,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
candidate.EdgeId = edge.DatabaseId;
candidate.Reverse = edge.Reverse;
candidate.EndIndex = i;
candidate.RoadCost = edge.Cost * (travelled / totalLength);
candidate.RoadCost = edgeCost(edge) * (travelled / totalLength);
candidate.DistanceSq = dsq;
candidate.Point = point;
bool const groundExitExhausted = blockedBridgeKeys &&
blockedBridgeKeys->count("ground_exit_edge:" + identity);
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
? groundCost(std::sqrt(dsq)) : std::numeric_limits<float>::infinity();
// A recorded edge can contain hundreds of samples. Calling
// FlightMgr::Estimate for every sample made the synchronous
@@ -1186,10 +1334,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
{
float const direct = std::sqrt(dsq);
// Forty plus 0.4 * distance is the cheapest possible
// free-flight cost. It is an admissible ordering key;
// rejected area segments simply advance to the next item.
rankedFlightExits.push_back({ candidate.RoadCost + 40.0f + direct * 0.4f,
// Use this journey's flight factor for both candidate
// ordering and execution cost, including expensive modes.
rankedFlightExits.push_back({ candidate.RoadCost + flightDistanceCost(direct),
std::move(candidate) });
}
}
@@ -1245,7 +1392,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
uint32 StartIndex = 0;
ClientlessTravelEdgePoint EntryPoint;
} bestExit;
float bestGoalCost = std::numeric_limits<float>::infinity();
float bestGoalCost = directFlightCost;
std::string bestGoalState;
bool bestFinalFlight = false;
// Every permitted node is a potential way back into the persistent graph.
@@ -1261,7 +1408,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (node.MapId != destination.MapId)
return std::numeric_limits<float>::infinity();
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
return 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);
auto edges = _edges.find(candidate.Key);
@@ -1275,7 +1422,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
auto next = _nodes.find(edge.ToNode);
if (next == _nodes.end())
continue;
best = std::min(best, edge.Cost + lowerBoundToGoal(next->second));
best = std::min(best, edgeCost(edge) + lowerBoundToGoal(next->second));
}
return best;
};
@@ -1308,7 +1455,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (fromNodeItr != _edges.end())
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
fullEdgeCost = edge.Cost;
fullEdgeCost = edgeCost(edge);
if (fullEdgeCost <= 0.0f)
continue;
float const partialCost = roadSectionCost(exit.EdgeId,
@@ -1371,7 +1518,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::string const finishKey = "finish:" + candidateNode.Key;
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
? std::numeric_limits<float>::infinity() :
std::min(direct<=locationAccessRadius ? direct : std::numeric_limits<float>::infinity(),air);
std::min(direct<=locationAccessRadius ? groundCost(direct) : std::numeric_limits<float>::infinity(),air);
if (current.Cost + finalCost < bestGoalCost)
{
bestGoalCost = current.Cost + finalCost;
@@ -1420,7 +1567,17 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
[](ClientlessTravelEdge const& edge)
{ 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.
auto edgeItr = _edges.find(current.Value.Node);
@@ -1438,7 +1595,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
State next{ edge.ToNode, false };
std::string const nextKey = stateKey(next);
float const candidate = current.Cost + edge.Cost;
float const candidate = current.Cost + edgeCost(edge);
auto old = distance.find(nextKey);
if (old != distance.end() && candidate >= old->second)
@@ -1474,7 +1631,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// Even a preferred legal flight costs at least this much.
// Skip expensive terrain sampling when it cannot improve the
// route already found; this preserves the full graph search.
float lowerBound=40.0f+std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
float lowerBound=flightDistanceCost(std::sqrt(dx*dx+dy*dy+dz*dz));
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
@@ -1637,9 +1794,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (exit.EdgeId != entry.Edge->DatabaseId ||
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
continue;
float const score = current.Cost + probe.PathLength + exit.LocalCost +
float const score = current.Cost + groundCost(probe.PathLength) + exit.LocalCost +
roadSectionCost(exit.EdgeId, exit.Reverse, entryIndex,
exit.EndIndex, entry.Edge->Cost);
exit.EndIndex, edgeCost(*entry.Edge));
if (score >= bestGoalCost)
continue;
bestGoalCost = score;
@@ -1651,9 +1808,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::string const nextKey = stateKey(next);
uint32 const begin = entry.Edge->Reverse
? entry.PointCount - 1 - entry.Index : entry.Index;
float const cost = current.Cost + probe.PathLength +
float const cost = current.Cost + groundCost(probe.PathLength) +
roadSectionCost(entry.Edge->DatabaseId, entry.Edge->Reverse,
begin, entry.PointCount - 1, entry.Edge->Cost);
begin, entry.PointCount - 1, edgeCost(*entry.Edge));
auto old = distance.find(nextKey);
if (old != distance.end() && cost >= old->second)
continue;
@@ -1767,7 +1924,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
// ground connections, not penalized fallback routes.
float const candidate = current.Cost + measuredLength;
float const candidate = current.Cost + groundCost(measuredLength);
auto old = distance.find(nextKey);
if (old != distance.end() && candidate >= old->second)
continue;
@@ -1782,6 +1939,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
}
if(bestGoalState.empty() && std::isfinite(directFlightCost))return directFlightRoute();
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
!bestGoalState.empty())
previous.erase(bestGoalState);
@@ -2132,8 +2291,11 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
std::vector<ClientlessTravelRouteStep>& route,
std::string& anchorNode,
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds)
std::unordered_set<uint32> const* blockedEdgeIds,
ClientlessTravelCost::Profile const* journeyCosts)
{
ClientlessTravelCost::Profile const defaults;
auto const& costs=journeyCosts?*journeyCosts:defaults;
route.clear();
anchorNode.clear();
error.clear();
@@ -2202,8 +2364,8 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
if (localStartDistance > startRadius)
continue;
distance[node.Key] = localStartDistance;
open.push({ localStartDistance, node.Key });
distance[node.Key] = costs.Cost(ClientlessTravelCost::Ground,localStartDistance);
open.push({ distance[node.Key], node.Key });
++startCandidateCount;
}
@@ -2233,7 +2395,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
continue;
float const candidate = current.Cost + edge.Cost;
float const candidate = current.Cost + costs.Cost(edge.Type,edge.Cost);
if (candidate >= distance[edge.ToNode])
continue;
@@ -12,6 +12,7 @@
#include "Common.h"
#include "ClientlessGraphProbeCache.h"
#include "ClientlessTravelCost.h"
#include <string>
#include <unordered_map>
@@ -114,13 +115,16 @@ public:
bool Reload();
bool EnsureLoaded();
ClientlessTravelCost::Profile NewJourneyCosts() const;
bool PlanRoute(
Player const* player,
std::string const& destinationNode,
float startRadius,
std::vector<ClientlessTravelRouteStep>& route,
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
bool PlanRouteToPosition(
Player const* player,
@@ -133,7 +137,8 @@ public:
std::vector<ClientlessTravelRouteStep>& route,
std::string& anchorNode,
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
// C8.13b: transient local bridges connect nearby graph islands for one
// route plan. Bridges are never persisted and may never follow another
@@ -151,7 +156,8 @@ public:
std::string& error,
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
std::unordered_set<std::string> const* blockedBridgeKeys = nullptr,
ClientlessTravelLocationGoal const* locationGoal = nullptr);
ClientlessTravelLocationGoal const* locationGoal = nullptr,
ClientlessTravelCost::Profile const* journeyCosts = nullptr);
bool FindNearestNodeToPosition(
uint32 mapId,
@@ -182,6 +188,7 @@ public:
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
private:
void RebuildEdgeDistances();
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
ClientlessGraphProbeCache _positionProbes;
@@ -4396,3 +4396,18 @@ Clientless.Browser.Battleground.QueueEnabled = 1
Clientless.Browser.Battleground.JoinEnabled = 0
# Explicit isolated test override. Does not override disabled Queue/Join switches.
Clientless.Browser.AllowBotOnlyTests = 0
# Clientless travel route scoring: measured path length (yards), not ticket
# price or the legacy travel_edge.cost column. Higher factors discourage a
# mode. Portal transitions have a unit base cost (no fictitious map distance).
# Variation draws independent positive mode factors once per journey; recovery
# retains the draw and group members inherit their leader's profile and route.
Clientless.Travel.CostMultiplier.Ground = 1.0
Clientless.Travel.CostMultiplier.Taxi = 1.0
Clientless.Travel.CostMultiplier.Flight = 1.0
Clientless.Travel.CostMultiplier.Ship = 1.0
Clientless.Travel.CostMultiplier.Zeppelin = 1.0
Clientless.Travel.CostMultiplier.Tram = 1.0
Clientless.Travel.CostMultiplier.Portal = 1.0
# 0 disables variation; 0.20 means a per-mode range of 80%..120% per journey.
Clientless.Travel.CostVariation = 0.20
+66 -2
View File
@@ -11,6 +11,7 @@ preamble=r'''
#include "ClientlessGraphProbeCache.h"
#include <chrono>
#include <map>
#include "ClientlessTravelCost.h"
#include <algorithm>
#include <cassert>
#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;
auto sClientlessTravelMgr=&travel;
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;
void BeginPlanning(Player const* = nullptr){}
bool Enabled(Player const*){return enabled;}
bool Preferred(Player const*,uint32,Position const&){return false;}
float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){
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;
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[3].NodeKey=="B"&&route[3].EdgeType=="free_flight");
flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true;
// Replanning AT an interactive portal must traverse it, even when flying
// directly from the indoor source looks cheaper to the estimate.
ClientlessTravelGraphMgr rootPortal;node(rootPortal,"P",500);node(rootPortal,"Q",-1000);node(rootPortal,"B",2000);
edge(rootPortal,"P","Q",91,1,false,"portal_use");
Player atPortal;atPortal.x=500;travel.complete=false;route.clear();
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why)&&done);
assert(route.size()>=3&&route[1].EdgeType=="portal_use"&&route[1].NodeKey=="Q");
// A blocked portal does not trap the actor: its recorded escape road remains.
edge(rootPortal,"P","B",92,400,false,"ground");
std::unordered_set<uint32> failedPortal{91};flight.enabled=false;route.clear();
assert(rootPortal.PlanAdaptiveRoute(&atPortal,"B",1,200,route,done,endKey,why,&failedPortal)&&done);
assert(route.back().EdgeDatabaseId==92);
// Many nearby inaccessible anchors must leave budget for an isolated portal
// outside the local-entry radius and the flight from its destination.
flight.enabled=true;flight.rejectNearbyTargets=true;
flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000;
ClientlessTravelGraphMgr crowded;node(crowded,"A",0);node(crowded,"P",500);node(crowded,"Q",1500);node(crowded,"B",2000);
edge(crowded,"P","Q",93,1,false,"portal_use");
for(unsigned i=1;i<=40;++i)node(crowded,"indoor"+std::to_string(i),float(i*10));
route.clear();assert(crowded.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)&&done);
bool usedIsolatedPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==93)usedIsolatedPortal=true;
assert(usedIsolatedPortal);
flight.rejectNearbyTargets=false;flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;
// Speculative road exits and an unusable initial departure must not starve a
// nearby flight-only portal. The cheap complete macro route must beat the
// long native fallback, not merely appear as a considered candidate.
flight.enabled=true;flight.closedFromX=0;travel.complete=true;
ClientlessTravelGraphMgr portalBudget;node(portalBudget,"A",0);node(portalBudget,"C",100);
node(portalBudget,"P",500);node(portalBudget,"Q",1500);node(portalBudget,"B",2000);
edge(portalBudget,"P","Q",94,1,false,"portal_use");
edge(portalBudget,"A","B",95,10000,false,"taxi");
for(unsigned i=0;i<4;++i){
auto key="remote"+std::to_string(i);node(portalBudget,key,3000+float(i*100));
edge(portalBudget,"A",key,100+i,10000);
for(float x:{0.f,1000.f,2000.f,3000.f}){ClientlessTravelEdgePoint pt;pt.X=x;pt.Y=float(i*20);portalBudget._edgePoints[100+i].push_back(pt);}
}
route.clear();assert(portalBudget.PlanAdaptiveRoute(&start,"B",150,200,route,done,endKey,why)&&done);
bool budgetPortal=false;for(auto const& step:route)if(step.EdgeDatabaseId==94)budgetPortal=true;
assert(budgetPortal);flight.closedFromX=1e9;
// Apply modal factors to ALL persisted edges and keep a profile stable over
// replanning. Independent journeys may choose different near-equal modes.
flight.enabled=false;travel.complete=true;PathGenerator::available=false;
ClientlessTravelGraphMgr modal;node(modal,"A",0);node(modal,"B",1000);
edge(modal,"A","B",200,1000,false,"ground");edge(modal,"A","B",201,1050,false,"taxi");
bool sawGround=false,sawTaxi=false;
for(unsigned seed=1;seed<=100;++seed){
auto profile=ClientlessTravelCost::Sample({},seed,.2f);
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
auto picked=route.back().EdgeDatabaseId;sawGround|=picked==200;sawTaxi|=picked==201;
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,nullptr,&profile)&&done);
assert(route.back().EdgeDatabaseId==picked);
}assert(sawGround&&sawTaxi);
// A same-map flight is a competing route, never an unconditional override.
flight.enabled=true;ClientlessTravelCost::Profile expensiveFlight;expensiveFlight.Factors[ClientlessTravelCost::Flight]=100;
ClientlessTravelLocationGoal sameMap;sameMap.X=1000;sameMap.AnchorRadius=5;
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
assert(route.back().NodeKey!="flight_location");
expensiveFlight.Factors[ClientlessTravelCost::Flight]=.25f;
route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done);
assert(route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight");
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
}
'''
@@ -18,6 +18,7 @@ def method(signature):
return source[start:end]
preamble = r'''
#include "ClientlessTravelCost.h"
#include <algorithm>
#include <cassert>
#include <cmath>
@@ -95,8 +96,8 @@ struct Graph {
} graph;
auto sClientlessTravelGraphMgr=&graph;
struct Areas{bool AllowsNode(Player*,ClientlessTravelNode const&){return true;}bool AllowsSegment(Player*,uint32,Position const&,Position const&){return true;}}areas;auto sAdventureAreaMgr=&areas;
static int recoveries=0;
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32,std::string const&){++recoveries;return false;}
static int recoveries=0;static uint32 recoveryEdge=0;
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32 edge,std::string const&){++recoveries;recoveryEdge=edge;return false;}
'''
methods = '\n'.join(method(sig) for sig in (
'bool StartGroundConnection(',
@@ -190,8 +191,10 @@ int main(){
// Owner adapters can reject, wait, or shorten a recorded road without
// losing the original index mapping or launching an unsafe spline.
Record adapted;adapted.Active=true;adapted.EdgePath.resize(5);for(unsigned i=0;i<5;++i)adapted.EdgePath[i].X=150+i*10;p.x=140;
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&){return false;};before=p.motion.launches;
adapted.Route.resize(1);adapted.Route[0].EdgeDatabaseId=1885;
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&){return false;};before=p.motion.launches;auto previousRecoveries=recoveries;
assert(!mgr.StartRecordedEdgePath(&p,26,adapted) && p.motion.launches==before);
assert(recoveries==previousRecoveries+1 && recoveryEdge==1885);
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(1);return true;};
assert(mgr.StartRecordedEdgePath(&p,26,adapted) && adapted.FormationWaiting && p.motion.launches==before);
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(3);return true;};
@@ -203,5 +206,5 @@ with tempfile.TemporaryDirectory(prefix='vcore-ground-exit-') as folder:
cpp = Path(folder) / 'test.cpp'
binary = Path(folder) / 'test'
cpp.write_text(preamble + structs + cls + doubles + methods + tests)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-I'+str(root/'src/server/game/World'),'-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
+7 -4
View File
@@ -5,6 +5,7 @@ root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(')
code=r'''
#include "ClientlessTravelCost.h"
#include <algorithm>
#include <cassert>
#include <cmath>
@@ -20,9 +21,9 @@ struct Group{Guid leader{29};Guid GetLeaderGUID(){return leader;}}group;
struct Player{bool grouped=true;unsigned map=0;Guid GetGUID(){return {31};}Group*GetGroup(){return grouped?&group:nullptr;}unsigned GetMapId(){return map;}float GetExactDist(Position const&p){return std::hypot(p.x,p.y);}};
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
struct ClientlessTravelRouteStep{std::string NodeKey,EdgeType;unsigned EdgeDatabaseId=0,MapId=0;float X=0,Y=0,Z=0;};
struct Graph{bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector<ClientlessTravelRouteStep>&r,bool&complete,std::string&,std::string&){complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct Graph{ClientlessTravelCost::Profile observed;bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector<ClientlessTravelRouteStep>&r,bool&complete,std::string&,std::string&,void const* =nullptr,void const* =nullptr,void const* =nullptr,ClientlessTravelCost::Profile const*costs=nullptr){if(costs)observed=*costs;complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct ClientlessActionExecutorMgr{
struct Record{std::vector<ClientlessTravelRouteStep>Route;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false;
struct Record{ClientlessTravelCost::Profile JourneyCosts;std::string JourneyCostKey;bool HasJourneyCosts=false;std::vector<ClientlessTravelRouteStep>Route;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false;
unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode;};
std::unordered_map<unsigned,Record>_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;}
bool AdoptRoute(Player*,ClientlessAction const&,Record&);
@@ -31,14 +32,16 @@ struct ClientlessActionExecutorMgr{
int main(){
ClientlessActionExecutorMgr mgr;Player p;ClientlessAction a;a.Name="travel_to_location";a.Parameters={{"id","destination"},{"route_source","29"}};
auto& lead=mgr._records[29];lead.Route={{"start","start",0,0,0,0,0},{"taxi_end","taxi",18,0,30,0,0},{"flight_end","free_flight",0,0,80,0,0}};
lead.JourneyCosts=ClientlessTravelCost::Sample({},12345,0.2f);lead.JourneyCostKey="leader-trip";lead.HasJourneyCosts=true;
lead.RouteStep=1;lead.FinalLocationId="destination";lead.FinalDatabaseId=77;lead.FinalX=90;lead.FinalLocalLegPending=true;
ClientlessActionExecutorMgr::Record peer;
assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==3&&peer.Route[1].EdgeType=="taxi"&&peer.Route[1].EdgeDatabaseId==18&&peer.Route[2].EdgeType=="free_flight");
assert(peer.FinalLocationId=="destination"&&peer.FinalLocalLegPending&&peer.FinalX==90&&peer.FinalDatabaseId==77);
assert(peer.HasJourneyCosts&&peer.JourneyCosts.Seed==12345&&peer.JourneyCostKey=="leader-trip"&&peer.JourneyCosts.Factors==lead.JourneyCosts.Factors);
assert(peer.RouteStep==0&&peer.Active&&lead.RouteStep==1);peer.Route[1].NodeKey="changed";assert(lead.Route[1].NodeKey=="taxi_end");
// Late arrival joins the active route at its previous reached node.
lead.RouteStep=2;assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==2&&peer.Route[0].NodeKey=="taxi_end"&&peer.Route[0].EdgeType=="start"&&peer.Route[1].EdgeType=="free_flight");
p.map=1;assert(mgr.AdoptRoute(&p,a,peer));graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0;
p.map=1;assert(mgr.AdoptRoute(&p,a,peer));assert(graph.observed.Factors==lead.JourneyCosts.Factors&&graph.observed.Seed==12345);graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0;
// No cross-group route copying and no stale destination reuse.
group.leader={32};assert(!mgr.AdoptRoute(&p,a,peer));group.leader={29};p.grouped=false;assert(!mgr.AdoptRoute(&p,a,peer));p.grouped=true;
a.Parameters["id"]="different";assert(!mgr.AdoptRoute(&p,a,peer));a.Parameters["id"]="destination";
@@ -47,6 +50,6 @@ int main(){
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'route.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'route.cpp'),'-o',str(p/'route')],check=True)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'route.cpp'),'-o',str(p/'route')],check=True)
subprocess.run([str(p/'route')],check=True)
print('Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.')
@@ -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.')
+64
View File
@@ -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.')