Use journey-stable distance costs for clientless travel routes
This commit is contained in:
1 parent
3c361e435a
commit
2778cf9bdd
19 files changed
+1375
-95
No files matched your search
@@ -0,0 +1,132 @@
|
||||
# Ursachenanalyse: Raidtest 01.11.2026, 22/23 Uhr
|
||||
|
||||
Nur Diagnose; kein Produktionscode, Botplan, Loader oder Serverzustand geändert.
|
||||
Branch feature/clientless-player-core; vorhandene Checkpoints/WIP erhalten.
|
||||
|
||||
## 1. Bestätigter Aufbruchblocker: falsche Zielvariante
|
||||
|
||||
Vorbereitung verwendete instance_visit_at_6354. BuildInstanceVariants in
|
||||
AdventureLocationMgr.cpp bevorzugt bei Raids mit vorhandener 25er-Normalvariante
|
||||
Difficulty1 für den Basisschlüssel. Difficulty0 wird als _d0 erzeugt.
|
||||
Native MapDifficulty.dbc für Karte671: modes0/1/2/3 haben10/25/10/25 Plätze.
|
||||
Dies wurde direkt aus der installierten DBC gelesen, nicht aus Namen geraten.
|
||||
Die passende 10er-Normalvariante ist instance_visit_at_6354_d0; native Eligibility
|
||||
für Renren bestätigt map671/content1/own1/invitation1 für beide Schlüssel.
|
||||
|
||||
Zusätzliche Inkonsistenz: InstanceCapacity=min(20,maxPlayers) begrenzt die 25er-
|
||||
Variante auf20, während Name und Difficulty bei25 bleiben. GroupPlan übernimmt
|
||||
InstanceCapacity. Complete verlangt exakt Capacity Mitglieder plus Rollen:
|
||||
capacity20 benötigt2T/4H/14D; tatsächlich sind10Bots mit2T/2H/6D in Gruppe2.
|
||||
Complete ist somit false, CheckFullGroup aktiviert keinen frühen Abreisehandoff.
|
||||
Um23:00 blockiert Begin vor ResolveInstance/Queue(travel_to_node) in WaitingGroup.
|
||||
Der korrekte Eingangspfad wird folglich gar nicht angefordert. Aktuell alle10
|
||||
im selben raid-Intent/WaitingGroup. Kein Beleg für ein Problem der Bastionroute.
|
||||
|
||||
Der falsche Basisschlüssel war ein Fehler der Testvorbereitung. Die20/25-
|
||||
Inkonsistenz ist ein separater Implementierungsfehler. Ein bloßer Teleport oder
|
||||
Reisereset kann beides nicht beheben. Der vorhandene Variantentest erwartet
|
||||
explizit20 für25er-Modi; Tests decken diese fehlerhafte Semantik daher nicht auf.
|
||||
Keine Tests/Builds in dieser reinen Diagnose gestartet.
|
||||
|
||||
## 2. Bestätigte Zerstreuungskette: Reiseablehnung und ungebundener Fallback
|
||||
|
||||
Historisches rotiertes Navigationlog:22:00:02 guid159 bei
|
||||
(-8864.300,682.378,97.943), ZielAllison(-8867.790,673.673,97.986).
|
||||
NAV_PLAN calculated1/complete1/type0x1, Weg9.38yd,4Punkte,maxZDelta0.03,
|
||||
finalDistance0.08 und reason ok. NAV_START trotzdem PathRejected.
|
||||
Fünf Recoveryversuche zu anderen Graphankern schlagen ebenfalls fehl;
|
||||
Flugfallback kann den Startplatz nicht erreichen.22:00:04 RECOVERY_FAILED und
|
||||
INTENT_ARRIVAL_REJECT beim vereinbarten Treffpunkt.
|
||||
|
||||
Fallback schützt einen aktiven Instanzbesuch sowie Botgruppenmitglieder mit
|
||||
GroupOverride, aber nicht den Treffpunkt einer Gruppenbildung beim Leader.
|
||||
Er durchsucht ALLE Allowed-Locations und priorisiert andere Maps lediglich mit
|
||||
Distanzaufschlag100000. Es gibt keine Bindung an vereinbarten Treffpunkt,
|
||||
Sturmwind oder aktuelle Hauptstadt. Logs zeigen Allison->GildedRose->weitere
|
||||
SW-Orte->Eisenschmiede->andere Hauptstädte, insgesamt37 Ersatzwahlen beim Leader.
|
||||
AdoptGroup übernimmt jeweils den Laufzeitzustand des Leaders. Mitglieder
|
||||
beenden laufende Taxi/Flug/Transport-Schritte vor dem Wechsel; deshalb verfolgen
|
||||
sie zeitversetzt verschiedene alte Ziele und verteilen sich über Kontinente.
|
||||
|
||||
Die genaue geometrische Ursache der ERSTEN PathRejected-Ablehnung ist offen.
|
||||
MMAP hat einen vollständigen Weg geliefert. Nachfolgende Ausführungsprüfungen
|
||||
umfassen AllowsSegment und den für Gruppen installierten GroundPathValidator
|
||||
(Höhenprobe/Steigung/LOS). Beide können ohne präzises NAV_REJECT-Log abbrechen.
|
||||
Die vorhandenen Logs rechtfertigen keine eindeutige Schuldzuweisung an eine
|
||||
bestimmte Höhen-/LOS-Probe. Zur Behebung diesen Ort gezielt reproduzieren und
|
||||
Ablehnungszweig/Koordinaten/erwartete und gemessene Höhe diagnostizieren; keine
|
||||
pauschale Abschaltung von Sicherheitsprüfungen.
|
||||
|
||||
## 3. Zwei beim Stundenwechsel verzögerte Mitglieder
|
||||
|
||||
Um23:00 warteten164/165 noch auf eine Leaderroute für die alte Gruppenbildung.
|
||||
Queue hängt Botmitgliedern route_source an; der Executor wartet bis15Sekunden
|
||||
auf eine geteilte Route, danach erfolgt eigenständige Anschlussplanung.
|
||||
SetWanted fordert während aktiver Reise StopAtNode an; Switching wartet auf
|
||||
Busy=false und führt gegebenenfalls einen Graphknoten-Handover aus.
|
||||
Nachfolgende native Diagnosen: beide travel_to_node stormwind_tradecenter
|
||||
Completed/Arrived und raid/WaitingGroup. Die anfängliche Aussage eines dauerhaften
|
||||
Intent-Hängers ist nicht belegt; das beobachtete Warten war vorübergehend.
|
||||
|
||||
## Gezielte Korrekturen für einen Folgetest
|
||||
|
||||
- Testziel explizit instance_visit_at_6354_d0; Difficulty0/capacity10/2T2H6D
|
||||
zusammen mit nativer Gruppen-Difficulty und Complete überprüfen.
|
||||
- 25er-Modi nicht als20er-Gruppe für einen25er-Raid vollständig melden;
|
||||
konsistente tatsächliche Kapazität oder ausdrückliche Nichtunterstützung.
|
||||
- Formationstreffpunkt beim Leader stabil halten; Fehlschlag führt zu lokaler
|
||||
Wiederholung/Diagnose, nicht automatischem Hauptstadtwechsel.
|
||||
- Anfangsablehnung in GoldenerRose mit genauer Ausführungsdiagnostik reproduzieren.
|
||||
- Regressionen für vollständige10er-Gruppe/früheAbreise, Treffpunktbindung bei
|
||||
Reisefehler, Stundenwechsel mit ausstehender geteilter Route und20/25-Konsistenz.
|
||||
|
||||
Belege: checkpoints/2026-11-01-raid-2200/initial-navigation-failure.log,
|
||||
investigation-console.log, reset-console.log und verified-db.tsv. Server.log
|
||||
enthält die Formation-Fallbacks und Mergefolge. Gameplay/Instanzanreise unbestätigt.
|
||||
|
||||
## Zusatz: drei Nutzerfragen zu Portalen und Kolonne, ca.23:17–23:21
|
||||
|
||||
1. Eisenschmiede: Portalspawn217855/GO195141/Spell65728 ist gebunden an
|
||||
edge1827 portal_use, enabled1/cost1, vonportal_go_217855_source nach
|
||||
portal_go_217855_target (VerwüsteteLande). Renren erreichte Quelle tatsächlich;
|
||||
23:09:48 folgte laut Log free_flight direkt zurBastion statt portal_use.
|
||||
Start wurde verworfen: no reachable open takeoff point, outdoor0,
|
||||
Diagnosecounter surface/indoor/area/column/mmap/movement=0/0/0/1/63/0.
|
||||
Danach Recovery über virtuelleStraße wird vom safetyAdapter abgelehnt.
|
||||
Also Portalbenutzung in dieser Folge NICHT ausgeführt, keine nachgewiesene
|
||||
native Portalzugangsverweigerung. Replan anQuelle: start->free_flight wiederholt.
|
||||
Plannerportalguard gilt nur isPortalSource && previous enthältCurrentState;
|
||||
Quellknoten alsStartwurzel fällt dadurch aus demGuard. Ein interaktivesPortal
|
||||
teleportiert anders als einAreaTrigger nicht beim bloßenAnlaufen.
|
||||
|
||||
2. Sturmwind-Schattenhochland: spawn200627/GO207694/Spell85545, edge1801,
|
||||
Quelle(-8186.15,413.729,116.75), Ziel(-4890.07,-6626.74,10.2846), map0.
|
||||
Enabled1/cost1. DB zeigt KEINE eingehende festeKante zurQuelle. Zurückliegender
|
||||
Livebeleg Elanora16:57:17 benutztPortal nativ mit arrivalError0; Voraussetzungen
|
||||
nicht pauschal alsGrund behaupten. Level80+-AdventureUnlock existiert.
|
||||
Aktuelle Route zumBastioneingang: SWtradecenter->taxi2->taxi6->IFportalQuelle,
|
||||
danach freieFlugplanung/Recovery. Nur dynamischerAnschluss kannSWPortal
|
||||
verbinden (allgemeineBrücken max200yd, Flugkandidaten16/Plan und6/Ursprung).
|
||||
Offline geometrische Kandidatenrangberechnung OHNEArea/Prereq/FlightFilter:
|
||||
SWportalQuelle steht vonSWtradecenter sowietaxi2 sogar aufRang1 der
|
||||
Makrokandidaten. Damit ist reineDistanzsortierung NICHT alsAblehnungsgrund
|
||||
belegt. Es fehlen personenbezogene Logs zu Kandidatenbudget/Estimate-Ablehnung
|
||||
und Folgeverbindung abPortalziel. Exakten verlorenenRoutenvergleich erst mit
|
||||
gezieltemPlantrace behaupten. Keine Runtimeplanung/Teleport zuTest erzwungen.
|
||||
|
||||
3. Kolonne: AdventureInteractionMgr kehrt beiBotLed vorGroupTravelMgr::Follow
|
||||
zurück. Botgruppenmitglieder adoptierenExecutorroute (AdoptRoute), teilen also
|
||||
Makroziel/Route, nicht automatisch individuellePositionen. Die allgemeine
|
||||
Formation()/Follow-Steuerung wird dabei nicht verwendet. AdaptRoad läuft
|
||||
nur imStartRecordedEdgePath, nicht für lokaleMMAP-Zuläufe/local_bridge/start.
|
||||
AdaptRoad erhält den gemeinsamenEndpunkt; wennSeitversatz unsicher ist, zweiter
|
||||
Versuch mitside0. Damit kann selbstStraßenadapter aufgleicheLinie zurückfallen.
|
||||
NativeTaxiflüge haben identischen nativenPfad ohne separateKolonnenversätze.
|
||||
EigeneFreeFlight hat sharedTrail mit3.5s+slot*0.6s Startverzögerung nur wenn
|
||||
passenderLeaderflightRecord mit gleichemZiel vorhanden. Kein allgemeiner
|
||||
Abstandsregler über alleReisemodi; keinePortalWarteaufstellung.
|
||||
Nutzerbeobachtung desÜbereinanderlaufens passt zu diesen Integrationslücken;
|
||||
keinBeleg, dass jedeFormationkomponente deaktiviert wäre.
|
||||
|
||||
Belege Zusatz: ironforge-portal-route-evidence.log; Sourceprüfung der erwähnten
|
||||
Methoden und nur lesendeWorldDB-Abfragen. KeineBot-/Codeänderung/Build/Restart.
|
||||
@@ -1,3 +1,265 @@
|
||||
# Aktueller Stand 02.11.2026, 06:21 MEZ
|
||||
|
||||
Das Entfernungskostenmodell ist gebaut und installiert. Acht gezielte Tests sind
|
||||
bestanden; Build und Installation exit 0. Details und Logs:
|
||||
`checkpoints/2026-11-02-travel-costs/`. Kein Commit oder Push.
|
||||
|
||||
Testserver nach Wiederaufnahme frisch gestartet: PID 219333, ready nachgewiesen,
|
||||
Ports 8085/8087 offen. Loader wieder aktiv; 110 Bots online. Die vorherigen 15
|
||||
Recovery-Bots wurden offline nativ in ihre Fraktionshauptstadt versetzt und wieder
|
||||
geladen. Die Unterbrechung und der ausgefallene ursprüngliche Test bleiben unten
|
||||
dokumentiert; keine Auswertung um 03:00 behaupten.
|
||||
|
||||
Vom Nutzer neu bestätigt: 06:00 Gruppenbildung, 07:00 Raid/Dungeon. 92 Bots ab
|
||||
Level 15 haben beide Slots verifiziert; missing=0. Allianz Goldene Rose,
|
||||
Horde Broken Tusk (poi_npc_287214). 18 aktuell online unter Level 15 wurden von
|
||||
Teständerungen ausgenommen. Alle zehn bekannten 85er haben 07:00 raid mit
|
||||
instance_visit_at_6354_d0 (Bastion normal10). Aktuelle Gruppe 5, Leader Renren159:
|
||||
159,160,161,162,163,164,165,167,176. Corric166 ist noch solo, schedule=1,
|
||||
Gruppenbildungs-Fallback poi_npc_313808 statt eigentlichem Treffpunkt beobachtet.
|
||||
Elanora168 wurde aus der Testgruppe entfernt; externe Allianz85 35/168 bekommen
|
||||
07:00 Dungeon statt Raid, damit sie nicht die zehnte Position belegen. Einmaliger
|
||||
nativer groupmanager run führte Corric bisher nicht zu. Nicht behaupten, alle
|
||||
zehn seien bereits gemeinsam. Im Managerlauf außerdem wiederholte Transfers
|
||||
Arthoran26 zwischen zwei Dungeon-Gruppen beobachtet; Log erhalten, kein Fix.
|
||||
|
||||
Read-only Beobachter und Berichtstimer sind implementiert und aktiv:
|
||||
`~/.config/systemd/user/vcore-adventure-observer-20261102.service`
|
||||
`~/.config/systemd/user/vcore-adventure-report-20261102.{service,timer}`.
|
||||
Timer nachgewiesen für 02.11.2026 08:00 Europe/Berlin, Persistent=true.
|
||||
Skripte, Manifest, Rohdaten: `checkpoints/2026-11-02-0600-group-test/`.
|
||||
Erste vollständige Beobachtung ca. 06:13; frühere Lücke im Bericht ausweisen.
|
||||
Bericht wird lokal nach `doc/clientless/2026-11-02-0800-test-report.md` geschrieben.
|
||||
Bis dahin ist die Datei ausdrücklich Platzhalter. Keine automatische Chatnachricht
|
||||
möglich/zugesagt. Bericht nach 08:00 auf erfolgreiche Erstellung prüfen und Nutzer
|
||||
über Ergebnisse informieren. Beobachter repariert keine Bots während des Tests.
|
||||
|
||||
Noch offen: tatsächliche Ankunft/Instanzeintritt und vollständige Zehnergruppe;
|
||||
ursprüngliche MMAP-Höhenvalidator-Ablehnung, Treffpunkt-Fallback und Kolonnenbildung
|
||||
sind nicht durch das Kostenmodell behoben. Gameplay-Abnahme steht aus.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Fortsetzung06:04MEZ, Ersatztest06/07/08 vomNutzerfreigegeben
|
||||
|
||||
Reisekostenmodell kompiliert/installiert (build-installed.log exit0, install.log
|
||||
exit0), ZweiterStart00:30 readyPID214613 Ports8085/8087. NativeTaxi15=6457.25yd,
|
||||
Transportgeometrien positiv. SiebengezielteTestsgrün: Kosten/NativeTransport/
|
||||
JourneyLifecycle/SharedRoute/Planner/GroundExit/AreaNode, RescueGraphzusätzlich.
|
||||
NeueQuellen ClientlessTravelCost.h undProfile-/Geometrieintegration inGraph/
|
||||
Executor/Transport; ConfigCostMultiplier alle7Modi1, Variation0.20. NativeRNG
|
||||
proReise, RecoverybehältProfil, FollowerkopierenLeaderRoute+Faktoren.
|
||||
NochkeinevollständigeLive-Abnahme/keinCommit/Push.
|
||||
|
||||
SitzungvorAbschlussunterbrochen: Loaderblieb0, alte03Uhr-TesteinrichtungNICHT
|
||||
abgeschlossen, KEINE03Uhr-Auswertung/keinTimer eingerichtet. NICHTanderesbehaupten.
|
||||
BeiFortsetzung06:02 Serveroffline, keinvcore-worldserver-tmux/Prozess. Logsenden
|
||||
04:03:45ohnesauberesShutdown; keinaktuellerCore (nurhistorischer01.11.Core).
|
||||
Ursacheungeklärt. VorigesServerloggesichertimKostencheckpoint.
|
||||
|
||||
NutzerneuerTest: JETZT06:00Gruppenbildung,07:00Dungeon/Raid; Auswertungentsprechend
|
||||
08MEZ (bisheralsrelativeFortsetzungkommuniziert). AlleAllianz:GoldeneRoseSW,
|
||||
Horde:BrokenTuskORG. Unter15ausnehmen undnormalePläneunverändert (explizitbeantwortet).
|
||||
Bekannte85er159..167/176 auf10normalBastion6354_d0für07. Alle10wiederladen.
|
||||
Testcheckpoint checkpoints/2026-11-02-0600-group-test/.
|
||||
Serverjetztrecovern, alte15Restbotsnochprüfen/bergensoweitnötig, Loader1restaurieren.
|
||||
NativeeditplanallnurOnline: erst07instance-or-raidauto,06formationmitFraktions-
|
||||
location. FürCohort07raid6354_d0 explizit. Unter15unverändert.
|
||||
|
||||
Plan fürlokaleZeitsteuerung: UserSystemd funktioniert, keineChat-Automationstools
|
||||
verfügbar. NochNICHTimplementiert: read-onlyObserver mitNativeStatus/DBGroup-
|
||||
Snapshots+Logdelta, einmaligerTimer08MEZ schreibtBericht. Keineautomatische
|
||||
Chatnachrichtzusagen. KeineCLI-Subagentenstarten (Delegationnichtbeauftragt).
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Routenalternativenfix final installiert, Restbetrieb restauriert
|
||||
|
||||
Details: checkpoints/2026-11-01-route-alternatives/README.md.
|
||||
FinalerBuild-j10/InstallPASS, freshreadyPID200529/Ports8085+8087.
|
||||
ViergezielteRegressionenPASS inklPortalroot, PortalblockEscape, verworfeneRoad-
|
||||
Kante1885 gehtanRecovery, konkurrierendeBudgetverbraucher+isolierterPortalflug.
|
||||
NegativeKontrolle ohnePortalverbindungs-Pass fällt erwartbarfehl.
|
||||
LiveIFPortalnutzungRenren23:32:04->06/arrivalError0.00 bestätigt.
|
||||
LiveSW23:42:21: vollständigeFlug->Portal200627->Flug-Verbindungwirdjetzt
|
||||
berücksichtigt, gewinntabernicht gegenTaxiroute. NeuerbelegterKostenbefund:
|
||||
Taxikante15(SW->IF) cost1, aufgezeichneteStraße478.308, FlügeDistanzkosten.
|
||||
Nichtbehaupten, SWPortal werdejetztgewählt; konsistenteMakroreisekosten offen.
|
||||
SourceänderungenPlanner/Executor, zweiTests; keinKolonnen- oderHöhenvalidator-
|
||||
Bypass. ZweiBuilds/Restarts mitLoader0/SafeLogout undje5Minuten-Regel; Recovery-
|
||||
Listen22/3 undBergungeninHauptstädten dokumentiert. RenrenScheduleonrestauriert,
|
||||
Loaderconfig1/nativeReloadrestauriert, normalerBetrieb läuftwiederan.
|
||||
UrsprünglicheZehnergruppewarbeimSafeLogout imaltenRuntimebereitsinTeilgruppen
|
||||
zerfallen;kein vollständigerRaid/Ankunftbestätigt. KeinCommit/Push.
|
||||
NächsteArbeit: echteTransportzeitkosten stattTaxipauschale1, genaueGrundweg-
|
||||
Ablehnungsdiagnostik, Kolonnenintegrationwievorheruntersucht.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: finaler Portal-Flugverbindungsfix gebaut, zweiterRestart ausstehend
|
||||
|
||||
ErsterFix installiert/frischready PID195658 Ports8085/8087. LiveRenren
|
||||
benutzteIFPortal217855 nativ23:32:04->23:32:06, arrivalError0. SW->Bastion
|
||||
wählteaberweiterIF. Deshalb zweiterPlannerfix: vorRoadExitBudget begrenzt
|
||||
vollständige nahePortal-Flugverbindungen root->source(freeflight)->portal->
|
||||
target->destination(freeflight) aufGesamtkosten prüfen, nativeRechte/Sperren
|
||||
behalten, maximal8Kandidaten/6neueEstimates imbestehenden16erBudget.
|
||||
KeineBodenkanten zuSWPortalen hinzugefügt. NeueRegression konkurrierendeExits+
|
||||
unbrauchbarerStart schlägt OHNE dieseErgänzung erwartbarfehl, MIT ihrPASS.
|
||||
FinalerCMakeBuild-j10 EXIT0 (build-final.log), nochNICHTinstalliert.
|
||||
ZweiterSafeLogout23:35:20MEZ, Loader weiterhin0; FünfMinutengrenze23:40:20.
|
||||
Zuletzt4Restbots73/92/96/132,96bereitsdanachsicherausgeloggt. NativeDiagnose,
|
||||
Recoveryliste recovery-before-final-install.tsv unter route-alternatives.
|
||||
VorShutdown aktuelleRestliste/Flags/Saveallaktualisieren, shutdown10, Prozessende
|
||||
prüfen, finalinstall,startready/ports, Restbotsprüfen/ggfHauptstädte.
|
||||
Loader1 zumAbschlussrestaurieren. Renren159 wurdefürPortaltests manuellgestoppt,
|
||||
abschließend schedule159on wiederherstellen. AndereBotpläneunverändert.
|
||||
KeinCommit/Push. ÄnderungenPlanner+Executor+zweiRegressionen. KeineKolonnen-
|
||||
oderGrundvalidatorreparatur. DerenBefunde bleibenoffen.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Routenalternativen-Fix gebaut, Installation steht unmittelbar aus
|
||||
|
||||
Autorisiert durchNutzerkontext: trotz gescheitertemFlugstart Graphalternativen
|
||||
nutzen; SWCataclysmPortale nurdurchFlug erreichbar lassen. ZweiSourcefixes:
|
||||
PortalGuard auchStartwurzel, bei gesperrtemPortal weiterhinStraßen zulassen;
|
||||
GruppenRoadAdapter-Reject führt überRecoverTravel mitgesperrterKante weiter.
|
||||
StartFlightProbes max4 reserviertPlanbudget für isolierteFluganschlüsse.
|
||||
Plannerregression erweitert: Portalroot/PortalblockEscape/40unerreichbareRoots+
|
||||
isolierterPortalflug; PASS. GroundExit-Test verifiziertRecovery1885; PASS.
|
||||
PortalPrerequisites/PortalUse PASS. CMakeDebugBuild-j10 EXIT0, nochNICHTinstalliert.
|
||||
Loaderconfig1->0, tatsächlichenabled0/busy0. SafeLogout23:24:15MEZ angefordert;
|
||||
22Restbots gegen23:28 inFlug/Kampf. FünfMinuten-Grenze23:29:15.
|
||||
Belege/Recoveryliste/Flags/NativeStatus unter
|
||||
checkpoints/2026-11-01-route-alternatives/. VorShutdown nochmalsStatus sichern;
|
||||
sauberShutdown10, Prozessende VOR cmake--install. Dannfreshready+Ports prüfen,
|
||||
22Recoverybots laden/Transitstatesprüfen und nötigenfallsAllianceSW/HordeORG.
|
||||
Loader1 undnormalenBetrieb danachwiederherstellen. Gruppe warbereits imalten
|
||||
Runtime beimSafeLogout inmehrereTeilgruppen zerfallen; keinevollständigeGruppe
|
||||
behaupten. KeinCommit/Push. KeinKolonnenumbau/keinGrundvalidatorBypass.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Nutzerfragen Portale/Kolonne untersucht, nurDiagnose
|
||||
|
||||
Zusatz imBericht doc/clientless/2026-11-01-raid-test-investigation.md.
|
||||
IFPortal wird NICHT benutzt: LeaderRoute enthält an217855Quelle free_flight
|
||||
zurBastion; Start indoor/no reachable open takeoff point; Recovery safetyAdapter
|
||||
reject. Quellknoten alsStartwurzel umgehtPortalGuard(previous fehlt).
|
||||
SW200627Portal vorhanden/benutzbarhistorisch, aber ohneeingehendefesteKante.
|
||||
Exakter Grund der Nichtauswahl UNGEKLÄRT: dynamischeFlugverbindung/budget/Estimate
|
||||
nicht kandidatenbezogen geloggt. Ungefiltert geometrisch sogarRang1 vonSWtrade/
|
||||
taxi2; nicht behaupten, es wäre nur zuweit/Questgesperrt.
|
||||
Kolonne: BotLed umgehtallgemeinesFollow; gemeinsameExecutorRouten statt dauernder
|
||||
Positionsformation. Adapter nuraufgezeichneteStraßen, keineZuläufe/nativeTaxi-
|
||||
Versätze; gemeinsameEndpunkte undside0Fallback ermöglichenÜberlagerung.
|
||||
Winienwi nachNutzerTeleport bisIF gereist, dort23:13:45 und23:14:25 gleiche
|
||||
safetyAdapterAblehnung bei(-4606.44,-928.997,501.07). Einzelbeobachtung auf
|
||||
Nutzerhinweis beendet: gesamteGruppebetroffen. KeinEingriff/Build/Restart.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Winienwis Blockade live genauer eingegrenzt,23:07
|
||||
|
||||
Nutzer bat besonders Winienwi163 zu beobachten, virtueller Einstieg vorGasthaus.
|
||||
Nur Diagnose, keine Code-/Botänderung. Wiederholte Versuche23:06:45 und23:07:17:
|
||||
virtueller Einstieg WIRD gewählt: (-8843.320,667.142,97.829), Start
|
||||
(-8863.633,676.073,97.903), vollständiger MMAP10Punkte/35.30yd/22.19Luftlinie,
|
||||
maxZDelta0.60, finalDistance0.00. Native travelinfo belegt eindeutig
|
||||
Path.Reason='owner ground-safety validator rejected spline'. Gruppenvalidator
|
||||
ValidateGroundPath (AdventureGroupTravelMgr) wird bei Observe auch fürLeader
|
||||
installiert; prüft alle2yd Höhe/Steigung sowieLOS proSegment. Welcher innere
|
||||
Zweig/Probe ablehnt, wird noch nicht geloggt. Nicht mehr bloß Vermutung über
|
||||
Gebietspflicht: bei Winienwi ist OWNER VALIDATOR nachgewiesen.
|
||||
Alternative echteKnoten und5Recoveries scheitern ebenso; Flugfallback scheitert
|
||||
am Bodenweg zum Freiluft-Startplatz. Statische Flight-probe liest am aktuellen
|
||||
XY roof115.617, nearFeet97.903 (korrekter Innenboden); beim Ziel97.830.
|
||||
Dies beweist mehrere Höhenlagen, aber NICHT, dass der Gruppenvalidator die
|
||||
Dachhöhe ausgewählt hat. Keine pauschale Ground-/LOS-Abschaltung vornehmen.
|
||||
Belege winienwi-live-diagnostics.log/winienwi-navigation.log unter
|
||||
checkpoints/2026-11-01-raid-2200/. Nächster sinnvoller Schritt: gezielte genaue
|
||||
Ablehnungsdiagnostik in ValidateGroundPath, an derselbenPosition reproduzieren.
|
||||
Kein Build/Install/Restart, kein Teleport.10erRaidintents bleiben gesetzt.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Alle10 Raidintents auf10normal korrigiert, ca.23:05
|
||||
|
||||
Nutzer autorisierte Anpassung aller zehn aktuellen Raidintents. Native editplan
|
||||
preview/apply für159..167/176: aktueller Slot23 raid instance_visit_at_6354_d0.
|
||||
DB bestätigt alle10 persistent. GroupPlan Leader159 jetzt capacity10; native
|
||||
INSTANCE_PLAN mode0/map671/entry6354. NeunBots in erster Nachkontrolle Moving/
|
||||
Travel zurBastion, Winienwi163 lokaler Anreiseversuch Failed und30sRetry zum
|
||||
selben Ziel (ground approach interrupted). Keine Ankunft/Gameplay bestätigt.
|
||||
Keine Sourceänderung/Build/Restart. Gruppe bleibt zehnköpfig. Nachweise:
|
||||
checkpoints/2026-11-01-raid-2200/10man-applied.tsv und10man-console.log.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Fehlgeschlagenen Raidtest untersucht (nur Diagnose)
|
||||
|
||||
Bericht: doc/clientless/2026-11-01-raid-test-investigation.md.
|
||||
Bestätigt: Basisschlüssel6354 wählt25normal/Difficulty1, intern fälschlich20;
|
||||
10er-Normal heißt6354_d0 (nativeEligibility erlaubt). Completefalse verhindert
|
||||
frühe und23Uhr-Abreise. Vorbereitung hätte explizite10er-Variante wählen müssen.
|
||||
Zerstreuung:22:00:02 vollständiger9.38yd-MMAP-Weg zuAllison bei Ausführung
|
||||
PathRejected, Recoveries erschöpft; Leader-Fallback wechselt37x Treffpunkt bis
|
||||
andere Kontinente. Genauer erster Höhen/LOS/Gebiet-Ablehnungszweig noch offen.
|
||||
164/165 nur vorübergehend im Reisehandover; inzwischen alle10 raid/WaitingGroup.
|
||||
Kein Codefix/Build/Restart/Botplanänderung bei dieser Diagnose.
|
||||
Belege unter checkpoints/2026-11-01-raid-2200/initial-navigation-failure.log und
|
||||
investigation-console.log. Vorschläge/Regressionsumfang im Bericht.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Raidtest-Reisereset am 01.11.2026 ca.22:53 MEZ
|
||||
|
||||
Auf ausdrücklichen Nutzerauftrag alle10 Raidbots159..167/176 zurück zur
|
||||
Goldenen Rose gesetzt und alte Reisepläne entfernt. Vorherige Diagnose22:47:
|
||||
Gruppe2/Leader159 hat exakt10 online, aber Plan meldet capacity20/Anzeige25;
|
||||
Formation-Fallback wechselte wiederholt Hauptstädte/Kontinente. Kein Codefix.
|
||||
Für vollständigen Reset gezielter Logout der10 (ohne Serverrestart), Offline-
|
||||
Status ALLER10 vor Positionsänderung geprüft. Offlineposition genau auf Allison
|
||||
poi_npc_314039: map0 -8867.79/673.673/97.9864/o5.20108, zone1519,
|
||||
instance0, taxi_path leer. Wieder eingeloggt: alte Executor-/Travel-/Flight-
|
||||
und Behavior-RAM-Records durch Logout verworfen. Aktuellen Formationplan aller10
|
||||
nativ erneut auf poi_npc_314039 gesetzt, Schedule1. 23-Raidplan unverändert.
|
||||
Alle10 native area5148/map0 bestätigt, jeweils kein freier Flugzustand.
|
||||
Loader weiter normal1; kein Build/Install/Serverrestart. Gruppe bleibt bestehen.
|
||||
Nachweise checkpoints/2026-11-01-raid-2200/{before-reset-positions.tsv,reset-console.log}.
|
||||
Reset behebt nicht den zugrunde liegenden Treffpunkt-/Raidkapazitätsfehler;
|
||||
weiteren Verlauf nicht als erfolgreich abgenommen darstellen.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Raidtest am 01.11.2026 um 22/23 Uhr vorbereitet
|
||||
|
||||
Nutzerauftrag: zehn Allianz-AdventureBots Stufe85 in Sturmwind, 22:00
|
||||
Gruppenbildung, 23:00 Raid Bastion des Zwielichts. Roster: Tanks Ragnorli167
|
||||
und Ashaelri176; Heiler Marnda161 (Schamane), Aruendas162 (Priester);
|
||||
Damage Renren159, Marndra160, Winienwi163, Taliaarnora164, Brynda165, Corric166.
|
||||
Rollen aus generierten Profilen geprüft: 2T/2H/6D. Alle zehn eingeloggt.
|
||||
Tagesplan für 20261101 persistent über native editplan-Befehle gesetzt:
|
||||
21 rest, 22 groupformation, beide poi_npc_314039 (Allison, Gilded Rose, SW);
|
||||
23 raid instance_visit_at_6354 (Twilight Bastion Entrance, NICHT3529/MoltenCore).
|
||||
Tagesplansteuerung an. Bestehende Teilgruppen bleiben dem normalen Gruppenmanager
|
||||
überlassen; noch keine vollständige Zehnergruppe oder Raid-Ankunft behauptet.
|
||||
Zunächst poi_guard_22 vom Editor abgelehnt, deshalb auf gültigen gemeinsamen
|
||||
NPC-Treffpunkt korrigiert. Aktive Altanreise einiger Bots brauchte sicheren
|
||||
Handover; 163/164/166 BehaviorStop, erneuter nativer SW-Teleport und ScheduleOn.
|
||||
|
||||
Testserver war zu Beginn offline (kein Prozess/tmux). Ohne Build gestartet,
|
||||
frische ready-Meldung/PID180309 und Ports8085/8087 geprüft. Normalen Loaderbetrieb
|
||||
beibehalten. Kein Build, keine Installation, kein Push/Commit.
|
||||
Nachweise: checkpoints/2026-11-01-raid-2200/console.log und verified-db.tsv.
|
||||
Final: alle10 nach Saveall online1/Level85/map0/zone1519 und taxi_path leer;
|
||||
Corric nach abgeschlossenem Routen-Handover erneut SW gesetzt, native area5148
|
||||
und schedule1/rest bestätigt. Alle10 DB-Slots22/23 geprüft.
|
||||
Die tatsächliche Gruppenbildung/Abreise/Gameplay ab22/23 bleibt live zu prüfen.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Quest-unabhängige Portalnutzung für AdventureBots ab Stufe 80
|
||||
|
||||
AdventureBots können keinen Questfortschritt erwerben. Graphgebundene öffentliche
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only, bounded observation and deterministic test report. No bot repairs."""
|
||||
from pathlib import Path
|
||||
import collections,datetime,json,os,re,subprocess,sys,time
|
||||
import test_driver as d
|
||||
BASE=d.BASE
|
||||
REPORT=d.ROOT/'doc/clientless/2026-11-02-0800-test-report.md'
|
||||
def append(path,value):
|
||||
with path.open('a') as f:f.write(json.dumps(value,ensure_ascii=False)+'\n')
|
||||
def load(path):
|
||||
result=[]
|
||||
if path.exists():
|
||||
for line in path.read_text().splitlines():
|
||||
try:result.append(json.loads(line))
|
||||
except json.JSONDecodeError:pass
|
||||
return result
|
||||
def manifest():return json.loads((BASE/'manifest.json').read_text())
|
||||
def alive():
|
||||
r=subprocess.run(['pgrep','-x','worldserver'],text=True,capture_output=True)
|
||||
result=[]
|
||||
for value in r.stdout.split():
|
||||
try:
|
||||
if os.readlink('/proc/'+value+'/exe')=='/home/wow_admin/wow_cata_server/server/debug/bin/worldserver':result.append(int(value))
|
||||
except OSError:pass
|
||||
return result
|
||||
EVENT=re.compile(r'\b(?:GROUP_[A-Z_]+|INSTANCE_[A-Z_]+|INTENT_[A-Z_]+|EDGE_[A-Z_]+|RECOVERY_[A-Z_]+|NAV_REJECT|TEMP_TRAVEL|PORTAL_[A-Z_]+|TRAVEL_(?:PLAN[A-Z_]*|JOURNEY_COSTS|SHARED_ROUTE)|CPC_PERF)\b')
|
||||
class Tail:
|
||||
def __init__(self,path):self.path=path;self.file=None;self.initial=True
|
||||
def poll(self):
|
||||
try:
|
||||
st=self.path.stat()
|
||||
if self.file is None:
|
||||
self.file=self.path.open();self.inode=st.st_ino
|
||||
if self.initial:self.file.seek(0,2);self.initial=False
|
||||
lines=self.file.readlines()
|
||||
if self.inode!=st.st_ino:
|
||||
self.file.close();self.file=self.path.open();self.inode=st.st_ino;lines+=self.file.readlines()
|
||||
elif self.file.tell()>st.st_size:self.file.seek(0)
|
||||
stamp=d.now().isoformat()
|
||||
for line in lines:
|
||||
match=EVENT.search(line)
|
||||
if match:append(BASE/'events.jsonl',{'observed_at':stamp,'source':self.path.name,'event':match[0],'line':line.rstrip()})
|
||||
except OSError as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
def snapshot(native=True):
|
||||
record={'time':d.now().isoformat(),'server_pids':alive()}
|
||||
record['roster']=d.roster()
|
||||
record['groups']=d.query('Character','SELECT g.guid,g.leaderGuid,g.groupType,m.memberGuid,c.name,c.level,c.online FROM groups g JOIN group_member m ON m.guid=g.guid JOIN characters c ON c.guid=m.memberGuid ORDER BY g.guid,m.memberGuid;')
|
||||
record['plans']=d.plans();record['native']={}
|
||||
if native and record['server_pids']:
|
||||
raw=[];ids=[r['guid'] for r in record['roster'] if r['level']>=15]
|
||||
for pos in range(0,len(ids),20):
|
||||
chunk=ids[pos:pos+20]
|
||||
for g in chunk:
|
||||
for command in [f'clientless adventure status {g}',f'clientless actioninfo {g}',f'clientless interaction {g}',f'clientless adventure area {g}']:d.command(command)
|
||||
time.sleep(1.5);text=d.console(1400);raw.append(text)
|
||||
for g in chunk:
|
||||
bot={}
|
||||
for key,pattern in {
|
||||
'behavior':rf'Adventure GUID {g}: enabled=(\d+) schedule=(\d+) phase=(\d+) intent=\x27([^\x27]*)\x27 location=\x27([^\x27]*)\x27.*?detail=\x27([^\x27]*)\x27',
|
||||
'action':rf'Action GUID {g}: state=(\w+) action=\x27([^\x27]*)\x27 target=\x27([^\x27]*)\x27.*?detail=\x27([^\x27]*)\x27',
|
||||
'area':rf'Area access GUID {g}: .*?map=(\d+) area=(\d+).*?allowed=(\d+)',
|
||||
}.items():
|
||||
matches=list(re.finditer(pattern,text))
|
||||
if matches:bot[key]=list(matches[-1].groups())
|
||||
record['native'][str(g)]=bot
|
||||
(BASE/('snapshot-'+d.now().strftime('%Y%m%d-%H%M%S')+'.log')).write_text('\n'.join(raw))
|
||||
append(BASE/'snapshots.jsonl',record)
|
||||
return record
|
||||
def observe():
|
||||
end=datetime.datetime.fromisoformat(manifest()['evaluate_at'])
|
||||
logs=Path('/home/wow_admin/wow_cata_server/server/debug/bin')
|
||||
tails=[Tail(logs/'Server.log'),Tail(logs/'adventurebot-navigation.log')]
|
||||
for tail in tails:tail.poll()
|
||||
next_snapshot=0
|
||||
append(BASE/'lifecycle.jsonl',{'event':'observer_started','time':d.now().isoformat(),'end':end.isoformat()})
|
||||
while d.now()<end:
|
||||
for tail in tails:tail.poll()
|
||||
if time.monotonic()>=next_snapshot:
|
||||
try:snapshot()
|
||||
except Exception as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
next_snapshot=time.monotonic()+300
|
||||
# Preserve a final snapshot before the activity slot ends.
|
||||
if (end-d.now()).total_seconds()<35:
|
||||
try:snapshot()
|
||||
except Exception as error:append(BASE/'observer-errors.jsonl',{'time':d.now().isoformat(),'error':str(error)})
|
||||
while d.now()<end:
|
||||
for tail in tails:tail.poll()
|
||||
time.sleep(1)
|
||||
break
|
||||
time.sleep(2)
|
||||
for tail in tails:tail.poll()
|
||||
append(BASE/'lifecycle.jsonl',{'event':'observer_finished','time':d.now().isoformat()})
|
||||
def report(preview=False):
|
||||
m=manifest();end=datetime.datetime.fromisoformat(m['evaluate_at']);start=end.replace(hour=m['formation_hour'])
|
||||
snapshots=[s for s in load(BASE/'snapshots.jsonl') if start<=datetime.datetime.fromisoformat(s['time'])<=end]
|
||||
events=[]
|
||||
for item in load(BASE/'events.jsonl'):
|
||||
stamp=datetime.datetime.fromisoformat(item['observed_at'])
|
||||
match=re.match(r'(\d{4}-\d\d-\d\d_\d\d:\d\d:\d\d)',item['line'])
|
||||
if match:stamp=datetime.datetime.strptime(match[1],'%Y-%m-%d_%H:%M:%S').replace(tzinfo=d.TZ)
|
||||
if start<=stamp<end:events.append(item)
|
||||
# Server and navigation logs can repeat the same event; never count duplicates.
|
||||
unique={}
|
||||
for item in events:
|
||||
line=re.sub(r'^.*?\[(?:server\.[^]]+)\]\s*','',item['line'])
|
||||
key=(item['event'],line)
|
||||
unique.setdefault(key,item)
|
||||
events=list(unique.values());counts=collections.Counter(e['event'] for e in events)
|
||||
arrivals=set();entered=set();cohort_entered=set();positions={};reasons=collections.Counter();plans=set()
|
||||
cohort=set(m['raid_guids']);goal='instance_visit_at_6354_d0'
|
||||
for e in events:
|
||||
line=e['line'];g=re.search(r'\bguid=(\d+)\b',line)
|
||||
guid=int(g[1]) if g else None
|
||||
if guid is not None:
|
||||
if e['event']=='INTENT_ARRIVED' and "intent='groupformation'" in line and any("location='"+v+"'" in line for v in m['meeting_locations'].values()):arrivals.add(guid)
|
||||
if e['event']=='INSTANCE_ENTERED':
|
||||
entered.add(guid)
|
||||
if 'map=671 ' in line and 'mode=0' in line and guid in cohort:cohort_entered.add(guid)
|
||||
if e['event']=='INSTANCE_PLAN' and guid in cohort:plans.add(guid)
|
||||
if e['event']=='TEMP_TRAVEL' and 'position=' in line:positions[guid]=line
|
||||
if e['event'] in ['RECOVERY_FAILED','PORTAL_USE_FAILED','INSTANCE_ENTRY_DENIED','INSTANCE_TRAVEL_RETRY']:
|
||||
detail=re.search(r"(?:reason|status)='([^']*)'",line)
|
||||
reasons[(e['event'],detail[1] if detail else 'siehe Rohlog')]+=1
|
||||
max_group=0;together=False
|
||||
rolemap={r['guid']:r.get('role','unknown') for r in m['roster']}
|
||||
latest_group={}
|
||||
for s in snapshots:
|
||||
if not s['server_pids']:continue
|
||||
grouped=collections.defaultdict(list)
|
||||
for row in s['groups']:grouped[row['guid']].append(row)
|
||||
for gid,rows in grouped.items():
|
||||
members={int(r['memberGuid']) for r in rows};overlap=members&cohort
|
||||
if len(overlap)>max_group:max_group=len(overlap)
|
||||
if len(overlap)==10:together=True
|
||||
latest_group=grouped
|
||||
for snap in snapshots:
|
||||
if not snap['server_pids']:continue
|
||||
for guid,bot in snap.get('native',{}).items():
|
||||
status=bot.get('behavior')
|
||||
if status and status[2]=='2' and status[3]=='groupformation' and status[4] in m['meeting_locations'].values():arrivals.add(int(guid))
|
||||
latest=snapshots[-1] if snapshots else None
|
||||
leader_seeds={};copies=0;matched_seeds=0;unknown_seeds=0;mismatched_seeds=0
|
||||
for e in events:
|
||||
line=e['line']
|
||||
if e['event']=='TRAVEL_JOURNEY_COSTS':
|
||||
match=re.search(r'guid=(\d+) seed=(\d+)',line)
|
||||
if match:leader_seeds[int(match[1])]=int(match[2])
|
||||
elif e['event']=='TRAVEL_SHARED_ROUTE':
|
||||
match=re.search(r'leader=(\d+).*?journeySeed=(\d+)',line)
|
||||
if match:
|
||||
copies+=1;leader=int(match[1]);seed=int(match[2])
|
||||
if leader not in leader_seeds:unknown_seeds+=1
|
||||
elif leader_seeds[leader]==seed:matched_seeds+=1
|
||||
else:mismatched_seeds+=1
|
||||
stamp=d.now();future=preview or stamp<end
|
||||
title='Vorläufige Testbeobachtung' if future else 'Testauswertung'
|
||||
lines=[f'# {title}: {start:%d.%m.%Y}, Gruppenbildung {m["formation_hour"]:02d}:00 / Instanzen {m["activity_hour"]:02d}:00',
|
||||
'',f'Erstellt: {stamp:%d.%m.%Y %H:%M:%S %Z}. Geplanter Auswertungszeitpunkt: {end:%H:%M %Z}.',
|
||||
'',f'- Geprüfte Testbots: {len(m["eligible_guids"])}. Unter Stufe 15 ausgenommen: {len(m["excluded_under15"])}.',
|
||||
f'- Feste Treffpunkte: Allianz Goldene Rose in Sturmwind; Horde Broken Tusk in Orgrimmar.',
|
||||
f'- Status-Snapshots: {len(snapshots)}; ausgewertete eindeutige Logereignisse: {len(events)}.',
|
||||
f'- Erste erfasste Statusstichprobe: {snapshots[0]["time"] if snapshots else "keine"}; frühere Ereignisse sind nicht vollständig erfasst.',
|
||||
f'- Bots mit belegter Ankunft am festgelegten Formationstreffpunkt: {len(arrivals)}.',
|
||||
f'- Bots mit protokolliertem tatsächlichem Instanzbeitritt: {len(entered)}.',
|
||||
f'- Snapshots ohne laufenden Testserver: {sum(not s["server_pids"] for s in snapshots)}.',
|
||||
'', '## Bekannte Zehnergruppe: Bastion des Zwielichts, normal', '',
|
||||
f'- Maximal gemeinsam in einer nativen Gruppe beobachtet: {max_group}/10.',
|
||||
f'- Alle zehn gemeinsam nachgewiesen: {"ja" if together else "nein; durch die Stichproben nicht nachgewiesen"}.',
|
||||
f'- Bots mit Instanzplanung: {len(plans)}/10; mit tatsächlichem Beitritt zu Karte 671 im Modus 0: {len(cohort_entered)}/10.',
|
||||
'', '| GUID | Name | Rolle | Bastion betreten | Letzter nativer Status |', '|---:|---|---|---|---|']
|
||||
byid={r['guid']:r for r in m['roster']}
|
||||
for g in sorted(cohort):
|
||||
row=byid.get(g,{});status=(latest or {}).get('native',{}).get(str(g),{}).get('behavior')
|
||||
detail=(status[3]+' / '+status[4]+' / '+status[5]) if status else 'kein frischer Status erfasst'
|
||||
lines.append(f'| {g} | {row.get("name","unbekannt")} | {rolemap.get(g,"unbekannt")} | {"ja" if g in cohort_entered else "nicht nachgewiesen"} | {detail.replace("|","/")} |')
|
||||
lines+=['','## Übernahme der Anführerroute','',
|
||||
f'- Protokollierte Routenübernahmen mit Journey-Seed: {copies}.',
|
||||
f'- Gleicher zuletzt beobachteter Anführer-Seed: {matched_seeds}; abweichend: {mismatched_seeds}; Anführer-Seed im Beobachtungsfenster nicht bekannt: {unknown_seeds}.',
|
||||
'- Ein abweichender letzter Seed erfordert Prüfung der konkreten Reise und des Zeitpunkts; diese Zählung allein beweist keinen Fehler.',
|
||||
'', '## Fehler und Reiseereignisse','']
|
||||
for key in ['GROUP_MERGE','GROUP_MANAGER_RUN','GROUP_EARLY_DEPART','PORTAL_USE_START','RECOVERY_FAILED','INSTANCE_TRAVEL_RETRY','INSTANCE_ENTRY_DENIED']:
|
||||
lines.append(f'- {key}: {counts[key]}.')
|
||||
if reasons:
|
||||
lines+=['','Häufigste protokollierte Fehler:','']
|
||||
for (kind,reason),count in reasons.most_common(12):lines.append(f'- {kind}: {reason} ({count} Ereignisse).')
|
||||
lines+=['','## Grenzen der Auswertung','',
|
||||
'- Ausgewertet werden technische Zustände und native Logereignisse; keine visuelle Gameplay-Abnahme und keine Bestätigung von Bosskämpfen.',
|
||||
'- Ein Reiseplan ist kein Ankunftsnachweis. Fehlende Ereignisse werden als nicht nachgewiesen ausgewiesen.',
|
||||
'- Die Gruppenmitgliedschaft und Zustände werden alle fünf Minuten stichprobenartig gelesen; kurzlebige Zwischenzustände können fehlen.',
|
||||
'- Charakterpositionen aus DB-Snapshots sind zuletzt gespeicherte Positionen. Live-Reisepositionen stehen in den TEMP_TRAVEL-Ereignissen.',
|
||||
'- Der Beobachter repariert, teleportiert oder startet den Server nicht. Ausfälle werden dokumentiert.',
|
||||
f'- Letzter Snapshot: {latest["time"] if latest else "keiner"}.',
|
||||
'',f'Rohdaten: {BASE.relative_to(d.ROOT)}/ (manifest.json, snapshots.jsonl, events.jsonl, snapshot-*.log).','']
|
||||
target=BASE/'report-preview.md' if future else REPORT
|
||||
tmp=target.with_suffix('.tmp');tmp.write_text('\n'.join(lines));tmp.replace(target)
|
||||
(BASE/'report-status.json').write_text(json.dumps({'generated_at':stamp.isoformat(),'expected_at':end.isoformat(),'preview':future,'path':str(target),'snapshots':len(snapshots),'events':len(events),'cohort_entered':sorted(cohort_entered)},indent=2))
|
||||
print(target)
|
||||
if __name__=='__main__':
|
||||
if sys.argv[1:]==['observe']:observe()
|
||||
elif sys.argv[1:]==['snapshot']:print(json.dumps({'time':snapshot()['time']}))
|
||||
elif sys.argv[1:]==['report-preview']:report(True)
|
||||
elif sys.argv[1:]==['report']:report()
|
||||
else:raise SystemExit('observe | snapshot | report | report-preview')
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-off native plan setup and read-only diagnostics; never prints credentials."""
|
||||
from pathlib import Path
|
||||
import datetime,json,os,re,subprocess,time
|
||||
from zoneinfo import ZoneInfo
|
||||
BASE=Path(__file__).resolve().parent
|
||||
ROOT=BASE.parents[3]
|
||||
CONF=Path('/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf')
|
||||
TZ=ZoneInfo('Europe/Berlin')
|
||||
DATE=20261102
|
||||
FORMATION=6
|
||||
ACTIVITY=7
|
||||
RAID=[159,160,161,162,163,164,165,166,167,176]
|
||||
MEET={'alliance':'the_gilded_rose','horde':'poi_npc_287214'}
|
||||
def now():return datetime.datetime.now(TZ)
|
||||
def query(kind,sql):
|
||||
if not re.match(r'^\s*(SELECT|SHOW)\b',sql,re.I):raise ValueError('Read-only database helper')
|
||||
conf=CONF.read_text();value=re.search(r'^'+kind+r'DatabaseInfo\s*=\s*"([^"]+)"',conf,re.M).group(1)
|
||||
h,port,u,password,db=value.split(';');env=os.environ.copy();env['MYSQL_PWD']=password
|
||||
r=subprocess.run(['mysql','--protocol=TCP','--host='+h,'--port='+port,'--user='+u,'--database='+db,'--batch','--default-character-set=utf8mb4','--connect-timeout=10'],input=sql,text=True,capture_output=True,env=env,timeout=25)
|
||||
if r.returncode:raise RuntimeError(r.stderr.replace(password,'[redacted]'))
|
||||
lines=r.stdout.splitlines()
|
||||
return [dict(zip(lines[0].split('\t'),line.split('\t'))) for line in lines[1:]] if lines else []
|
||||
def command(text):
|
||||
subprocess.run(['tmux','send-keys','-t','vcore-worldserver','-l',text],check=True,timeout=5)
|
||||
subprocess.run(['tmux','send-keys','-t','vcore-worldserver','Enter'],check=True,timeout=5)
|
||||
def console(lines=2000):
|
||||
return subprocess.check_output(['tmux','capture-pane','-p','-J','-t','vcore-worldserver','-S',str(-lines)],text=True,timeout=10)
|
||||
def roster():
|
||||
rows=query('Character',"SELECT c.guid,c.name,c.race,c.level,c.online,c.map,c.instance_id,c.position_x,c.position_y,c.position_z,HEX(COALESCE(g.profile_lua,'')) profile FROM characters c LEFT JOIN vcore_adventure_generated_bot g ON g.character_guid=c.guid WHERE c.online=1 AND (c.guid=26 OR g.state='ready') ORDER BY c.guid;")
|
||||
for r in rows:
|
||||
for k in ['guid','race','level','online','map','instance_id']:r[k]=int(r[k])
|
||||
r['faction']='alliance' if r['race'] in [1,3,4,7,11,22] else 'horde'
|
||||
profile=bytes.fromhex(r.pop('profile')).decode(errors='replace')
|
||||
role=re.search(r'\brole\s*=\s*["\'](tank|healer|damage)',profile)
|
||||
r['role']=role[1] if role else ('tank' if r['guid']==26 else 'unknown')
|
||||
return rows
|
||||
def plans():return query('World',f'SELECT character_guid,hour_slot,intent,location_key FROM vcore_adventure_day WHERE local_date={DATE} AND hour_slot IN({FORMATION},{ACTIVITY}) ORDER BY character_guid,hour_slot;')
|
||||
def apply():
|
||||
rows=roster();p=BASE/'before-plans.json'
|
||||
if not p.exists():p.write_text(json.dumps(plans(),indent=2))
|
||||
current={(int(r['character_guid']),int(r['hour_slot'])):r for r in plans()}
|
||||
pending=[]
|
||||
for r in rows:
|
||||
g=r['guid']
|
||||
if r['level']<15:continue
|
||||
a=current.get((g,ACTIVITY),{});f=current.get((g,FORMATION),{})
|
||||
valid=a.get('intent') in ['raid','instance'] and f.get('intent')=='groupformation' and f.get('location_key')==MEET[r['faction']]
|
||||
if g in RAID:valid=valid and a.get('location_key')=='instance_visit_at_6354_d0' and a.get('intent')=='raid'
|
||||
if valid:continue
|
||||
pending.append(g)
|
||||
target='raid instance_visit_at_6354_d0' if g in RAID else 'instance-or-raid auto'
|
||||
command(f'clientless adventure editplan {g} {ACTIVITY} {target} both 15 85 apply')
|
||||
command(f'clientless adventure editplan {g} {FORMATION} groupformation {MEET[r["faction"]]} {r["faction"]} 15 85 apply')
|
||||
time.sleep(2 if pending else .1)
|
||||
current=plans();lookup={(int(r['character_guid']),int(r['hour_slot'])):r for r in current}
|
||||
eligible=[];missing=[]
|
||||
for r in rows:
|
||||
if r['level']<15:continue
|
||||
g=r['guid'];a=lookup.get((g,ACTIVITY),{});f=lookup.get((g,FORMATION),{})
|
||||
good=f.get('intent')=='groupformation' and f.get('location_key')==MEET[r['faction']] and a.get('intent') in ['raid','instance']
|
||||
if g in RAID:good=good and a.get('location_key')=='instance_visit_at_6354_d0' and a.get('intent')=='raid'
|
||||
(eligible if good else missing).append(g)
|
||||
manifest={'updated_at':now().isoformat(),'date':DATE,'formation_hour':FORMATION,'activity_hour':ACTIVITY,'evaluate_at':'2026-11-02T08:00:00+01:00','raid_guids':RAID,'meeting_locations':MEET,'roster':rows,'eligible_guids':eligible,'missing_plan_guids':missing,'excluded_under15':[r['guid'] for r in rows if r['level']<15],'plans':current}
|
||||
(BASE/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2))
|
||||
(BASE/('plan-apply-'+now().strftime('%H%M%S')+'.log')).write_text(console())
|
||||
print(json.dumps({'online':len(rows),'configured':len(eligible),'missing':missing,'newly_applied':pending,'excluded_under15':manifest['excluded_under15'],'raid_online':sorted(set(RAID)&{r['guid'] for r in rows})}))
|
||||
return manifest
|
||||
if __name__=='__main__':
|
||||
import sys
|
||||
if sys.argv[1:] == ['apply']:apply()
|
||||
elif sys.argv[1:]==['roster']:print(json.dumps(roster(),ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,36 @@
|
||||
# Reisekosten
|
||||
|
||||
Der Planer bewertet Strecken nach Entfernung × Verkehrsmittel-Multiplikator.
|
||||
Geschwindigkeitsunterschiede werden wie vom Nutzer gewünscht ausschließlich über
|
||||
die Multiplikatoren abgebildet. Es wird kein Reisezeitmodell verwendet.
|
||||
|
||||
Aufgezeichnete Bodenrouten verwenden ihre Linienlänge einschließlich der
|
||||
Anschlüsse. Taxis verwenden native Flugpfadpunkte statt Fahrpreis oder alter
|
||||
Pauschalkosten (Sturmwind–Eisenschmiede, Kante 15: 6457.25 Yards).
|
||||
Schiffe, Zeppeline und Tram verwenden native Transportgeometrie; Kartenwechsel
|
||||
und Teleports erzeugen keine künstlichen Koordinatensprünge. Statische
|
||||
Transportanimationen berücksichtigen Spawnposition und ParentRotation.
|
||||
Freiflug und lokale Anschlüsse verwenden ihre geometrische Entfernung.
|
||||
Portale erhalten eine Basis von 1, da die Entfernung zwischen Karten keinen
|
||||
sinnvollen physischen Reiseweg beschreibt. Unmessbare Verbindungen werden nicht
|
||||
mit erfundenen niedrigen Kosten bevorzugt. Direkter Flug konkurriert mit dem
|
||||
Graphen, statt unabhängig von dessen Kosten sofort ausgewählt zu werden.
|
||||
|
||||
Konfiguration: `Clientless.Travel.CostMultiplier.Ground`, `.Taxi`, `.Flight`,
|
||||
`.Ship`, `.Zeppelin`, `.Tram`, `.Portal`; derzeit alle 1. Faktoren werden auf
|
||||
0.01 bis 100 begrenzt. `Clientless.Travel.CostVariation` ist derzeit 0.20
|
||||
(zulässig 0 bis 0.90). Pro Reise wird jeder Verkehrsmittelfaktor einmal unabhängig
|
||||
variiert; innerhalb der Reise bleiben die Gewichte stabil. Ein neues Ziel oder
|
||||
eine nach Abschluss begonnene Reise erhält ein neues Profil. Fehlgeschlagene
|
||||
Wiederholungen und Recovery derselben Reise behalten das bestehende Profil.
|
||||
|
||||
Gruppenmitglieder übernehmen Route und Kostenprofil des Anführers. Auch
|
||||
Anschlussplanung und Recovery verwenden dieses Profil. Native Logs enthalten
|
||||
Journey-Seeds und Faktoren zur Überprüfung; eine gemeinsame Makroroute garantiert
|
||||
keine räumlich versetzte Kolonnenbewegung.
|
||||
|
||||
Validierung: acht gezielte Regressionstests bestanden, CMake-Build mit -j10 und
|
||||
Installation erfolgreich. Native Taxi-/Transportentfernungen und gemeinsame
|
||||
Journey-Seeds wurden beobachtet. Der Gruppentest 02.11.2026 um 06/07 Uhr prüft
|
||||
die tatsächlichen Reiseergebnisse; erfolgreicher Build ist keine Gameplay-Abnahme.
|
||||
Build-/Testlogs: `checkpoints/2026-11-02-travel-costs/`.
|
||||
@@ -214,6 +214,35 @@ bool ClientlessActionExecutorMgr::OwnsActiveFlight(uint32 characterGuid) const
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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.')
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Measured production edge lengths and stable per-journey modal preferences."""
|
||||
from pathlib import Path
|
||||
import subprocess, tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
|
||||
a=s.index('void ClientlessTravelGraphMgr::RebuildEdgeDistances(')
|
||||
b=s.index('\nstd::string ClientlessTravelGraphMgr::FindNearestStartNode(',a)
|
||||
code=r'''
|
||||
#include "ClientlessTravelCost.h"
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
using uint32=unsigned;
|
||||
#define TC_LOG_DEBUG(...) ((void)0)
|
||||
struct Node{unsigned MapId=0;float X=0,Y=0,Z=0;};
|
||||
struct Point:Node{float O=0;};
|
||||
struct Edge{unsigned DatabaseId=0;std::string FromNode,ToNode,Type;float Cost=99999;bool Reverse=false;};
|
||||
struct NativePoint{unsigned ContinentID;struct{float X,Y,Z;}Loc;};
|
||||
using TaxiPathNodeList=std::vector<NativePoint const*>;
|
||||
std::vector<TaxiPathNodeList>sTaxiPathNodesByPath(2);
|
||||
struct Objects{void GetTaxiPath(unsigned,unsigned,unsigned&p,unsigned&fare){p=1;fare=98765;}}objects;auto sObjectMgr=&objects;
|
||||
struct Transports{static bool IsTransportEdge(std::string const&t){return t=="ship"||t=="tram";}float ConnectionDistance(std::string const&,std::string const&){return 777;}}transports;
|
||||
using ClientlessTransportMgr=Transports;auto sClientlessTransportMgr=&transports;
|
||||
struct ClientlessTravelGraphMgr{
|
||||
std::unordered_map<std::string,Node>_nodes;std::unordered_map<std::string,std::vector<Edge>>_edges;
|
||||
std::unordered_map<unsigned,std::vector<Point>>points;
|
||||
auto GetEdgePoints(unsigned id){return points.count(id)?&points[id]:nullptr;}
|
||||
void RebuildEdgeDistances();
|
||||
};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
using namespace ClientlessTravelCost;
|
||||
Profile neutral;
|
||||
auto first=Sample(neutral,1234,.2f),same=Sample(neutral,1234,.2f),other=Sample(neutral,9876,.2f);
|
||||
assert(first.Factors==same.Factors&&first.Factors!=other.Factors);
|
||||
for(float factor:first.Factors)assert(factor>=.8f&&factor<=1.2f);
|
||||
assert(Sample(neutral,1234,0).Factors==neutral.Factors);
|
||||
bool ground=false,taxi=false;
|
||||
for(unsigned seed=1;seed<=200;++seed){auto p=Sample(neutral,seed,.2f);if(p.Cost(Ground,1000)<p.Cost(Taxi,1050))ground=true;else taxi=true;}
|
||||
assert(ground&&taxi); // Repeated journeys can choose different near-equal modes.
|
||||
neutral.Factors[Flight]=2;assert(neutral.Cost(Flight,100)==200);
|
||||
assert(neutral.Cost(Portal,1000000)==1); // Cross-map coordinates never act as travelled distance.
|
||||
ClientlessTravelGraphMgr g;g._nodes["A"]={0,0,0,0};g._nodes["B"]={0,10,0,0};
|
||||
g._nodes["taxi_1"]={0,0,0,0};g._nodes["taxi_2"]={1,100000,0,0};
|
||||
g._edges["A"]={{1,"A","B","ground",99999,false},{2,"A","taxi_2","portal_use",99999,false},
|
||||
{3,"A","B","ship",100,false},{4,"A","taxi_2","ground",1,false}};
|
||||
g._edges["B"]={{1,"B","A","ground",1,true}};
|
||||
g.points[1]={{{0,0,0,0}},{{0,0,10,0}},{{0,10,10,0}},{{0,10,0,0}}};
|
||||
g._edges["taxi_1"]={{5,"taxi_1","taxi_2","taxi",1,false}};
|
||||
NativePoint a{0,{0,0,0}},b{0,{3,4,0}},c{0,{3,4,12}},d{1,{100000,0,0}},e{1,{100003,4,0}};
|
||||
sTaxiPathNodesByPath[1]={&a,&b,&c,&d,&e};g.RebuildEdgeDistances();
|
||||
assert(g._edges["A"][0].Cost==30&&g._edges["B"][0].Cost==30);
|
||||
assert(g._edges["A"][1].Cost==1&&g._edges["A"][2].Cost==777);
|
||||
assert(!std::isfinite(g._edges["A"][3].Cost));
|
||||
assert(g._edges["taxi_1"][0].Cost==22); // Native geometry, not ticket price; no cross-map jump.
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-travel-costs-') as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production measured roads/native taxi paths/portal transitions/transport distance and reproducible per-mode journey variation passed.')
|
||||
Reference in new issue
Block a user