Fix AdventureBot travel recovery, area eligibility and safe build logout
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Waiting to run

This commit is contained in:
Nico committed 2026-10-30 12:43:22 +01:00
1 parent 60cab950bd
commit 649915e575
46 files changed
+1500 -77

No files matched your search

@@ -79,8 +79,15 @@ def export(directory):
4707, 4720, 4737}
outland = {3483, 3521, 3519, 3518, 3522, 3523, 3520, 3703}
cataclysm = {616, 4922, 5034, 5042, 5095, 5389}
# Native city roots still have physical surroundings. These semantic
# catalog parents are editable and never alter the native DBC hierarchy.
city_parents = {1519:12,1537:1,1657:141,1637:14,1638:215,1497:85,
3487:3430,3557:3524,3703:3519,4395:2817}
area_maps = {r[0]:r[1] for r in areas}
for r in sorted(areas):
area, map_id, parent = r[:3]
if not parent and area_maps.get(city_parents.get(area)) == map_id:
parent = city_parents[area]
faction = '1' if area in alliance else '2' if area in horde else 'NULL'
note = 'Racial home region/capital' if faction != 'NULL' else ''
if area == 5146:
@@ -0,0 +1,126 @@
# Gebietskatalog, POI-Zuordnung und sichere Builds
## Befund und Korrektur
Pibwin (GUID88, Stufe18) kündigte ein Abenteuer bei „Dalaran Krasus' Landing“ an.
Der importierte Ort `poi_guard_140` lag tatsächlich auf Karte1 bei5831/508/Z3,55,
Gebiet2078, Zone148 (Dunkelküste). Der Gebietskatalog prüfte deshalb einen
niedrigstufigen Ort, nicht Dalaran. Ursache war die Auswahl des nächsten
`game_tele`-Ankers nach X/Y über verschiedene Kontinente hinweg.
POIs erhalten ihre Karte jetzt über die nativen Gesprächsmenüs der tatsächlich
platzierten NPCs (`creature_template_gossip`, `gossip_menu_option`, `creature`).
Untermenüs werden verfolgt, Zyklen abgefangen. Mehrdeutige oder fehlende native
Zuordnungen werden nicht durch eine Koordinatenvermutung ersetzt. Erst innerhalb
der eindeutigen Karte werden die Bodenhöhe und die Gebietszuordnung bestimmt.
`clientless adventure repairimport` erkennt unveränderte generierte Orte anhand
von Quelle, Name, Position und Geografiemetadaten. Es korrigiert ihre Karte/Höhe/
Gebietsdaten und deaktiviert solche ohne sichere native Zuordnung. Manuell
veränderte Orte sowie Original-POIs und aufgezeichnete Reiserouten bleiben erhalten.
Die Katalogmigration `2026_10_30_00_adventure_city_parents.sql` ordnet native
Stadtwurzeln ihren geografischen Umgebungen zu, insbesondere Dalaran4395 dem
Kristallsangwald2817 und Shattrath3703 Terokkar3519. Auch die acht Volksstädte
bekommen ihre Heimatregion als semantischen Katalog-Elternbereich. Vorhandene
manuelle Elternzuordnungen bleiben bestehen; Karten müssen übereinstimmen.
Neue DBC-Exporte erzeugen dieselben editierbaren Vorgaben. Die native DBC- und
Spielergebietshierarchie wird nicht geändert. Levelminimum, Fraktionssperren und
fehlende Flugvorgaben werden über die vorhandene Katalogvererbung berücksichtigt.
Gruppenpläne prüfen auch das tatsächlich angekündigte nächste Ziel gegen die
Berechtigungen des Bots. Ungültige gespeicherte/manuelle Ziele werden nicht mehr
beworben. Bei Einladungen werden sowohl Anführer als auch Teilnehmer geprüft.
Gespeicherte Tagespläne werden durch die vorhandene Reparatur beim Laden bzw.
Katalogwechsel auf zulässige Ziele umgestellt.
## Verbindlicher Ablauf vor künftigen Builds
1. Den bisherigen Wert von `Clientless.Adventure.Loader.Enabled` sichern, auf0
setzen, `reload config` und `clientless population reload` ausführen. Bei einer
noch laufenden Loader-Operation deren Abschluss abwarten und erneut laden.
2. `clientless logout safe all` ausführen. Der Welt-Thread fordert einmalig einen
sicheren Verhaltensstopp an und lässt die bestehenden Controller weiterlaufen.
Er wartet auf lebende, geladene Bots ohne Kampf, Transportbindung, Taxi, Flug,
Fall, Teleport, Zauber oder aktive Bewegung. Es gibt keinen erzwungenen Logout
nach einer Wartefrist. Menschen und normale Netzwerksitzungen sind ausgenommen.
3. Mit `clientless logout status` warten, bis `pending=0 sessions=0` gemeldet wird.
Erst dann `cmake --build /home/wow_admin/wow_cata_server/build/debug -j10` starten.
Bei blockierten Bots zuerst die Ursache untersuchen; nicht nebenher bauen.
4. Den Loader durch Installation und Neustart hindurch pausiert lassen.
Laufende Binärdateien nicht überschreiben; nach sauberem Serverstopp installieren.
Frischen Ready-Eintrag, Prozess und Listener prüfen.
5. `clientless logout resume` beendet die Sammel-Anforderung, falls derselbe
Weltprozess weiterläuft. Nach Neustart ist sie ohnehin zurückgesetzt.
Bisherige Loader-Konfiguration wiederherstellen, `reload config` und
`clientless population reload` ausführen. Der Loader lädt die Bots wieder
schrittweise. Die Regel ist zusätzlich dauerhaft in `AGENTS.md` festgehalten.
Einzelne Bots können mit `clientless logout safe <guid>` ebenso sicher abgemeldet
werden. Der bisherige unmittelbare Einzel-Logout bleibt als Administrationswerkzeug
bestehen und ist nicht der empfohlene Build-Ablauf.
Beim ersten Übergang auf diesen Ablauf war der sichere Sammel-Befehl noch nicht
installiert. Die vorhandenen sicheren Intent-Stopps dienten zur Abmeldung.
Für zwei im alten Schiffscode festhängende Bots wurde zunächst der bereits fertig
gebaute vorherige Transportfix installiert. Tholgar92 und Lillina118 erreichten
anschließend regulär den Anleger (`Arrived`, `passenger=0`,11:54:06).
Bei den zuletzt stationären Bots Edwin36 und Drakogzug77 wurden vermutete
verwaiste native Kampfzustände separat mit `combatstop` bereinigt; dies ist kein
Bestandteil des neuen automatischen Logout-Verfahrens. Anschließend wurden ihre
sicheren Stopps und Offline-Zustände bestätigt. Vor dem neuen Build waren sämtliche
110 Bots offline, der Loader aus und nur eine normale Spielersitzung aktiv.
## Weltkarte
Der vorhandene GM-Befehl lautet `.cheat explore 1`. Ohne Zielauswahl wird die
eigene Karte aufgedeckt; bei Auswahl eines Spielers dessen Karte. `.cheat explore 0`
löscht die Erkundungsbits wieder. Behoben wurden die bisher falsche Zielperson
und das wirkungslose Löschen per `SetFlag(...,0)`. Der Befehl schreibt jetzt alle
Erkundungsfelder des ausgewählten Spielers korrekt.
## Prüfstand
113 isolierte Clientless-Testprogramme erfolgreich. Neue Prüfungen führen die
produktive POI-Zuordnung, den nativen Logout-Sicherheitscheck und den bestehenden
Kartenbefehl aus. Weitere Prüfungen bestätigen die Katalogmigration einschließlich
Idempotenz und Erhalt manueller Eltern sowie neue DBC-Exporte und Gruppenpläne
mit unzulässigem Folgeziel. Die Integrationsergebnisse nach Installation werden
im Session-Checkpoint und im Abschlussbericht festgehalten.
Build mit bestehender Debug/Ninja-Konfiguration und `-j10` erfolgreich, ohne
parallel eingeloggte Bots. Logs unter `/tmp/vcore-city-safe-logout-*`, Testausgaben
unter `/tmp/vcore-travel-fixes-tests/`. Geografiesnapshots unter
`/tmp/vcore-city-fix-state/` enthalten keine Konfigurationszugangsdaten.
### Bestätigte Integration
- Finaler Testserverstart: PID 446178, frische Ready-Meldung nach 10 Sekunden,
Listener 8085/8087. Beide Builds mit `-j10` bei 0 Bots erfolgreich, ohne OOM-Abbruch.
- Reparatur: 437 native Wächter-POIs, 166 nicht nutzbare Koordinaten übersprungen,
2916 aktive Locations einschließlich Instanzvarianten. Die Höhenprüfung nutzt
zunächst NPC-Höhe +3; bei weit entferntem Bodentreffer folgt eine Probe von +35.
Der nähere Treffer gewinnt. Nahe Innenböden werden beibehalten.
- `poi_guard_140`: Karte 571, X 5831, Y 508,617, Z 657,7, Gebiet 4564,
Zone 4395 Dalaran. Pibwin auf Stufe 18: `own=0 invitation=0`. Sein Tagesblock
12 behält Adventure, bekommt `poi_tele_651` (Blutmythosinsel, niedriger Levelbereich).
- Katalogeltern 4395→2817 und 3703→3519 bestätigt. Policytests bestätigen
geerbte Levelminima 77 bzw. 62 und Flugvorgaben.
- Nodes, Kanten und Transportstopps vor/nach Reparatur bytegleich. Alle 17
vorherigen manuellen Ortsdatensätze unverändert. 236 generierte Datensätze
repariert/deaktiviert, 12 semantische Datensätze ergänzt. Snapshots unter
`/tmp/vcore-city-fix-state/`.
- Sammel-Logout mit Arthoran 26 und Pibwin 88 live ausgeführt: erste Rückmeldung
pending=2/sessions=2/flight=1. Arthorans native Taxiverbindung 2221 endete vor
seinem `CLIENTLESS_SAFE_LOGOUT`, danach Pibwins Bewegungsabschluss und Logout.
Abschluss: `pending=0 sessions=0`. Kein zeitgesteuerter Zwangslogout.
- Sammel-Anforderung mit `logout resume` beendet, ursprünglichen Loaderwert 1
wiederhergestellt, Config und Population neu geladen. Bots kehren schrittweise
zurück; keine neuen Sammel-Logouts nach der Wiederaufnahme. Zielkonfiguration
unverändert: 50 persistente + 5 temporäre Bots pro Fraktion. Abschließend alle
110 Bots online, keine ausstehenden Logins, Loader ohne Fehler.
Die Liveprüfung bestätigt Zielberechtigung und Logoutablauf. Sie ersetzt keine
vollständige Abnahme sämtlicher Reisewege oder aller reparierten POIs. Der
Kartenbefehl wurde am produktiven Handler isoliert geprüft; keinem Spieler wurde
ungefragt seine Karte verändert.
+85
View File
@@ -0,0 +1,85 @@
# Reise- und Gruppenfixes vom 30.10.2026
## Betriebsgrenze
Der Nutzer zeichnet am laufenden Testserver Routen auf. Daher ausschließlich
Quellcode, lokaler Build mit `-j10` und isolierte Tests; **keine Installation,
kein Neustart und keine Änderungen an aufgezeichneten Routen oder Live-Daten**.
Die native Instanz-/Raid-Zulassung bleibt erhalten.
## Änderungen
- Gruppenwiederherstellung: Ein vorhandener Charakter ohne Gilde ist weiterhin
ein gültiges Gruppenmitglied. Nur nicht vorhandene Charaktere werden entfernt;
Rollen, Untergruppe und Mitgliedsflags werden regulär wiederhergestellt.
- Portalplanung: Ein generischer read-only ConditionMgr-Zugriff bewertet die
bereits geladenen Spielerbedingungen für den jeweiligen nativen Portalzauber.
Level, Quests, Gegenstände und weitere Spieleranforderungen einschließlich
AND/OR-Gruppen, Referenzen und negativer Bedingungen werden berücksichtigt.
Die Spielerrolle im nativen Cast bestimmt den geprüften ConditionTarget.
Orts-/Kampf-/Skriptbedingungen bleiben bis zur tatsächlichen Benutzung offen.
Diese dynamische Prüfung erfolgt vor dem Cache der statischen Gebietserlaubnis
und nochmals vor der Benutzung. Keine festen Ersatz-Level, entfernten
Bedingungen oder künstlichen Portalteleports.
- Deckrückkehr: Aufgezeichnete Ausstiege benötigen kein optionales freies Bordnetz.
Auch Kraken und Zeppeline können eine unterbrochene Rampenposition sicher zum
aufgezeichneten Stand korrigieren. Weiterhin nur bei stehendem Transport,
echter Passagierbindung und genügend verbleibender Haltezeit.
- Transportzustand: Ein Fehler bei noch angebundenem Passagier behält die Fahrt
und deren Ziel bei, statt sie pro Tick als neue Fahrt zu übernehmen. Kurze
Wiederholungsverzögerung; sichere Ausstiegsversuche am nächsten passenden Halt.
Die Ausstiegsreserve berücksichtigt auch die aktuelle Steuerungstaktung.
- Verlust beim Ein-/Aussteigen: Der erwartete Passagierzustand wird aus dem
tatsächlichen Fortschritt der aufgezeichneten Übergangspunkte abgeleitet.
Ein beabsichtigtes Erreichen des Landpunkts löst keine Bergung aus; tatsächlicher
Verlust kann die bestehende Rettung zum aufgezeichneten Abfahrtsanleger nutzen.
- Flugstart: Ein unterstützter Bodenstand in einem Gebäude führt zunächst zur
Suche nach einem erreichbaren offenen Abflugpunkt. Bis 200 Yards Suchradius,
vollständiger unverdächtiger MMAP-Bodenweg bis 600 Yards, native Außenbereichs-
und Kollisionsprüfung sowie Gebietskatalog. Erst dort native Flugmount-Auswahl
und Cast. Reitfertigkeit und regionale Flugberechtigungen bleiben erforderlich.
- Landung: Mehrere Ringe bis 180 Yards, vollständiger MMAP-Anschluss bis 600 Yards
zum Ziel. Ein Höhenunterschied allein schließt einen sicheren Bodenanschluss
nicht aus; Wasser-, Kollisions-, Gebiets- und native Pfadprüfungen bleiben.
- Kartengrenzen bleiben echte Portal-/Schiffs-/Zeppelinabschnitte. Flug ersetzt
weder einen Kartenübergang noch eine native Zugangsvoraussetzung.
## Temporäre Diagnose
`ClientlessTravelTelemetry.h` enthält den ausdrücklichen Vermerk
`TEMPORARY_ADVENTURE_TRAVEL_TELEMETRY` / vor Entwicklungsabschluss entfernen.
`TEMP_TRAVEL` protokolliert ausschließlich aktive Instanz-/Raidreisen: Reise-ID,
Bot, Gruppe, Anführer, Ziel, Route/Abschnitte, Transportphasen, Portalankunft,
Flugzustand und Berechtigungen, Start-/Landekandidaten und Ablehnungszähler,
Fortschritt sowie Eingang und das getrennte Ergebnis der nativen Zulassung.
Fortschritt höchstens alle 20 Sekunden, gleiche Ereignisdetails innerhalb einer
Minute unterdrückt, insgesamt höchstens 40 Zeilen je Bot/Minute. Auch wechselnde
Fallback-Ziele setzen dieses Minutenbudget nicht zurück. Offline oder anderer
Intent entfernt die Erfassung. Das Verhalten hängt nicht von der Diagnose ab.
Vor Entwicklungsabschluss diese Instrumentierung und ihre Aufrufstellen entfernen.
## Prüfung
109 Clientless-Testprogramme erfolgreich (vollständiger Lauf), einschließlich
neuer produktiver Methoden-Harnesses für gildenlose Gruppen, Portalbedingungen,
alternative Landeflächen und die begrenzte Telemetrie. Transport-/Abflugtests
wurden um unterbrochene Fahrten, Schiffe ohne Bordnetz, Verluste beim Ein- und
Aussteigen und die Bodenannäherung aus einem Gebäude erweitert.
Die Harnesses verwenden deterministische Welt-/Terrain-Doubles. Sie bestätigen
Entscheidungen und Zustandsübergänge, ersetzen aber keine Prüfung mit echten
VMAP/MMAP-Dateien und fahrenden Schiffen im laufenden Spiel. Durchgängige Reisen
zu allen Instanzzielen und das Verhalten der Gruppenmitglieder müssen nach der
später freigegebenen Installation mit der neuen Telemetrie beobachtet werden.
Build: bestehende Debug/Ninja-Konfiguration und Installationspräfix unverändert.
Erster `-j10`-Lauf durch getöteten Compilerprozess unter Speicherlast abgebrochen;
Wiederholung ebenfalls mit `-j10`. Abschließender `-j10`-Build erfolgreich (exit 0), inklusive der letzten
Bodenstützprüfung; Log `/tmp/vcore-travel-fixes-build-final.log`.
Rohlogs: `/tmp/vcore-travel-fixes-build*.log`, Testlogs und Ergebnisliste unter
`/tmp/vcore-travel-fixes-tests/`. Keine Gameplay-Abnahme behauptet.
Der vorherige Laufzeitabsturz hat keinen erhaltenen vollständigen Stacktrace;
diese Änderungen sind kein Nachweis, dass dessen Ursache behoben ist.
+238
View File
@@ -1,3 +1,241 @@
# COMPLETED Oct30: safe builds, city inheritance, native POI map/height fix
Authorized branch feature/clientless-player-core, expected uncommitted work
includes previous travel fixes and this task. No push or commit requested.
User ALLOWS restart/install again. Permanent new rule in AGENTS.md: before EVERY
build pause loader, request safe logout, let native combat/transport/flight finish,
verify zero CPC sessions; builds ALWAYS -j10. New admin command:
`clientless logout safe all` (or individual guid), `clientless logout status`,
`clientless logout resume`. Native world-thread safety predicate, no timeout force.
Pause loader first; resume clears collection, restore loader setting after work.
Both new -j10 builds PASSED with all bots OFFLINE and no OOM. Final installed
binary includes POI raised-platform height fallback. Full113 tests passed, final
POI height regression also passed. git diff --check clean. Build logs
/tmp/vcore-city-safe-logout-build.log and /tmp/vcore-city-height-build.log.
Final server446178 in tmuxvcore-worldserver; console /tmp/vcore-city-height-start.log.
Fresh ready10sec, ports8085/8087 verified. Final startup proof saved separately
/tmp/vcore-city-fix-state/fresh-startup-evidence.txt because reloadconfig truncates
Server.log. No pending build/install/restart. Prior prebuilt transport fixes were
installed first to release old stranded ship passengers;92/118 Arrived passenger0
at11:54:06. Bootstrap orphan combat flags36/77 were separately cleared with native
combatstop after stationary old-state diagnosis; NOT part of future auto logout.
Pibwin88 lvl18 root cause: guardPOI140 DalaranKrasusLanding was guessed on map1
Darkshore based on closestXY across continents. New AdventurePoiPolicy.h uses
recursive native gossip menus -> spawned NPC map bindings; cycles/ambiguity
closed. Height from native NPC neighborhood, +35 retry only if first floor hit
far>25yd below/reference (floating Dalaran); retain close normal floor. Scoped
repair protects source/name/position/geography and manual edits. Original POI
source table and recorded routes untouched. Native repairimport final437POIs,
166invalid skipped,2916active locations. poi_guard140 now map571 X5831 Y508.617
Z657.7 area4564 zone4395 Dalaran. Live eligibility88 adventure140:own0 invitation0.
Pibwin's preserved12:00Adventure block repaired to poi_tele651(Bloodmyst low-level
Alliance starting region, map530 NOT Outland). No manual test schedule override.
City-parent migration2026_10_30_00_adventure_city_parents.sql APPLIED; exporter
updated likewise. Dalaran4395->CrystalSong2817,Shattrath3703->Terokkar3519 +eight
racial capitals->home regions. Catalog only; operator nonzero parents preserved,
no native DBC/player hierarchy changes. Effective native query at taxi310:
minLevel77/max85/preferFlight1,Arthoran60 denied. GroupPlan validates actualnextgoal
before advertising; CanInvite validates leader +member. Existing .cheat explore1
fixed selected-player target, explore0 clears fields. Tell user: clear selection
then `.cheat explore 1` to reveal own map. Native command fixture passed; no human
exploration fields were altered by us.
Live safe-batch test26+88 passed: pending2/sessions2/flight1 first; native taxi2221
completed for26 BEFORE CLIENTLESS_SAFE_LOGOUT26, then88 motion complete+logout.
Final native pending0 sessions0. Called logoutresume, restored original config
Loader.Enabled1, reloadconfig/populationreload. Targets UNCHANGED50persistent+
5temporary PERFACTION. Loader schema1,busy0,error empty, gradually loading110;
FINAL native population status: ALL110 ONLINE, pools50/5/50/5, pending0 in
every pool, manager110, schema1,busy0,error empty. No new safe-logouts after
resume. Bot operation fully restored, final process446178 alive,branch correct.
Snapshots /tmp/vcore-city-fix-state/: nodes/edges/stops before vsafter EXACTMATCH;
all17 prior manual semantic records unchanged.236 imported rows repaired/disabled,
12 semantic rows added. No production/remote auth changes. Detailed German report
in doc/clientless/2026-10-30-city-catalog-build-policy.md. Earlier historical WIP
and checkpoints below are superseded. Need only final current pool/process/port
check and concise German final response now. Do NOT restart or build again.
# UPDATE current: final POI height correction built, install/restart pending
Builds /tmp/vcore-city-safe-logout-build.log and /tmp/vcore-city-height-build.log
bothPASSED -j10 withallbotsOFFLINE. Full113/113testsPASS; finalnewPoiHeightfixture
passes. NewcitymigrationAPPLIEDandbackupunder/tmp/vcore-city-fix-state/.
InstallednewbatchSafeLogoutcode(firstcurrenttaskbuild),server445223ready11sec,
ports8085/8087,console/tmp/vcore-city-safe-logout-start.log. Ranrepairimport:
421nativePOIs,182skipped,2913active. poi_guard140map571BUTfirstheight184wasunder
floatingcity. AddedAdventurePoiPolicy::Height referenceNPC+3 thenretry+35ONLYif
firstfloorfar>25ydfromNPC,choosecloserfloor (keepsnormalnearbyfloor). Addedscoped
firstNative height correctioncandidate torepaircurrentintermediaterecord; doNOT
manuallypatchcoordinates. Nativeanchor651,nearestservice639,realplatform658.
FinalheightbuildPASSED;server445223shutdown10requestednow. Waitexit,cmakeinstall,
startnewfreshconsolethenready/process/ports. LoaderSTILL0andallCPC0confirmedvia
native`clientless logout safe all` +`logout status`pending0sessions0. NativeAllflag
setoncurrentprocess,willresetafterrestart. NormalhumanTestonlineearliernotCPC.
Nextrequired: finalrestart->repairimportagain->reloadcatalogs->verifypoi140actual
platform/cityandlevel88denied;checkmanualroutes/semanticmanualRows snapshots;
loginPibwin88+Arthoran26 withloader0;eligibility88adventurepoi_guard140mustown0,
planrepaired;nativebatchsafelogout2bots->pending0sessions0;logoutresume;restore
loaderconfigpreviousvalue1 from/tmp/vcore-build-drain-loader-setting.json then
reloadconfig/populationreload;verifynormalgradual110population. Preservefreshready
beforeconfigreloadtruncatesServer.log. Updatedocs/checkpointfinalreply.No commitasked.
# ACTIVE NEW TASK Oct30: city/POI eligibility, safe pre-build logout, map reveal
User now ALLOWS INSTALL/RESTART. New permanent rule: before EVERY build pause
loader, finish combat/flights/transports safely, log out ALL CPC bots, verify0
native sessions. Always j10. Rule appended AGENTS.md. Previous fixes' prebuilt
binary installed after clean shutdown of417346; fresh437976 started ready12sec,
ports8085/8087. Console /tmp/vcore-prebuilt-travel-start.log, native ready in
freshServer.log. Loader currentlyDISABLED (config Enabled0), previous value saved
/tmp/vcore-build-drain-loader-setting.json (value1). Restore only after final
install/restart/native verification. Original110botlist /tmp/vcore-build-drain-bots.json.
19remainingresumedmanuallyafterprebuiltinstall so oldships canfinallyexit;92/118
ARRIVED passenger0 at11:54:06. Most thenstopped/logout; currentremaining36/77
were ground/healthy/stationary butUnsafe stuck. Nativecombatstop requested to
clear suspectedorphancombatstate, inspectresultsbeforelogout/build. Never
force transportdetach or airborne logout. PlayerTest/Allitesterarehumans, leavealone.
RootPibwin88level18: poi_guard_140 namedDalaranKrasusLanding wronglyIMPORTEDon
map1Darkshore XY5831,508Z3.55 area2078zone148! ThusactualAreaCatalogchecks saw
lowlevelzone, notDalaran. Realnativegossipbinding140uniquemap571,39guardanchors.
NewAdventurePoiPolicy.h resolvesrecursivegossipMenu->POI via spawnedNPCmaps,
rejectsambiguous/unbound, choosesnativeheight. AdventureLocationMgr.cpp importer
usesit; repairimport matchesoldgeneratedposition+metadata, preservesmanualedits,
updatesmap/Z/geography anddisablesunchangedunboundlegacyPOIs. Mustrunrepairimport
onTEST afternewbuildinstalled. DoNOTchangeoriginalpoints_of_interest oruserRoutes.
Newworldmigration2026_10_30_00_adventure_city_parents.sql +export_area_catalog.py
curatededitablecatalogcityparents10capitals:4395->2817(DalaranCrystalSong),
3703->3519(ShattrathTerokkar),othershomezones. Parent0only/samemap, preserveoperator
parentassignments. ExistingResolve inheritsmin andflight;testsenhanced. SQLnotyet
applied. GroupPlan disablesrecruitmentforinvalidactualgoal;CanInvitechecksboth
leader/membernativeArea position. Existing.cheat explore1commandbugfix selected
playerfields(andexplore0clearing), no newlyinventedcommandneeded.
NewadminbatchsafeLogout namespaceinCustom/viano_bot_commands.cpp:
clientless logout safe all|<guid>, logout status, logout resume.
WorldScriptUpdate requestsBehavior stoponce, waitsnativealive/inworld/noncombat/
nontransport/nontaxi/nonflying/nonfalling/nonteleport/noncast/nonspline then
StopPlayer atomicallyonworldthread. No timeoutforce. PauseLoaderbeforecommand.
All modecontinuescatchingnewCPCsessions untilresume/restart;futurebuildworkflow
mustusenativepending0+sessions0. Bootstrapcurrentprebuiltlackscommand,uses
Behaviorstop+statusEnabled0thenlogout;twooldshipsfixedviaprebuiltdeployment.
Tests113expected(109previous+POI+safeLogout+cheatExplore+cityCatalog), fullrunbefore
lastfixturefixhad2mockerrors(instance_autoresume/instance_loader missingnewSafe
Update double), nowbothfixedandtargetedpass. NewPOI/area/city/explore/safeallpass,
groupPlan/departure/groupspass. Fullrerunandbuild-j10stillREQUIRED. No newbuildyet
sincebotsneedfinishlogout. SourcechangesuncommittedincludeprevioustaskexpectedWIP.
Next:confirm0CPCbots ->buildj10(noOOMnowbotsout),full113tests;resolvecompilation;
applycitymigration;cleanstop/install/restartloader0;nativeReady/process/ports;
runrepairimport,snapshotmanualroutesnotchanged/Pibwinpoi140now571actualDalaran;
verifyPibwin18deniedandplanrepaired;testnativebatchsafelogoutwith2bots;logoutresume,
restoreloader1reloadconfig/populationreload,verifybotsgraduallynormal. Updatedocs,
reportcommand.cheat explore1plusvalidation. No push/commitasked.
# CURRENT Oct30: travel fixes implemented, DEPLOYMENT HELD BY USER
User is recording routes: NO INSTALL and NO RESTART until expressly released.
Source-only task implemented. Worktree includes expected source/test/doc changes;
no commit/push, DB writes or live route edits in this task. Authorized branch
feature/clientless-player-core. Keep all builds -j10.
Implemented guildless native group restoration; conservative native portal actor
prerequisites BEFORE geographic permission cache and before real portal use;
recorded deck-stand fallback for ships without optional deck meshes; interrupted
passenger transitions retain ride/3-second retry delay; lost-passenger detection
in Boarding/Leaving; exit reserve accounts for update cadence. Flight walks from
indoor/layered starts to verified open MMAP-reachable departure (200yd search,
600yd ground cap), landing alternatives180yd/600yd native MMAP approach, no fixed
5yd height rejection. Native permission/collision/area/raid admission preserved.
Temporary scoped TEMP_TRAVEL instrumentation is header-only, bounded40/bot/min,
20sec progress, deduplicated, includes REMOVE BEFORE DEVELOPMENT COMPLETION.
Details: doc/clientless/2026-10-30-travel-fixes.md.
109/109 clientless test programs passed. Added production-method harnesses for
guildless restore, portal prerequisites, landing alternatives and telemetry;
expanded deck/transition/indoor-departure tests. Final NaN-foot-support regression
passed separately after full suite. git diff --check clean.
Final build PASSED (exit0), existing Debug/Ninja configuration with -j10.
Attempts1–3 had compiler OOM kills; attempt4 and final incremental build passed.
Final exact source guard included: /tmp/vcore-travel-fixes-build-final.log,
4/4 linking worldserver. No CMake config changes. Tests109/109 plus final targeted
flight departure/NaN-foot-support regression passed. Raw earlier build logs
/tmp/vcore-travel-fixes-build{,-2,-3,-4}.log; test results
/tmp/vcore-travel-fixes-tests/results.json. DO NOT INSTALL / RESTART.
Server remains SAME installed binary/process417346 in tmuxvcore-worldserver,
ports8085/8087 checked. No lifecycle action this task. User performs route recording.
Live behavior and end-to-end instance journeys remain unverified until later
authorized deployment/observation. Prior runtime crash root remains unproven.
The entries below are historical checkpoints, not current task instructions.
# Latest: testserver recovered after10:36 Oct30 runtime crash
User asked whether testserver offline. Verified no worldserver/ports; kernel
journal records SIGSEGV of389322 at2026-10-30 10:36:35 inlibstdc++. No kernelOOM
observed for this incident; exact cause not investigated. This happened AFTER
02:20–04:00 observationwindow, not during thattest. Old console tail preserved
/tmp/vcore-flight-regions-group-test-start-20261030.log. No source/build/install.
Restarted SAME installedbinary in freshdedicatedtmuxvcore-worldserver, PID417346.
Freshready11s,processalive,ports8085/8087listening. New console
/tmp/vcore-recovery-20261030-1036.log. Deferredfix/discussioninstructions below
remainunchanged; no nextfix implementation started. Preserveanycoredump forlater
analysis before subsequent crash overwrites it.
# Latest user request: record next-fix scope; discussion only, no implementation yet
User explicitly adds ship boarding/disembarking gaps to the NEXT fix. More
importantly, investigate end-to-end destination reachability of flying instance/
raid bots and groups, not whether they know flight. Existing transitions should
connect Kalimdor/EasternKingdoms/Outland/Northrend for flyers; missing low-level
non-flying Horde Kalimdor↔EK links remain a known exception. Roofs or failure of
one landing candidate must lead to a reachable ground exit/free takeoff area or
alternative safe landing+validated final ground approach, not abandonment or a
replanning loop. Preserve safety/native access checks.
TEMPORARY TELEMETRY expressly authorized for NEXT fix: track flying bots' whole
instance journeys, leaders/followers, routes/map transitions/modes, exact flight
state rejection, takeoff/landing candidates and ground approaches, progress,
fallbacks and entry-vs-admission. Instrumentation MUST carry a clear code comment
that it is TEMPORARY and must be REMOVED BEFORE DEVELOPMENT COMPLETION. Targeted,
bounded/deduplicated logging; no per-tick spam. Not implemented in this turn.
Detailed scope recorded in doc/clientless/group-observation-followups.md.
Previously deferred items1(guildless group restore) and4(portal prerequisites)
remain recorded. Item5(native instance admission restriction) is accepted as
correct, not something to bypass. No source/server changes during discussion.
Existing uncommitted resume/followup-doc changes belong to this discussion.
# Latest user steering: discussion first; defer fixes1+4, accept restriction5
User asks to note reportpoints1(guildlessgrouprestore) and4(portalprerequisites)
for laterfix, not implement now. Reportpoint5(raid admissiondenial) is initially
CORRECT intended instance restriction; do not bypass/remove it. Recorded in
`doc/clientless/group-observation-followups.md`. Current turn is investigation/
explanation of shipstandfallback, fallenpassengerrescue, and flightgroupfailures.
No source/server changes requested atthisstage.
Codeconfirmed:DeckMgrGet supports onlyBravery176310 andtram176080–86. Kraken190536
returnsnull andUpdate returns true BEFORE ReturnToRecordedStand. Thus its stopped
standteleportfallback neverruns, recordedexit startsfrommid-ramp andfails>8yd.
Tholgarremainedpassenger1/transguid17,notactuallydetached. RecoverPassenger entry
is inRiding whenGetTransport becomesnull; no universal Boarding/Leaving lossguard.
ResumePassenger re-adoptsFailedattachedride andresetsprogress, causingretryloop.
Flightrequiredskillsarepresent; failurepoints include map transitions, indoor
starts, rejecteddepartureheights, safe landing/shortapproach. TKsuccess concrete
NAVFLIGHT_LANDED guid74/53 27segments736.81yd error0 andnativeinstanceentries.
Do notpresent allfailedflightstarts asprovenindoors; generalguardmessageambiguous.
# COMPLETED Oct30 04:00CET: flightregions committed + group observation
Observation endedautomatically04:00:00.126CET; monitorPID387861/execcell67325 exited0.
@@ -0,0 +1,123 @@
# Umsetzung begonnen am 30.10.2026
Die unten dokumentierten Besprechungspunkte wurden anschließend zur Umsetzung
freigegeben. Quellcodekorrekturen und Regressionstests sind in Arbeit.
**Installation und Neustart bleiben ausdrücklich gesperrt**, während der Nutzer
Routen aufzeichnet. Details und abschließender Prüfstand folgen in
`2026-10-30-travel-fixes.md`. Native Raid-Zulassung bleibt unverändert.
# Nachverfolgung nach dem Gruppentest vom 30.10.2026
Nutzerentscheidung nach dem Bericht:
- **Berichtspunkt 1, Gruppenwiederherstellung ohne Gilde:** für einen späteren
Fix vormerken. Nicht während der anschließenden Besprechung implementieren.
- **Berichtspunkt 4, Portalvoraussetzungen in der Routenplanung:** ebenfalls
für einen späteren Fix vormerken. Native Zugangsbeschränkungen beibehalten.
- **Berichtspunkt 5, abgewiesener Raidbeitritt:** zunächst als korrekt greifende
Instanzbeitrittsbeschränkung behandeln, nicht als zu beseitigenden Fehler.
## Erklärung der Transportlücke
Der bestehende Rücktransfer zum aufgezeichneten Deckstand liegt hinter der
Deckmodell-Auswahl in `ClientlessTransportDeckMgr::Update`. Modelle werden nur
für Bravery 176310 und Tiefenbahn 176080–176086 geladen. Kraken 190536 fällt vor
Rückwegprüfung und `ReturnToRecordedStand` mit `return true` heraus. Dadurch
startet der Ausstieg direkt, auch wenn der Passagier an einer Zwischenposition
steht. Der generelle Fallback ist somit zu eng an die freie Bordnavigation
gekoppelt und greift auf Kraken nicht. Dasselbe Modell-Gate ist für andere
Transporttypen ohne Decknetz relevant.
Tholgar blieb laut Log mit `passenger=1` und Transport-GUID 17 angebunden. Er war
nicht nachweislich von Bord gefallen. Der Verlust-Fallback setzt erst beim
fehlenden Transport während der Phase Riding `Recovering=true`. Er ist damit
für diesen Fall nicht zuständig. Ein echter Verlust während Boarding/Leaving
ist durch diesen Einstieg ebenfalls nicht allgemein abgedeckt; erwartetes
Absteigen und versehentliches Fallen müssten dort ausdrücklich unterschieden
werden. Nach Failed gibt es derzeit zusätzlich eine Wiederaufnahme des noch
angebundenen Passagiers ohne Fortschrittsübernahme des begonnenen Ausstiegs.
## Erklärung der Flugprobleme
Flugfähigkeit ersetzt keine Kartenübergänge; dafür werden weiterhin Schiffe,
Zeppeline und Portale benötigt. Zusätzlich verlangt der echte Flugstart unter
anderem einen lebenden, kampffreien, nicht fahrenden, nicht fallenden Bot im
Freien. Das Planungsmodell kann einen Flug vorher bereits als günstig einstufen.
Logs zeigen Fehlstarts an Gasthäusern, abgewiesene Abflughöhen und fehlende sichere
Landepunkte. Die allgemeine Fehlermeldung unterscheidet nicht alle Startzustände;
der exakte Zustand ist daher nicht für jede Ablehnung nachgewiesen.
Für Festung der Stürme sind erfolgreiche letzte Flugabschnitte konkret belegt:
unter anderem Nimbin 74 und Noranda 53, je 27 Segmente über 736,81 Yards,
`FLIGHT_LANDED ... error=0.00`, anschließend bestätigter Instanzeintritt.
Das belegt einen funktionierenden konkreten Weg, nicht die Zuverlässigkeit
sämtlicher Start-, Übergangs- und Landekombinationen anderer Ziele.
## Für den nächsten Fix ausdrücklich vorgemerkt
### Ein- und Aussteigen von Schiffen
- Den sicheren Rücktransfer zum aufgezeichneten Deckstand unabhängig davon
ermöglichen, ob für das Fahrzeug bereits ein freies Decknetz vorhanden ist.
Die bestehenden Bedingungen für einen stehenden Transport und eine sichere
Passagierbindung beibehalten.
- Einen unterbrochenen Ausstieg beim nächsten geeigneten Halt sinnvoll fortsetzen
beziehungsweise zuerst zum normalen Stand zurückkehren. Keine dauernde
Wiederaufnahme vom falschen Startpunkt mit derselben Acht-Yards-Ablehnung.
- Verlorene Passagiere auch während Boarding und Leaving erfassen. Einen echten
Sturz vom vorgesehenen Absteigen unterscheiden und bei tatsächlichem Verlust
die bereits vereinbarte Rettung zum aufgezeichneten Anleger einsetzen.
### Durchgängige Instanzanreise flugfähiger Bots und Gruppen
Die Nutzerfrage betrifft die tatsächliche Erreichbarkeit des Ziels, nicht das
Vorhandensein der Flugfähigkeit. Für flugfähige Bots müssen Übergänge zwischen
Kalimdor, den Östlichen Königreichen, Scherbenwelt und Nordend über die vorhandenen
Verbindungen nutzbar sein. Diese Erreichbarkeit ist im nächsten Fix konkret zu
prüfen. Bei nicht flugfähigen Horde-Bots sind fehlende Verbindungen zwischen
Kalimdor und den Östlichen Königreichen weiterhin eine bekannte Einschränkung.
Überdachungen und ein ungeeigneter erster Landeversuch sind keine ausreichende
Begründung, die Reise aufzugeben oder dauerhaft neu zu planen:
- Aus einem überdachten Startbereich zunächst auf gültigem Bodenweg eine
erreichbare freie Abflugfläche suchen und ansteuern, anschließend abheben.
- Am Ziel alternative sichere Landeflächen mit validiertem Bodenanschluss zum
Instanzeingang suchen. Ein einzelner abgewiesener Punkt darf nicht den gesamten
Reiseweg unbrauchbar machen.
- Die Sicherheitsprüfungen für Geometrie, Höhen, Kollisionen, Gebiete und native
Voraussetzungen erhalten. Die Suche und Verkettung der Reiseabschnitte
verbessern, statt Prüfungen abzuschalten oder normale Reisen durch Teleports
zu ersetzen.
- Gruppenanführer und Mitglieder getrennt erfassen: gemeinsame Route,
Kartenwechsel, Anschlussverlust, Zielwechsel und Fortschritt müssen erklärbar
sein. Die tatsächliche Ankunft am Eingang getrennt von einer anschließend
korrekt abgewiesenen Instanzzulassung beurteilen; Berichtspunkt 5 bleibt gültig.
### Temporäre Telemetrie für die Untersuchung
Der Nutzer erlaubt für diesen nächsten Fix ausdrücklich temporäre Telemetrie
im Code. Sie ist jetzt nur vorgemerkt, noch nicht implementiert. Jede entsprechende
Instrumentierung erhält einen eindeutigen Codevermerk, zum Beispiel:
```cpp
// TEMPORARY_ADVENTURE_TRAVEL_TELEMETRY:
// Remove before development completion / vor Entwicklungsabschluss entfernen.
```
Erfassen sollen wir mindestens:
- Bot-, Gruppen- und Anführer-ID sowie eine zusammenhängende Reise-/Abschnitt-ID;
- geplantes Instanzziel, tatsächlich verfolgtes Ziel und Gründe für Zielwechsel;
- Graphroute, Kartenübergänge, gewähltes Reisemittel und konkrete Ablehnungsgründe;
- Zustand bei Flugstart: drinnen/draußen, Kampf, Wasser, Fallen, Taxi, Transport,
Teleport, Reitfertigkeit, gelernte Berechtigungen und gewähltes Reittier;
- gesuchte Abflug- und Landekandidaten, ihre Bodenanschlüsse und Ablehnungsgründe;
- Fortschritt, Stillstand, Wiederholungen, Zeit pro Abschnitt und Fallbacks;
- erreichten Eingang und das davon getrennte Ergebnis der nativen Zulassung.
Die Erfassung gezielt auf die relevanten Instanz-/Raidanreisen begrenzen und
wiederholte identische Meldungen zusammenfassen. Sie soll Ursachen nachvollziehbar
machen, ohne erneut pro Tick tausende gleiche Fehlermeldungen zu erzeugen.
Vor Abschluss der Entwicklung die temporäre Instrumentierung entfernen und dies
als eigenen Abschlussprüfpunkt behandeln.
@@ -0,0 +1,11 @@
-- Semantic containment for cities that are independent roots in AreaTable.dbc.
-- Catalog-only: native area hierarchy and player permissions are unchanged.
-- Preserve existing operator parent assignments; inherited minima cannot weaken.
UPDATE vcore_adventure_area_catalog city
JOIN vcore_adventure_area_catalog region ON region.area_id=CASE city.area_id
WHEN 1519 THEN 12 WHEN 1537 THEN 1 WHEN 1657 THEN 141 WHEN 1637 THEN 14
WHEN 1638 THEN 215 WHEN 1497 THEN 85 WHEN 3487 THEN 3430 WHEN 3557 THEN 3524
WHEN 3703 THEN 3519 WHEN 4395 THEN 2817 END
SET city.parent_area_id=region.area_id
WHERE city.parent_area_id=0 AND city.map_id=region.map_id
AND city.area_id IN(1519,1537,1657,1637,1638,1497,3487,3557,3703,4395);
@@ -966,6 +966,54 @@ bool ConditionMgr::CanHaveConditionType(ConditionSourceType sourceType, Conditio
return true;
}
bool ConditionMgr::IsPlayerMeetingPrerequisiteConditions(ConditionSourceType sourceType, uint32 entry,
Player const* player, uint8 playerTargetMask) const
{
if(!player || sourceType<=CONDITION_SOURCE_TYPE_NONE || sourceType>=CONDITION_SOURCE_TYPE_MAX)return false;
auto list=ConditionStore[sourceType].find(entry);
if(list==ConditionStore[sourceType].end())return true;
ConditionSourceInfo info(const_cast<Player*>(player),const_cast<Player*>(player),const_cast<Player*>(player));
// Three states preserve OR branches containing requirements that cannot yet
// be evaluated at a remote waypoint. Only a proven failure excludes a route.
auto evaluate=[&](auto&& self,ConditionContainer const& conditions,unsigned depth)->int
{
if(depth>32)return -1;
std::map<uint32,int> groups;
for(auto condition:conditions)
{
if(!condition->isLoaded())continue;
auto inserted=groups.emplace(condition->ElseGroup,1);auto& group=inserted.first->second;
if(group==0)continue;
int result=-1;
if(condition->ReferenceId)
{
auto ref=ConditionReferenceStore.find(condition->ReferenceId);
if(ref!=ConditionReferenceStore.end())result=self(self,ref->second,depth+1);
}
else if(!condition->ScriptId && condition->ConditionTarget<3 && (playerTargetMask & (1u<<condition->ConditionTarget)))
{
switch(condition->ConditionType)
{
case CONDITION_ITEM: case CONDITION_ITEM_EQUIPPED:
case CONDITION_REPUTATION_RANK: case CONDITION_TEAM: case CONDITION_SKILL:
case CONDITION_QUESTREWARDED: case CONDITION_QUESTTAKEN: case CONDITION_QUEST_NONE:
case CONDITION_CLASS: case CONDITION_RACE: case CONDITION_ACHIEVEMENT:
case CONDITION_TITLE: case CONDITION_GENDER: case CONDITION_SPELL:
case CONDITION_LEVEL: case CONDITION_QUEST_COMPLETE:
case CONDITION_DAILY_QUEST_DONE: case CONDITION_QUESTSTATE:
result=condition->Meets(info)?1:0;break;
default:break;
}
}
if(result==0)group=0;else if(result<0)group=-1;
}
bool unknown=false;
for(auto const& group:groups){if(group.second==1)return 1;if(group.second<0)unknown=true;}
return groups.empty()?1:unknown?-1:0;
};
return evaluate(evaluate,list->second,0)!=0;
}
bool ConditionMgr::IsObjectMeetingNotGroupedConditions(ConditionSourceType sourceType, uint32 entry, ConditionSourceInfo& sourceInfo) const
{
if (sourceType > CONDITION_SOURCE_TYPE_NONE && sourceType < CONDITION_SOURCE_TYPE_MAX)
@@ -262,6 +262,10 @@ class TC_GAME_API ConditionMgr
bool IsObjectMeetingNotGroupedConditions(ConditionSourceType sourceType, uint32 entry, WorldObject* target0, WorldObject* target1 = nullptr, WorldObject* target2 = nullptr) const;
bool IsMapMeetingNotGroupedConditions(ConditionSourceType sourceType, uint32 entry, Map const* map) const;
bool HasConditionsForNotGroupedEntry(ConditionSourceType sourceType, uint32 entry) const;
// Conservative remote planning check. Evaluate only actor prerequisites;
// spatial/script/other-target requirements remain unknown until actual use.
bool IsPlayerMeetingPrerequisiteConditions(ConditionSourceType sourceType, uint32 entry,
Player const* player, uint8 playerTargetMask) const;
bool IsObjectMeetingSpellClickConditions(uint32 creatureId, uint32 spellId, WorldObject* clicker, WorldObject* target) const;
ConditionContainer const* GetConditionsForSpellClickEvent(uint32 creatureId, uint32 spellId) const;
bool IsObjectMeetingVehicleSpellConditions(uint32 creatureId, uint32 spellId, Player* player, Unit* vehicle) const;
+6 -3
View File
@@ -206,12 +206,11 @@ void Group::LoadGroupFromDB(Field* fields)
void Group::LoadMemberFromDB(ObjectGuid::LowType guidLow, uint8 memberFlags, uint8 subgroup, uint8 roles)
{
MemberSlot member;
MemberSlot member{};
member.guid = ObjectGuid(HighGuid::Player, guidLow);
// skip non-existed member
if (!sCharacterCache->GetCharacterNameByGuid(member.guid, member.name) ||
!sCharacterCache->GetPlayerGuildIdByGUID(member.guid, member.guildId))
if (!sCharacterCache->GetCharacterNameByGuid(member.guid, member.name))
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_GROUP_MEMBER);
stmt->setUInt32(0, guidLow);
@@ -219,6 +218,10 @@ void Group::LoadMemberFromDB(ObjectGuid::LowType guidLow, uint8 memberFlags, uin
return;
}
// A cached character without a guild is still a valid group member.
// The cache query deliberately returns false for GuildId 0.
sCharacterCache->GetPlayerGuildIdByGUID(member.guid, member.guildId);
member.group = subgroup;
member.flags = memberFlags;
member.roles = roles;
@@ -207,6 +207,9 @@ bool AdventureAreaMgr::AllowsEdge(Player const* player,ClientlessTravelEdge cons
if(!Applies(player))return true;
auto from=sClientlessTravelGraphMgr->FindNode(edge.FromNode),to=sClientlessTravelGraphMgr->FindNode(edge.ToNode);
if(!from || !to || !AllowsNode(player,*from) || !AllowsNode(player,*to))return false;
// Items/quests/spells may change without a catalog reload; never cache
// actor prerequisites as geography or test them only after reaching a portal.
if(edge.Type=="portal_use" && !sClientlessTravelGraphMgr->PortalPrerequisites(player,edge.DatabaseId))return false;
std::string key=std::to_string(player->getLevel())+":"+std::to_string(player->GetTeam())+":"+std::to_string(edge.DatabaseId)+":"+edge.FromNode+":"+edge.ToNode;
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
auto cached=_edgeAccess.find(key);if(cached!=_edgeAccess.end())return cached->second;
@@ -1,6 +1,7 @@
#include "ClientlessPositioning.h"
#include "MotionMaster.h"
#include "AdventureAreaMgr.h"
#include "ClientlessTravelTelemetry.h"
#include "ClientlessTravelGraphMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
@@ -225,6 +226,7 @@ void AdventureBehaviorMgr::RefreshDay(Player* player)
void AdventureBehaviorMgr::Offline(Player * player)
{
ClientlessTravelTelemetry::Clear(player);
auto i = _records.find(player->GetGUID().GetCounter());
if (i != _records.end())
{
@@ -457,6 +459,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
r.Dirty = false;
r.Intent = r.WantedIntent;
r.Location = r.WantedLocation;
ClientlessTravelTelemetry::Begin(player,r.Intent,r.Location,r.Deadline);
if (r.Intent.empty())
{
r.Enabled = false;
@@ -1415,6 +1418,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
auto entry = _records.find(guid);
if (entry == _records.end() || ! entry->second.Enabled || ! player->IsInWorld()) return;
auto & r = entry->second;
ClientlessTravelTelemetry::Tick(player);
if(UpdateNavigationRescue(player,r))return;
if(r.AreaRevision!=sAdventureAreaMgr->Revision())
{
@@ -1874,6 +1878,7 @@ bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
if (validExit(id)) {exit=id;break;}
} while(rows->NextRow());
}
ClientlessTravelTelemetry::Note(player,"instance-route","entry="+std::to_string(entry)+" map="+std::to_string(target->target_mapId)+" mode="+std::to_string(selected->InstanceDifficulty));
r.Instance.Active=true;r.Instance.Location=r.Location;r.Instance.Entry=entry;r.Instance.Exit=exit;
r.Instance.Map=target->target_mapId;r.Instance.Difficulty=selected->InstanceDifficulty;r.Instance.BlockEnd=r.Deadline;
r.Instance.LossPercent=std::uniform_int_distribution<uint32>(0,50)(r.Random);
@@ -1932,6 +1937,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
{
v.InstanceId=player->GetInstanceId();v.Stage=Stage::Inside;
if (!SaveInstance(player,r)) {v.Stage=Stage::Entering;return true;}
ClientlessTravelTelemetry::Note(player,"instance-entered","instance="+std::to_string(v.InstanceId));
r.Detail="waiting just inside private instance entrance";
TC_LOG_INFO("server.adventurebot","INSTANCE_ENTERED guid=%u map=%u instance=%u returnAt=%llu mode=%u",
player->GetGUID().GetCounter(),v.Map,v.InstanceId,
@@ -1979,6 +1985,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
if (player->GetMapId()!=v.Map || !player->GetInstanceId() ||
player->GetExactDist(entryTarget->target_X,entryTarget->target_Y,entryTarget->target_Z)>5)
{
ClientlessTravelTelemetry::Note(player,"instance-denied","native admission denied at entry="+std::to_string(v.Entry));
TC_LOG_WARN("server.adventurebot","INSTANCE_ENTRY_DENIED guid=%u entry=%u",player->GetGUID().GetCounter(),v.Entry);
v.Stage=Stage::Rest;SaveInstance(player,r);r.Detail="native instance entry denied; no visit started";
}
@@ -2227,6 +2234,8 @@ AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player)
plan.Forming=plan.Enabled && plan.Intent=="groupformation";
auto activity=plan.Forming?plan.NextIntent:plan.Intent;
plan.Capacity=AdventureGroupPolicy::Capacity(activity);
auto destination=sAdventureLocationMgr->Find(plan.Forming?plan.NextLocation:plan.Location);
if(!destination || !Allowed(player,*destination,activity))plan.Enabled=false;
if(auto location=sAdventureLocationMgr->Find(plan.Forming?plan.NextLocation:plan.Location))
if(activity=="raid" || activity=="instance")
{plan.Capacity=location->InstanceCapacity;plan.Mode=location->InstanceDifficulty;}
+3 -2
View File
@@ -136,10 +136,11 @@ bool AdventureGroupMgr::CanInvite(Player* leader,Player* member)
if(bot->GetSession() && bot->GetSession()->IsClientless() && !AdventureGroupPolicy::AcceptPlayerLevel(bot->getLevel(),member->getLevel()))return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);
auto location=sAdventureLocationMgr->Find(plan.Forming?plan.NextLocation:plan.Location);
if(!location || !sAdventureLocationMgr->LevelEligible(member,*location,Activity(plan),true) ||
if(!plan.Enabled || !location || !sAdventureLocationMgr->LevelEligible(member,*location,Activity(plan),true) ||
!sAdventureLocationMgr->LevelEligible(leader,*location,Activity(plan),true))return false;
Position goal;goal.Relocate(location->Point.X,location->Point.Y,location->Point.Z);
return sAdventureAreaMgr->AllowsPosition(member,location->Point.MapId,goal);
return sAdventureAreaMgr->AllowsPosition(leader,location->Point.MapId,goal) &&
sAdventureAreaMgr->AllowsPosition(member,location->Point.MapId,goal);
}
void AdventureGroupMgr::CheckFullGroup(Player* p)
{
+99 -20
View File
@@ -1,4 +1,5 @@
#include "AdventureAreaMgr.h"
#include "AdventurePoiPolicy.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "AdventureLocationMgr.h"
@@ -183,8 +184,8 @@ bool AdventureLocationMgr::Import(ChatHandler * handler, bool repair)
struct Seed
{
std::string Key, Name, Type, Source;
uint32 Id, Map;
float X, Y, Z, O;
uint32 Id=0, Map=0;
float X=0, Y=0, Z=0, O=0;
uint8 Faction = 0;
};
std::vector < Seed > seeds, tele;
@@ -232,6 +233,21 @@ bool AdventureLocationMgr::Import(ChatHandler * handler, bool repair)
if (! t) t = sTerrainMgr.LoadTerrain(map);
return t ? t->GetAreaId(phase, map, x, y, z) : 0u;
};
// A POI has no map. Recover it through its real gossip menu graph and
// spawned NPCs, never by comparing XY coordinates across continents.
AdventurePoiPolicy::Menus menus;
if(auto rows=WorldDatabase.Query("SELECT MenuID,ActionMenuID,ActionPoiID FROM gossip_menu_option WHERE ActionMenuID>0 OR ActionPoiID>0"))do
{
auto f=rows->Fetch();menus[f[0].GetUInt32()].push_back({f[1].GetUInt32(),f[2].GetUInt32()});
}while(rows->NextRow());
std::vector<AdventurePoiPolicy::Root> roots;
if(auto rows=WorldDatabase.Query("SELECT DISTINCT g.MenuID,c.map,c.position_x,c.position_y,c.position_z FROM creature_template_gossip g JOIN creature c ON c.id=g.CreatureID"))do
{
auto f=rows->Fetch();roots.push_back({f[0].GetUInt32(),{f[1].GetUInt16(),f[2].GetFloat(),f[3].GetFloat(),f[4].GetFloat()}});
}while(rows->NextRow());
auto bindings=AdventurePoiPolicy::Bind(menus,roots);
struct Correction {Seed Old,New;bool Valid=false;};
std::vector<Correction> corrections;
uint32 inferred = 0, skipped = 0;
if (auto rows = WorldDatabase.Query("SELECT ID,Name,PositionX,PositionY FROM points_of_interest ORDER BY ID")) do
{
@@ -258,34 +274,97 @@ bool AdventureLocationMgr::Import(ChatHandler * handler, bool repair)
anchor = & t;
}
}
if (! anchor)
Seed previous=s;previous.Z=INVALID_HEIGHT;
if(anchor)
{
++ skipped;
continue;
}
s.Map = anchor->Map;
auto & t = terrains[s.Map];
if (! t) t = sTerrainMgr.LoadTerrain(s.Map);
Seed const* ground=anchor;
float groundDistance=std::hypot(s.X-anchor->X,s.Y-anchor->Y);
for (auto const& candidate:seeds)
if (candidate.Source=="service_npc" && candidate.Map==s.Map)
previous.Map = anchor->Map;
auto & t = terrains[previous.Map];
if (! t) t = sTerrainMgr.LoadTerrain(previous.Map);
Seed const* ground=anchor;
float groundDistance=std::hypot(previous.X-anchor->X,previous.Y-anchor->Y);
for (auto const& candidate:seeds)
if (candidate.Source=="service_npc" && candidate.Map==previous.Map)
{
float d=std::hypot(previous.X-candidate.X,previous.Y-candidate.Y);
if (d<groundDistance) {groundDistance=d;ground=&candidate;}
}
previous.Z = t ? t->GetStaticHeight(phase, previous.Map, previous.X, previous.Y, ground->Z+3,true,100) : INVALID_HEIGHT;
if (! std::isfinite(previous.Z) || previous.Z <= INVALID_HEIGHT + 1)
{
float d=std::hypot(s.X-candidate.X,s.Y-candidate.Y);
if (d<groundDistance) {groundDistance=d;ground=&candidate;}
previous.Z=INVALID_HEIGHT;
}
s.Z = t ? t->GetStaticHeight(phase, s.Map, s.X, s.Y, ground->Z+3,true,100) : INVALID_HEIGHT;
if (! std::isfinite(s.Z) || s.Z <= INVALID_HEIGHT + 1)
{
++ skipped;
continue;
}
AdventurePoiPolicy::Anchor native;
if(!AdventurePoiPolicy::Select(bindings,s.Id,s.X,s.Y,native))
{corrections.push_back({previous,s,false});++skipped;continue;}
s.Map=native.Map;
auto& nativeTerrain=terrains[s.Map];if(!nativeTerrain)nativeTerrain=sTerrainMgr.LoadTerrain(s.Map);
float height=native.Z,nearest=std::hypot(s.X-native.X,s.Y-native.Y);
for(auto const& candidate:seeds)if(candidate.Source=="service_npc" && candidate.Map==s.Map)
{
float distance=std::hypot(s.X-candidate.X,s.Y-candidate.Y);
if(distance<nearest){nearest=distance;height=candidate.Z;}
}
Seed firstNative=s;
firstNative.Z=nativeTerrain?nativeTerrain->GetStaticHeight(phase,s.Map,s.X,s.Y,height+3,true,100):INVALID_HEIGHT;
s.Z=AdventurePoiPolicy::Height(height,INVALID_HEIGHT,[&](float from)
{return nativeTerrain?nativeTerrain->GetStaticHeight(phase,s.Map,s.X,s.Y,from,true,100):INVALID_HEIGHT;});
if(!std::isfinite(s.Z) || s.Z<=INVALID_HEIGHT+1)
{corrections.push_back({previous,s,false});++skipped;continue;}
corrections.push_back({previous,s,true});
if(std::isfinite(firstNative.Z) && firstNative.Z>INVALID_HEIGHT+1 && std::fabs(firstNative.Z-s.Z)>0.01f)
corrections.push_back({firstNative,s,true});
seeds.push_back(s);
++ inferred;
}
while (rows->NextRow());
auto transaction = WorldDatabase.BeginTransaction();
uint32 count = 0, areas = 0;
if(repair)for(auto const& correction:corrections)
{
auto const& old=correction.Old;auto const& next=correction.New;
if(!std::isfinite(old.Z) || old.Z<=INVALID_HEIGHT+1)continue;
std::string key=old.Key,name=old.Name;
WorldDatabase.EscapeString(key);WorldDatabase.EscapeString(name);
uint32 oldArea=areaAt(old.Map,old.X,old.Y,old.Z),oldZone=oldArea;
if(auto entry=sAreaTableStore.LookupEntry(oldArea))if(entry->ParentAreaID)oldZone=entry->ParentAreaID;
std::string oldRegion;uint32 oldFaction=0;
if(auto entry=sAreaTableStore.LookupEntry(oldZone))
{
oldRegion=Normalize(entry->AreaName?entry->AreaName:"");
auto mask=entry->FactionGroupMask & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE);
oldFaction=mask==FACTION_GROUP_MASK_ALLIANCE?1:mask==FACTION_GROUP_MASK_HORDE?2:0;
}
if(oldZone==12)oldRegion="elwynn";else if(oldZone==1)oldRegion="dun_morogh";else if(oldZone==1519)oldRegion="stormwind";else if(oldZone==4)oldRegion="blasted_lands";
bool oldIsArea=false;
if(auto entry=sAreaTableStore.LookupEntry(oldZone))oldIsArea=Normalize(old.Name)==Normalize(entry->AreaName?entry->AreaName:"");
std::string oldType=oldIsArea?"zone":Classify(old.Name);
WorldDatabase.EscapeString(oldRegion);WorldDatabase.EscapeString(oldType);
std::string guard=" AND a.area_id="+std::to_string(oldArea)+" AND a.zone_id="+std::to_string(oldZone)+
" AND a.region_key='"+oldRegion+"' AND a.faction="+std::to_string(oldFaction)+
" AND a.is_area="+std::to_string(oldIsArea?1:0)+" AND w.category='"+oldType+"'";
// Match the old generated position and source exactly. Manually moved
// locations, transport recordings and original POI tables stay untouched.
if(!correction.Valid)
{
transaction->PAppend("UPDATE vcore_clientless_world_point w JOIN vcore_adventure_location a USING(point_key) SET w.enabled=0 WHERE w.point_key='%s' AND a.source_type='guard_poi_inferred' AND a.source_id=%u AND a.display_name='%s' AND w.map=%u AND ABS(w.position_x-%.6f)<0.001 AND ABS(w.position_y-%.6f)<0.001 AND ABS(w.position_z-%.6f)<0.01%s",key.c_str(),old.Id,name.c_str(),old.Map,old.X,old.Y,old.Z,guard.c_str());
continue;
}
uint32 area=areaAt(next.Map,next.X,next.Y,next.Z),zone=area;
if(auto entry=sAreaTableStore.LookupEntry(area))if(entry->ParentAreaID)zone=entry->ParentAreaID;
std::string region;
uint32 faction=0;
if(auto entry=sAreaTableStore.LookupEntry(zone))
{
region=Normalize(entry->AreaName?entry->AreaName:"");
auto mask=entry->FactionGroupMask & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE);
faction=mask==FACTION_GROUP_MASK_ALLIANCE?1:mask==FACTION_GROUP_MASK_HORDE?2:0;
}
if(zone==12)region="elwynn";else if(zone==1)region="dun_morogh";else if(zone==1519)region="stormwind";else if(zone==4)region="blasted_lands";
WorldDatabase.EscapeString(region);
transaction->PAppend("UPDATE vcore_clientless_world_point w JOIN vcore_adventure_location a USING(point_key) SET w.map=%u,w.position_z=%.6f,a.area_id=%u,a.zone_id=%u,a.region_key='%s',a.faction=%u WHERE w.point_key='%s' AND a.source_type='guard_poi_inferred' AND a.source_id=%u AND a.display_name='%s' AND w.map=%u AND ABS(w.position_x-%.6f)<0.001 AND ABS(w.position_y-%.6f)<0.001 AND ABS(w.position_z-%.6f)<0.01%s",next.Map,next.Z,area,zone,region.c_str(),faction,key.c_str(),old.Id,name.c_str(),old.Map,old.X,old.Y,old.Z,guard.c_str());
}
auto metadata =[&](Seed const & s, bool isArea, uint32 area, uint32 zone)
{
std::string key = s.Key, name = s.Name, source = s.Source, region;
@@ -0,0 +1,64 @@
#ifndef VCORE_ADVENTURE_POI_POLICY_H
#define VCORE_ADVENTURE_POI_POLICY_H
#include <cmath>
#include <limits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace AdventurePoiPolicy
{
struct Anchor { unsigned Map=0;float X=0,Y=0,Z=0; };
struct Root { unsigned Menu=0;Anchor Position; };
struct Edge { unsigned Menu=0,Poi=0; };
using Menus=std::unordered_map<unsigned,std::vector<Edge>>;
using Bindings=std::unordered_map<unsigned,std::vector<Anchor>>;
inline Bindings Bind(Menus const& menus,std::vector<Root> const& roots)
{
Bindings bindings;
for(auto const& root:roots)
{
std::vector<unsigned> pending{root.Menu};std::unordered_set<unsigned> seen;
while(!pending.empty())
{
unsigned menu=pending.back();pending.pop_back();
if(!seen.insert(menu).second)continue;
auto it=menus.find(menu);if(it==menus.end())continue;
for(auto const& edge:it->second)
{
if(edge.Poi)bindings[edge.Poi].push_back(root.Position);
if(edge.Menu)pending.push_back(edge.Menu);
}
}
}
return bindings;
}
template<class Sample>
inline float Height(float reference,float invalid,Sample&& sample)
{
float floor=sample(reference+3);
auto valid=[&](float z){return std::isfinite(z) && z>invalid+1;};
// A nearby NPC may be at the foot of a raised platform. A low query can
// miss the floating city's collision and hit terrain hundreds of yards below.
// Keep the close lower-floor hit; otherwise retry above the NPC's level.
if(!valid(floor) || std::fabs(floor-reference)>25)
{
float raised=sample(reference+35);
if(valid(raised) && (!valid(floor) || std::fabs(raised-reference)<std::fabs(floor-reference)))floor=raised;
}
return floor;
}
inline bool Select(Bindings const& bindings,unsigned poi,float x,float y,Anchor& chosen)
{
auto it=bindings.find(poi);if(it==bindings.end() || it->second.empty())return false;
unsigned map=it->second.front().Map;float nearest=std::numeric_limits<float>::infinity();
for(auto const& anchor:it->second)
{
// Ambiguous native associations must not silently choose another continent.
if(anchor.Map!=map)return false;
float distance=std::hypot(anchor.X-x,anchor.Y-y);
if(distance<nearest){nearest=distance;chosen=anchor;}
}
return std::isfinite(nearest);
}
}
#endif
@@ -1,3 +1,4 @@
#include "ClientlessTravelTelemetry.h"
#include "AdventureAreaMgr.h"
#include "Group.h"
#include "ObjectAccessor.h"
@@ -996,6 +997,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
step.NodeKey.c_str(), step.NodeType.c_str(), step.EdgeType.c_str(),
step.EdgeDatabaseId, step.EdgeReverse ? 1u : 0u, record.RecoveryCount);
ClientlessTravelTelemetry::Note(player,"route-step","step="+std::to_string(record.RouteStep)+"/"+std::to_string(record.Route.size())+" node="+step.NodeKey+" kind="+step.EdgeType+" map="+std::to_string(step.MapId));
if (step.EdgeType == "free_flight")
{
std::string error;
@@ -1025,6 +1027,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
if (!validSource || step.MapId!=player->GetMapId() ||
!sClientlessFlightMgr->Start(player,target,error))
{
ClientlessTravelTelemetry::Note(player,"flight-start-rejected",error+" "+ClientlessTravelTelemetry::State(player));
return RecoverFlightLeg(player,characterGuid,record,error.empty()?"free flight source not physically reached":error);
}
record.FlightActive=true;record.Active=true;
@@ -1565,6 +1568,7 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
auto const* portal = sClientlessTravelGraphMgr->GetPortalUse(step.EdgeDatabaseId);
auto fail = [&](char const* reason)
{
ClientlessTravelTelemetry::Note(player,"portal-rejected",reason);
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_FAILED guid=%u edge=%u reason='%s'",
characterGuid, step.EdgeDatabaseId, reason);
record.PortalUseActive = false;
@@ -1573,6 +1577,8 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
record.Status.Detail = reason;
return false;
};
if(portal && !sClientlessTravelGraphMgr->PortalPrerequisites(player,step.EdgeDatabaseId))
return fail("native portal prerequisites not satisfied");
if (!portal || !record.RouteStep || step.EdgeReverse ||
player->GetMapId() != portal->Source.MapId || !player->IsAlive() ||
player->IsBeingTeleported() || player->GetTransport() ||
@@ -1647,6 +1653,7 @@ void ClientlessActionExecutorMgr::UpdatePortalUse(Player* player,
!player->IsBeingTeleported() && player->GetMapId() == step.MapId && arrivalError <= 5.0f;
if (arrived)
{
ClientlessTravelTelemetry::Note(player,"portal-arrived","node="+step.NodeKey+" "+ClientlessTravelTelemetry::State(player));
TC_LOG_INFO("server.adventurebot.navigation", "EDGE_COMPLETE guid=%u name='%s' edgeType='portal_use' edgeDbId=%u node='%s' spawn=%u map=%u arrivalError=%.2f recovery=%u",
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId, step.NodeKey.c_str(),
record.PortalUseSpawnId, player->GetMapId(), arrivalError, record.RecoveryCount);
@@ -1911,6 +1918,7 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
std::string error;
if (!sClientlessFlightMgr->Start(player, target, error))
{
ClientlessTravelTelemetry::Note(player,"flight-fallback-rejected",error+" "+ClientlessTravelTelemetry::State(player));
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_FALLBACK_UNAVAILABLE guid=%u reason='%s'",
characterGuid, error.c_str());
return false;
+66 -26
View File
@@ -1,4 +1,5 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelTelemetry.h"
#include "ClientlessFlightMgr.h"
#include <cstring>
#include "ClientlessLuaEngine.h"
@@ -319,9 +320,10 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
error = "invalid free-flight destination coordinates";
return false;
}
if (! player || ! player->IsInWorld() || ! Enabled(player) || ! player->IsAlive() || player->GetMap()->GetEntry()->MapType != MAP_COMMON || player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported() || player->IsInCombat() || ! player->IsOutdoors() || player->IsInWater() || player->IsFalling())
if (! player || ! player->IsInWorld() || ! Enabled(player) || ! player->IsAlive() || player->GetMap()->GetEntry()->MapType != MAP_COMMON || player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported() || player->IsInCombat() || player->IsInWater() || player->IsFalling())
{
error = "free flight unavailable in current player/location state";
if(player)ClientlessTravelTelemetry::Note(player,"flight-state-rejected",ClientlessTravelTelemetry::State(player));
return false;
}
if (!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,target))
@@ -337,6 +339,29 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
error = "battlefield currently forbids flying";
return false;
}
if(((player->GetMapId()==0 || player->GetMapId()==1) && !player->HasSpell(90267)) ||
(player->GetMapId()==571 && !player->HasSpell(54197)))
{error="native regional flying license missing";return false;}
Record record;
auto previous=_records.find(player->GetGUID().GetCounter());
if(previous!=_records.end() && previous->second.ApproachingDeparture)
record.Published=previous->second.Published;
record.Status.MapId = player->GetMapId();
record.Status.Target = target;
record.Start.Relocate(player);
record.LastPosition.Relocate(player);
record.Settings.Clearance = std::clamp(sConfigMgr->GetFloatDefault("Clientless.Flight.Clearance", 60), 20.0f, 200.0f);
auto origin = Sample(player, record, player->GetPositionX(), player->GetPositionY());
float support=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
bool supported=std::isfinite(support) && support>INVALID_HEIGHT+1 && std::fabs(support-player->GetPositionZ())<=3;
if(supported && (!player->IsOutdoors() || !origin.Valid || std::fabs(origin.Ground-player->GetPositionZ())>3 ||
!OpenColumn(player,player->GetPositionX(),player->GetPositionY(),origin)))
{
if(ApproachDeparture(player,record,following))return true;
error="departure-unavailable: no reachable open takeoff point";
ClientlessTravelTelemetry::Note(player,"flight-departure-rejected",error+" "+ClientlessTravelTelemetry::State(player));
return false;
}
auto const * profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
std::vector < std::pair < uint32, uint32 >> candidates;
uint32 total = 0;
@@ -362,30 +387,27 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
if (roll <= candidate.second) break;
roll -= candidate.second;
}
Record record;
auto previous=_records.find(player->GetGUID().GetCounter());
if(previous!=_records.end() && previous->second.ApproachingDeparture)
record.Published=previous->second.Published;
record.Status.MapId = player->GetMapId();
record.Status.Target = target;
record.Start.Relocate(player);
record.LastPosition.Relocate(player);
record.Settings.Clearance = std::clamp(sConfigMgr->GetFloatDefault("Clientless.Flight.Clearance", 60), 20.0f, 200.0f);
auto origin = Sample(player, record, player->GetPositionX(), player->GetPositionY());
uint32 existingMount=0;
if(player->IsMounted() && player->CanFly())
for(auto const& candidate:candidates)if(player->HasAura(candidate.first)){existingMount=candidate.first;break;}
bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f &&
player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5});
float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
if (!origin.Valid || (!airborne && (std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3)))
if (!origin.Valid || (!airborne && (!std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3)))
{
if(origin.Valid && std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true;
if(std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true;
error = "departure-unavailable: no supported open takeoff point";
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u mounted=%u canFly=%u",player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,origin.Valid?1:0,player->IsMounted()?1:0,player->CanFly()?1:0);
return false;
}
bool foundLanding = false;
std::array<uint32,5> landingRejected{}; // surface, water, column, MMAP, area
auto landingSummary=[&]()
{
std::string detail="rejected surface/water/column/mmap/area=";
for(auto count:landingRejected)detail+=std::to_string(count)+"/";
ClientlessTravelTelemetry::Note(player,"flight-landing-search",detail);
};
if(following)
{
auto surface=Sample(player,record,target.GetPositionX(),target.GetPositionY());
@@ -395,7 +417,7 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
}
if(!following) for (float radius :
{
0.0f, 6.0f, 12.0f, 20.0f
0.0f, 6.0f, 12.0f, 20.0f, 40.0f, 80.0f, 120.0f, 180.0f
})
{
for (unsigned i = 0; i <(radius == 0 ? 1u : 12u); ++ i)
@@ -403,27 +425,31 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
float angle = float(i) * 6.283185307f / 12;
float x = target.GetPositionX() + radius * std::cos(angle), y = target.GetPositionY() + radius * std::sin(angle);
auto landing = Sample(player, record, x, y);
if (! landing.Valid || std::fabs(landing.Ground - target.GetPositionZ()) > 5) continue;
if (! landing.Valid){++landingRejected[0];continue;}
float solid = player->GetMap()->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
if (landing.Ground > solid + 0.7f) continue;
if (landing.Ground > solid + 0.7f){++landingRejected[1];continue;}
// water is no solid touchdown
if(!OpenColumn(player,x,y,landing,record.Settings.Clearance))continue;
if(!OpenColumn(player,x,y,landing,record.Settings.Clearance)){++landingRejected[2];continue;}
Position candidate;
candidate.Relocate(x, y, landing.Ground, target.GetOrientation());
if (radius > 0 || std::fabs(landing.Ground - target.GetPositionZ()) > 0.7f)
{
auto path = PathGenerator::AnalyzePositions(player->GetMapId(), player->GetMapId(), player->GetInstanceId(), candidate, target, NAV_GROUND);
if (! path.IsComplete() || path.FinalDistance > 1.5f || path.PathLength > 120) continue;
if (! path.IsComplete() || path.FinalDistance > 1.5f || path.PathLength > 600){++landingRejected[3];continue;}
}
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),candidate,target)){++landingRejected[4];continue;}
ClientlessTravelTelemetry::Note(player,"flight-landing","radius="+std::to_string(radius)+" xyz="+std::to_string(x)+","+std::to_string(y)+","+std::to_string(landing.Ground));
record.Status.Landing = candidate;
foundLanding = true;
break;
}
if (foundLanding) break;
}
landingSummary();
if (! foundLanding)
{
error = "no free solid landing with a validated short target approach";
error = "no free solid landing with a validated target approach";
ClientlessTravelTelemetry::Note(player,"flight-landing-rejected",error);
return false;
}
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
@@ -467,6 +493,7 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
error = "native mount spell rejected";
return false;
}
ClientlessTravelTelemetry::Note(player,"flight-start","spell="+std::to_string(spell)+" target="+std::to_string(target.GetPositionX())+","+std::to_string(target.GetPositionY())+","+std::to_string(target.GetPositionZ())+" "+ClientlessTravelTelemetry::State(player));
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_START guid=%u map=%u spell=%u target=(%.2f %.2f %.2f)", player->GetGUID().GetCounter(), player->GetMapId(), spell, target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
return true;
}
@@ -496,30 +523,43 @@ bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool
{
// Only called after a real support surface was confirmed at the feet.
// Walk out from under the upper floor; never raise the player through it.
std::array<uint32,6> rejected{}; // surface, indoor, area, column, MMAP, movement
auto summary=[&]()
{
std::string detail="rejected surface/indoor/area/column/mmap/movement=";
for(auto count:rejected)detail+=std::to_string(count)+"/";
ClientlessTravelTelemetry::Note(player,"flight-departure-search",detail);
};
float heading=std::atan2(record.Status.Target.GetPositionY()-player->GetPositionY(),record.Status.Target.GetPositionX()-player->GetPositionX());
for(float radius:{8.0f,16.0f,24.0f,40.0f})for(float turn:{0.0f,0.785398f,-0.785398f,1.570796f,-1.570796f,2.356194f,-2.356194f,3.141593f})
for(float radius:{8.0f,16.0f,24.0f,40.0f,64.0f,96.0f,144.0f,200.0f})for(float turn:{0.0f,0.785398f,-0.785398f,1.570796f,-1.570796f,2.356194f,-2.356194f,3.141593f})
{
float x=player->GetPositionX()+radius*std::cos(heading+turn),y=player->GetPositionY()+radius*std::sin(heading+turn);
auto surface=Sample(player,record,x,y);
if(!surface.Valid)continue;
if(!surface.Valid){++rejected[0];continue;}
PositionFullTerrainStatus terrain;
player->GetMap()->GetFullTerrainStatusForPosition(player->GetPhaseShift(),x,y,surface.Ground,terrain);
if(!terrain.outdoors){++rejected[1];continue;}
Position point;point.Relocate(x,y,surface.Ground,player->GetOrientation());
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,point))continue;
if(!OpenColumn(player,x,y,surface))continue;
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,point)){++rejected[2];continue;}
if(!OpenColumn(player,x,y,surface)){++rejected[3];continue;}
ClientlessPathDiagnostics path;
if(!sClientlessTravelMgr->AnalyzePath(player,point,path) || !path.Calculated || !path.Complete || path.Suspicious ||
path.FinalDistance>1.5f || path.PathLength>80 || path.MaxSegmentLength>8 || path.MaxZDelta>4)continue;
if(!sClientlessTravelMgr->StartTravel(player,point,false))continue;
path.FinalDistance>1.5f || path.PathLength>600 || path.MaxSegmentLength>8 || path.MaxZDelta>4){++rejected[4];continue;}
if(!sClientlessTravelMgr->StartTravel(player,point,false)){++rejected[5];continue;}
summary();
ClientlessTravelTelemetry::Note(player,"flight-departure-walk","radius="+std::to_string(radius)+" length="+std::to_string(path.PathLength));
record.ApproachingDeparture=true;record.Departure=point;
record.Status.State=ClientlessFlightState::Mounting;record.Status.FollowingPlayer=following;
record.Status.Detail="walking to an open takeoff point";
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_DEPARTURE_WALK guid=%u destination=(%.3f %.3f %.3f) path=%.2f",player->GetGUID().GetCounter(),x,y,surface.Ground,path.PathLength);
_records[player->GetGUID().GetCounter()]=std::move(record);return true;
}
return false;
summary();return false;
}
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
{
ClientlessTravelTelemetry::Note(player,"flight-failed",reason+" "+ClientlessTravelTelemetry::State(player));
if(record.ApproachingDeparture)
{
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());record.ApproachingDeparture=false;
@@ -111,7 +111,18 @@ void ClientlessTransportDeckMgr::Clear(uint32 guid){Passengers.erase(guid);}
bool ClientlessTransportDeckMgr::Update(Player* player,GameObject* object,Position const& exitAnchor,bool arriving,uint32 exitBudget)
{
auto model=Get(object->GetEntry());
if(!model || !model->Mesh.Polygons() || std::fabs(object->GetObjectScale()-1.0f)>0.001f)return true;
if(!model || !model->Mesh.Polygons() || std::fabs(object->GetObjectScale()-1.0f)>0.001f)
{
// Recorded transitions do not require an optional free-walking mesh.
// An interrupted exit can leave the passenger halfway down the ramp.
// Restore the recorded stand only at a sufficiently long real stop.
if(!arriving)return false;
Point anchor{exitAnchor.GetPositionX(),exitAnchor.GetPositionY(),exitAnchor.GetPositionZ()};
TransportDockState dock;
if(!object->GetTransportDockState(dock) || !dock.Stopped)return false;
if(Distance(Local(player),anchor)<=0.65f)return dock.RemainingPause>=exitBudget;
return ReturnToRecordedStand(player,object,exitAnchor,exitBudget);
}
uint32 guid=player->GetGUID().GetCounter();auto& state=Passengers[guid];
if(state.Transport!=object->GetGUID()){state=Passenger{};state.Transport=object->GetGUID();}
TransportDockState dock;bool stopped=object->GetTransportDockState(dock) && dock.Stopped;
@@ -2,6 +2,7 @@
#include "ClientlessPositioning.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessTravelTelemetry.h"
#include "ClientlessTransportDeckMgr.h"
#include "Chat.h"
#include "ClientlessPlayerMgr.h"
@@ -475,6 +476,7 @@ void ClientlessTransportMgr::SetPhase(Player* player, Ride& ride, ClientlessTran
{
if(phase==ClientlessTransportPhase::Arrived || phase==ClientlessTransportPhase::Failed)
ClientlessTransportDeckMgr::Clear(player->GetGUID().GetCounter());
ClientlessTravelTelemetry::Note(player,"transport",std::string(PhaseName(phase))+" from="+ride.From.Node+" to="+ride.To.Node+" "+detail);
ride.Status = { phase, detail, ride.Abort }; ride.PhaseElapsed = 0; ride.Moving = false;
TC_LOG_INFO("server.adventurebot.navigation", "TRANSPORT_STATE guid=%u phase='%s' from='%s' to='%s' passenger=%u detail='%s'",
player->GetGUID().GetCounter(), PhaseName(phase), ride.From.Node.c_str(), ride.To.Node.c_str(), player->GetTransport() ? 1u : 0u, detail);
@@ -497,15 +499,24 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
return;
}
if(ride.Status.Recovering){RecoverPassenger(player,ride,diff);return;}
if(ride.RetryDelay){ride.RetryDelay=diff>=ride.RetryDelay?0:ride.RetryDelay-diff;return;}
GameObject* object = FindObject(player, ride.From);
uint32 limit = uint32(std::min<uint64>(1800000, std::max<uint64>(180000, uint64(ride.From.Period) * 3 + 30000)));
auto fail = [&](char const* detail)
{
player->StopMoving();
SetPhase(player, ride, Phase::Failed, detail);
if(player->GetTransport() && object && player->GetTransport()==object->ToTransportBase())
{
ride.Abort=true;ride.RetryDelay=3000;
SetPhase(player,ride,Phase::Riding,detail);
// Keep the original ride; its recorded stand recovery runs at the
// next stopped dock. Never recreate this ride on every world tick.
}
else SetPhase(player, ride, Phase::Failed, detail);
// Never detach an actual passenger in midair or in the sea.
};
if (ride.Elapsed > limit) { fail("transport timeout; any passenger attachment retained"); return; }
if (ride.Elapsed > limit && !ride.Abort)
{fail("transport timeout; any passenger attachment retained");return;}
if (ride.Status.Phase == Phase::Waiting)
{
if(player->GetMapId()!=ride.From.Map){fail("departure map changed while waiting");return;}
@@ -555,6 +566,26 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
ride.Point = 0;
SetPhase(player, ride, Phase::Boarding, "boarding recorded land-to-deck transition");
}
if((ride.Status.Phase==Phase::Boarding || ride.Status.Phase==Phase::Leaving) && !player->GetTransport())
{
auto const& transition=ride.Status.Phase==Phase::Boarding?ride.From:ride.To;
bool boarding=ride.Status.Phase==Phase::Boarding;
bool expectedPassenger=boarding ? (ride.Point>0 && transition.Points[ride.Point-1].OnTransport) :
(ride.Point<transition.Points.size() && transition.Points[ride.Point].OnTransport);
// During a final land segment native movement may already detach the
// passenger. It is legitimate only beside that actual recorded segment.
bool onLand=!boarding && ride.Point<transition.Points.size() && !transition.Points[ride.Point].OnTransport &&
Distance(*player,transition.Points[ride.Point].Pos)<=8.0f;
if(expectedPassenger || (!boarding && !onLand))
{
ClientlessTransportDeckMgr::Clear(player->GetGUID().GetCounter());
player->StopMoving();ride.Status.Recovering=true;
ride.Status.Detail="lost passenger during recorded transition; returning to departure";
ride.RecoveryMs=0;ride.RecoveryMoving=false;++ride.Recoveries;
ClientlessTravelTelemetry::Note(player,"transport-loss",ride.Status.Detail);
RecoverPassenger(player,ride,diff);return;
}
}
if (ride.Status.Phase == Phase::Boarding && ride.Abort)
{
player->StopMoving();
@@ -579,7 +610,7 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
if (permittedArrival && AtDock(player, object, ride.To, 0)) arrival = &ride.To;
else if (!ride.Resumed && ride.Abort && AtDock(player, object, ride.From, 0)) arrival = &ride.From;
auto const& exit = arrival ? *arrival : ride.From;
uint32 exitBudget=uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*100+1500;
uint32 exitBudget=uint32(PathLength(exit)/std::max(1.0f,player->GetSpeed(MOVE_RUN))*1000)+uint32(exit.Points.size())*std::max(100u,std::min(diff,2000u)*2)+2500;
bool ready=ClientlessTransportDeckMgr::Update(player,object,exit.Points.back().Pos,arrival!=nullptr,exitBudget);
if (!arrival || !ready || !AtDock(player,object,*arrival,exitBudget)) return;
if (arrival == &ride.From) ride.To = ride.From;
@@ -81,6 +81,7 @@ private:
uint32 PhaseElapsed = 0;
uint32 RequiredPause = 0;
uint32 PointTimeout = 0;
uint32 RetryDelay = 0;
bool Moving = false;
bool Abort = false;
bool Resumed = false;
@@ -1,4 +1,5 @@
#include "AdventureAreaMgr.h"
#include "ConditionMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
@@ -102,6 +103,18 @@ ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDat
return itr != _portalUses.end() ? &itr->second : nullptr;
}
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
{
auto portal=GetPortalUse(edgeDatabaseId);
auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr;
if(!info || info->type!=GAMEOBJECT_TYPE_SPELLCASTER)return false;
// Native Spell::CheckCast sees caster,target. Public portals either let the
// player cast on themself or let the object cast on the visiting player.
uint8 playerTargets=info->spellcaster.playerCast?3:2;
return sConditionMgr->IsPlayerMeetingPrerequisiteConditions(CONDITION_SOURCE_TYPE_SPELL,
portal->SpellId,player,playerTargets);
}
bool ClientlessTravelGraphMgr::Reload()
{
_positionProbes.Clear();
@@ -176,6 +176,7 @@ public:
static bool ResolvePortalUse(uint32 spawnId, ClientlessPortalUse& portal, std::string& error);
ClientlessPortalUse const* GetPortalUse(uint32 edgeDatabaseId) const;
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
private:
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
@@ -0,0 +1,74 @@
#ifndef VCORE_CLIENTLESS_TRAVEL_TELEMETRY_H
#define VCORE_CLIENTLESS_TRAVEL_TELEMETRY_H
// TEMPORARY_ADVENTURE_TRAVEL_TELEMETRY:
// Remove before development completion / vor Entwicklungsabschluss entfernen.
// Investigation only: native travel/access decisions must not depend on this.
#include "GameTime.h"
#include "Group.h"
#include "Log.h"
#include "Player.h"
#include <sstream>
#include <string>
#include <unordered_map>
namespace ClientlessTravelTelemetry
{
struct Trace
{
uint64 Id=0;
std::string Intent,Goal;
time_t Deadline=0,Started=0,Window=0,ProgressAt=0;
uint32 Lines=0,Suppressed=0;
std::unordered_map<std::string,std::pair<std::string,time_t>> Last;
};
inline auto& Traces(){static std::unordered_map<uint32,Trace> traces;return traces;}
inline void Note(Player const* player,char const* event,std::string detail)
{
if(!player)return;
auto it=Traces().find(player->GetGUID().GetCounter());if(it==Traces().end())return;
auto& trace=it->second;auto now=GameTime::GetGameTime();
if(now-trace.Window>=60){trace.Window=now;trace.Lines=0;}
for(auto& c:detail)if(c=='\n' || c=='\r' || c=='\'')c=' ';
if(detail.size()>900)detail.resize(900);
auto& last=trace.Last[event];
if(trace.Lines>=40 || (last.first==detail && now-last.second<60)){++trace.Suppressed;return;}
last={detail,now};++trace.Lines;
auto group=player->GetGroup();
TC_LOG_INFO("server.adventurebot.navigation",
"TEMP_TRAVEL trace=%llu guid=%u group=%u leader=%u map=%u elapsed=%llu intent='%s' goal='%s' event='%s' suppressed=%u detail='%s'",
static_cast<unsigned long long>(trace.Id),player->GetGUID().GetCounter(),group?group->GetGUID().GetCounter():0,
group?group->GetLeaderGUID().GetCounter():0,player->GetMapId(),static_cast<unsigned long long>(now-trace.Started),
trace.Intent.c_str(),trace.Goal.c_str(),event,trace.Suppressed,detail.c_str());
trace.Suppressed=0;
}
inline void Begin(Player const* player,std::string const& intent,std::string const& goal,time_t deadline)
{
uint32 guid=player->GetGUID().GetCounter();
if(intent!="instance" && intent!="raid"){Traces().erase(guid);return;}
auto& trace=Traces()[guid];
if(trace.Id && trace.Intent==intent && trace.Goal==goal && trace.Deadline==deadline)return;
static uint64 next=0;trace.Id=++next;trace.Intent=intent;trace.Goal=goal;trace.Deadline=deadline;
trace.Started=GameTime::GetGameTime();trace.Last.clear();
Note(player,"journey","selected target; deadline="+std::to_string(deadline));
}
inline void Clear(Player const* player){if(player)Traces().erase(player->GetGUID().GetCounter());}
inline std::string State(Player const* p)
{
std::ostringstream s;s<<"position="<<p->GetPositionX()<<","<<p->GetPositionY()<<","<<p->GetPositionZ()
<<" level="<<p->getLevel()<<" area="<<p->GetAreaId()<<" zone="<<p->GetZoneId()
<<" alive="<<p->IsAlive()<<" outdoor="<<p->IsOutdoors()<<" water="<<p->IsInWater()
<<" falling="<<p->IsFalling()<<" combat="<<p->IsInCombat()<<" taxi="<<p->IsInFlight()
<<" transport="<<bool(p->GetTransport())<<" teleport="<<p->IsBeingTeleported()
<<" mounted="<<p->IsMounted()<<" canFly="<<p->CanFly()<<" riding="<<p->GetSkillValue(SKILL_RIDING)
<<" license="<<p->HasSpell(90267)<<" cold="<<p->HasSpell(54197);
return s.str();
}
inline void Tick(Player const* player)
{
auto it=Traces().find(player->GetGUID().GetCounter());if(it==Traces().end())return;
auto now=GameTime::GetGameTime();if(now<it->second.ProgressAt)return;it->second.ProgressAt=now+20;
Note(player,"progress",State(player));
}
}
#endif
+2 -2
View File
@@ -260,9 +260,9 @@ public:
for (uint8 i = 0; i < PLAYER_EXPLORED_ZONES_SIZE; ++i)
{
if (flag != 0)
handler->GetSession()->GetPlayer()->SetFlag(PLAYER_EXPLORED_ZONES_1+i, 0xFFFFFFFF);
chr->SetUInt32Value(PLAYER_EXPLORED_ZONES_1+i, 0xFFFFFFFF);
else
handler->GetSession()->GetPlayer()->SetFlag(PLAYER_EXPLORED_ZONES_1+i, 0);
chr->SetUInt32Value(PLAYER_EXPLORED_ZONES_1+i, 0);
}
return true;
@@ -66,6 +66,12 @@
#include <unordered_map>
#include <unordered_set>
namespace ClientlessSafeLogout
{
bool Command(ChatHandler* handler,char const* args);
void Update();
}
namespace
{
bool Population(ChatHandler* handler,char const* args)
@@ -2297,6 +2303,9 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
}
static bool HandleClientlessLogout(ChatHandler* handler, char const* args)
{
std::string command=args?args:"";
if(command=="status" || command=="resume" || command.rfind("safe",0)==0)
return ClientlessSafeLogout::Command(handler,args);
uint32 characterGuid = 0;
if (!ParseCharacterGuid(handler, args, characterGuid))
return true;
@@ -2627,6 +2636,78 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
}
// Restart recovery belongs to the optional AdventureBot script layer.
namespace ClientlessSafeLogout
{
std::unordered_map<uint32,bool> Pending;
bool All=false;
bool Ready(Player const* player)
{
// Never force a timeout: native transport/flight/combat controllers must
// finish first. Being on solid ground is sufficient even if a graph
// planner is waiting on an unreachable future waypoint.
return player && player->IsInWorld() && player->IsAlive() && !player->IsInCombat() &&
!player->GetTransport() && !player->IsInFlight() && !player->IsFlying() &&
!player->IsFalling() && !player->IsBeingTeleported() &&
!player->HasUnitState(UNIT_STATE_CASTING) && !player->IsSplineEnabled();
}
bool Command(ChatHandler* handler,char const* args)
{
std::istringstream input(args?args:"");std::string op,target;input>>op>>target;
if(op=="resume"){All=false;Pending.clear();}
else if(op=="safe" && target=="all")
{
All=true;
for(auto const& session:sWorld->GetAllClientlessSessions())
if(sClientlessPlayerMgr->IsManaged(session.first))Pending.emplace(session.first,false);
}
else if(op=="safe")
{
char* end=nullptr;auto guid=std::strtoul(target.c_str(),&end,10);
if(target.empty() || !end || *end || !guid || guid>0xFFFFFFFFul || !sClientlessPlayerMgr->IsManaged(uint32(guid)))
{handler->SendSysMessage("Usage: clientless logout safe <guid|all> OR logout status. Pause the loader first.");return false;}
Pending.emplace(uint32(guid),false);
}
else if(op!="status")return false;
uint32 combat=0,transport=0,flight=0,loading=0;
for(auto const& entry:Pending)
{
auto session=sWorld->FindClientlessSession(entry.first);auto player=session?session->GetPlayer():nullptr;
if(!player || !player->IsInWorld()){++loading;continue;}
combat+=player->IsInCombat()?1:0;transport+=player->GetTransport()?1:0;
flight+=(player->IsInFlight() || player->IsFlying())?1:0;
}
handler->PSendSysMessage("Safe logout pending=%u sessions=%u combat=%u transport=%u flight=%u loading=%u",uint32(Pending.size()),sWorld->GetClientlessSessionCount(),combat,transport,flight,loading);
return true;
}
void Update()
{
if(All)for(auto const& session:sWorld->GetAllClientlessSessions())
if(sClientlessPlayerMgr->IsManaged(session.first))Pending.emplace(session.first,false);
for(auto it=Pending.begin();it!=Pending.end();)
{
uint32 guid=it->first;
auto session=sWorld->FindClientlessSession(guid);
if(!session){it=Pending.erase(it);continue;}
auto player=session->GetPlayer();
if(!player || !player->IsInWorld() || sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Online){++it;continue;}
if(!it->second)
{
ChatHandler handler(session);
sAdventureBehaviorMgr->Command(&handler,("stop "+std::to_string(guid)).c_str());
it->second=true;
}
if(Ready(player))
{
// World-thread eligibility and lifecycle request occur together;
// never infer safety from stale database positions or intent text.
if(sClientlessPlayerMgr->StopPlayer(guid))
TC_LOG_INFO("server.worldserver","CLIENTLESS_SAFE_LOGOUT guid=%u",guid);
}
++it;
}
}
}
class ClientlessInstanceVisitResume : public WorldScript
{
public:
@@ -2640,6 +2721,7 @@ public:
void OnUpdate(uint32 diff) override
{
ClientlessCombatPreparation::Update(diff);
ClientlessSafeLogout::Update();
_queries.ProcessReadyCallbacks();
if(_delay>diff){_delay-=diff;return;}
_delay=2000;
+13
View File
@@ -0,0 +1,13 @@
// Travel algorithms are extracted into small world doubles in these tests.
// Logging is independently exercised by test_travel_telemetry.py.
#ifndef CLIENTLESS_TELEMETRY_TEST_STUB_H
#define CLIENTLESS_TELEMETRY_TEST_STUB_H
#include <string>
namespace ClientlessTravelTelemetry {
template<class... T> void Note(T const&...) {}
template<class... T> void Begin(T const&...) {}
template<class... T> void Tick(T const&...) {}
template<class... T> void Clear(T const&...) {}
template<class... T> std::string State(T const&...) { return {}; }
}
#endif
@@ -37,6 +37,14 @@ int main()
assert(!Resolve(areas,999).Valid);
areas[8]={0,9};assert(!Resolve(areas,8).Valid); // orphan
areas[9]={0,8};assert(!Resolve(areas,8).Valid); // cycle
// Independent native city roots inherit physical surrounding regions.
areas[2817]={571,0,-1,77,80,-1,1};
areas[4395]={571,2817,-1,68,85,-1,-1};
auto dalaran=Resolve(areas,4395);
assert(dalaran.PreferFlight&&dalaran.MinLevel==77&&!dalaran.Allows(1,18,true));
areas[3519]={530,0,-1,62,65,-1,1};areas[3703]={530,3519};
auto shattrath=Resolve(areas,3703);
assert(shattrath.PreferFlight&&shattrath.MinLevel==62&&!shattrath.Allows(2,18,true));
// A reloaded parent restriction changes the exact same child without edits.
areas[1].Factions=0;
assert(!Resolve(areas,2).Allows(2,85,true));
@@ -24,6 +24,7 @@ std::tm Local(time_t){std::tm tm{};tm.tm_hour=5;return tm;}unsigned DateOf(std::
struct Location{unsigned InstanceCapacity=5,InstanceDifficulty=1;};struct Locations{Location l;Location*Find(std::string const&){return &l;}}locations;auto sAdventureLocationMgr=&locations;
struct AdventureBehaviorMgr{
struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;AdventureGroupPolicy::Departure Departure;};
bool allowed=true;bool Allowed(Player*,::Location const&,std::string const&){return allowed;}
std::unordered_map<unsigned,Record>_records;int changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;}
@@ -34,6 +35,7 @@ int main(){
AdventureBehaviorMgr mgr;Player leader{{29}},member{{31}};auto& a=mgr._records[29];auto& b=mgr._records[31];
a.Intent="groupformation";a.Location="leader-capital";a.Plan[5]={"groupformation","leader-capital"};a.Plan[6]={"instance","deadmines"};
b.Intent="groupformation";b.Location="other-capital";b.Plan[5]={"groupformation","other-capital"};b.Plan[6]={"instance","shadowfang"};
mgr.allowed=false;assert(!mgr.GroupPlan(&member).Enabled);mgr.allowed=true;
auto plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="instance"&&plan.NextLocation=="shadowfang");
assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedLocation=="leader-capital"&&b.Deadline==a.Deadline&&b.Schedule);
assert(b.Plan[6].Location=="shadowfang"); // own future plan remains available after departure
+1 -1
View File
@@ -120,6 +120,6 @@ int main(){
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'instance.cpp';exe=Path(tmp)/'instance';src.write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.')
@@ -14,6 +14,7 @@ code=r'''
#include <string>
using uint32=uint32_t;
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}virtual void OnUpdate(uint32){}};
namespace ClientlessSafeLogout{void Update(){}}
namespace ClientlessCombatPreparation{int ticks=0;void Update(uint32){++ticks;}}
struct Field{uint32 value;uint32 GetUInt32(){return value;}};
struct Rows{std::vector<std::vector<Field>> data;unsigned index=0;Field* Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
@@ -17,7 +17,7 @@ struct Store{bool exists=true;Location* Find(std::string const&){return exists?&
struct Manager{void ClearIntent(int){}void SetIntent(int,ClientlessIntent){}}manager;auto sClientlessPlayerMgr=&manager;
struct AdventureBehaviorMgr{
enum class Phase{Idle,Travel};
struct Record{bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
struct Record{long Deadline=0;bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
bool mapped=true,allowed=true;int fallback=0;std::string action,parameter,target;bool hard=false;
void Begin(Player*,Record&);
bool Allowed(Player*,Location const&,std::string const&){return allowed;}
@@ -38,6 +38,6 @@ int main(){Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'begin.cpp';exe=Path(tmp)/'begin';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.')
@@ -30,6 +30,7 @@ enum class ClientlessPlayerState{Offline,Online};
struct World{std::unordered_map<unsigned,ClientlessPlayerState>states;auto GetClientlessPlayerState(unsigned id){return states[id];}}world;auto sWorld=&world;
struct Core{std::vector<unsigned>starts;void StartPlayer(unsigned,unsigned guid,int){starts.push_back(guid);world.states[guid]=ClientlessPlayerState::Online;}}core;auto sClientlessPlayerMgr=&core;
int CreateClientlessAdventureLuaController(){return 0;}
namespace ClientlessSafeLogout{void Update(){}}
namespace ClientlessCombatPreparation{unsigned updates=0;void Update(unsigned){++updates;}}
'''+s[a:b]+r'''
int main(){
@@ -132,6 +132,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-adventure-state-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
print('Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.')
@@ -0,0 +1,34 @@
"""Native gossip binding resolves POI maps independently of overlapping continent XY."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "AdventurePoiPolicy.h"
#include <cassert>
using namespace AdventurePoiPolicy;
int main(){
Menus menus{{100,{{101,0}}},{101,{{100,140},{0,141}}}};
std::vector<Root> roots{{100,{571,5800,600,650}}};
auto bindings=Bind(menus,roots);Anchor chosen;
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571&&chosen.Z==650);
assert(!Select(bindings,999,5831,508,chosen));
// A much closer unrelated teleport anchor on map1 is not part of gossip ownership.
roots.push_back({999,{1,5831,508,3}});bindings=Bind(menus,roots);
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571);
// Native association with several maps is ambiguous; do not guess by XY.
roots.push_back({100,{0,5831,508,3}});bindings=Bind(menus,roots);
assert(!Select(bindings,140,5831,508,chosen));
// A floating platform must not become the terrain hundreds of yards below.
assert(Height(640,-100000,[](float from){return from<658?184.f:658.f;})==658);
assert(Height(650,-100000,[](float from){return from<675?650.f:675.f;})==650);
assert(Height(640,-100000,[](float){return -100000.f;})==-100000);
// Choose a nearby guard's height only after its map has been established.
roots.pop_back();roots.push_back({100,{571,5832,508,658}});bindings=Bind(menus,roots);
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571&&chosen.Z==658);
}
'''
with tempfile.TemporaryDirectory()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('POI native gossip graph: recursive menus/cycles, unique map, ambiguity rejection, local height anchor.')
+30
View File
@@ -0,0 +1,30 @@
"""The existing reveal/hide command must change the selected player's full bitfield."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/scripts/Commands/cs_cheat.cpp').read_text()
a=s.index(' static bool HandleExploreCheatCommand(');b=s.index('\n};',a)
code=r'''
#include <cstdlib>
#include <string>
#include <cassert>
using uint8=unsigned char;
constexpr int PLAYER_EXPLORED_ZONES_SIZE=156,PLAYER_EXPLORED_ZONES_1=0;
constexpr int LANG_NO_CHAR_SELECTED=0,LANG_YOU_SET_EXPLORE_ALL=1,LANG_YOURS_EXPLORE_SET_ALL=2,LANG_YOU_SET_EXPLORE_NOTHING=3,LANG_YOURS_EXPLORE_SET_NOTHING=4;
struct Session{};
struct Player{unsigned zones[156]{};Session session;Session*GetSession(){return &session;}void SetUInt32Value(int i,unsigned v){zones[i]=v;}};
struct ChatHandler{Player*target=nullptr;explicit ChatHandler(Session*){}ChatHandler()=default;
Player*getSelectedPlayer(){return target;}void SendSysMessage(int){}void SetSentErrorMessage(bool){}
bool needReportToTarget(Player*){return false;}std::string GetNameLink(Player* =nullptr){return "Tester";}
template<class...T>void PSendSysMessage(T const&...){} };
'''+s[a:b]+r'''
int main(){Player selected,caller;ChatHandler handler;handler.target=&selected;
assert(HandleExploreCheatCommand(&handler,"1"));for(auto bit:selected.zones)assert(bit==0xFFFFFFFFu);for(auto bit:caller.zones)assert(bit==0);
assert(HandleExploreCheatCommand(&handler,"0"));for(auto bit:selected.zones)assert(bit==0);
assert(!HandleExploreCheatCommand(&handler,""));handler.target=nullptr;assert(!HandleExploreCheatCommand(&handler,"1"));
}
'''
with tempfile.TemporaryDirectory()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('Native cheat explore reveals/clears every zone field on selected player; caller stays unchanged.')
@@ -0,0 +1,34 @@
"""Execute the city-parent migration on fixtures, preserving operator overrides."""
from pathlib import Path
import sqlite3,re,importlib.util,struct,tempfile,sys
sys.dont_write_bytecode=True
root=Path(__file__).resolve().parents[2]
s=(root/'sql/custom/world/2026_10_30_00_adventure_city_parents.sql').read_text()
s='\n'.join(l for l in s.splitlines() if not l.startswith('--'))
# Only adapt UPDATE JOIN syntax; execute the production CASE/filters unchanged.
m=re.search(r'UPDATE (\w+) city\s+JOIN \w+ region ON (.*?)\s+SET city.parent_area_id=region.area_id\s+WHERE (.*);',s,re.S)
assert m
sql=f'UPDATE {m[1]} AS city SET parent_area_id=region.area_id FROM {m[1]} AS region WHERE ({m[2]}) AND ({m[3]})'
db=sqlite3.connect(':memory:');db.execute('CREATE TABLE vcore_adventure_area_catalog(area_id INTEGER PRIMARY KEY,map_id INTEGER,parent_area_id INTEGER,min_level INTEGER,prefer_flight INTEGER)')
rows=[(2817,571,0,77,1),(4395,571,0,68,None),(3519,530,0,62,1),(3703,530,0,None,None),(12,0,0,1,0),(1519,0,999,None,None),(1,0,0,1,0),(1537,1,0,None,None),(999,0,0,1,0)]
db.executemany('INSERT INTO vcore_adventure_area_catalog VALUES(?,?,?,?,?)',rows);db.execute(sql)
after=db.execute('SELECT * FROM vcore_adventure_area_catalog ORDER BY area_id').fetchall()
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=4395').fetchone()==(2817,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=3703').fetchone()==(3519,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=1519').fetchone()==(999,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=1537').fetchone()==(0,)
db.execute(sql);assert db.execute('SELECT * FROM vcore_adventure_area_catalog ORDER BY area_id').fetchall()==after
# New DBC exports use the same semantic parents, without needing a C++ change.
spec=importlib.util.spec_from_file_location('catalog',root/'contrib/clientless/export_area_catalog.py');mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod)
def dbc(path,fields,rows):
data=b''.join(struct.pack('<'+'I'*fields,*r) for r in rows);strings=b'\0Test\0'
path.write_bytes(struct.pack('<4s4I',b'WDBC',len(rows),fields,fields*4,len(strings))+data+strings)
with tempfile.TemporaryDirectory()as directory:
p=Path(directory);maps=[];areas=[]
for mid in (530,571):
r=[0]*20;r[0]=mid;r[6]=1;maps.append(r)
for aid,mid in [(2817,571),(4395,571),(3519,530),(3703,530)]:
r=[0]*26;r[0]=aid;r[1]=mid;r[11]=1;areas.append(r)
dbc(p/'Map.dbc',20,maps);dbc(p/'AreaTable.dbc',26,areas);exported=mod.export(p)
assert 'SELECT 4395,571,2817,' in exported and 'SELECT 3703,530,3519,' in exported
print('Editable city catalog parents: Dalaran/CrystalSong, Shattrath/Terokkar, operator preservation, map guard, idempotence and new DBC exports.')
+18 -4
View File
@@ -9,6 +9,8 @@ a=s.index('bool ClientlessFlightMgr::ApproachDeparture(');b=s.index('\nvoid Clie
approach=s[a:b]
a=s.index(' bool OpenColumn(');b=s.index(' char const * StateName',a)
column=s[a:b]
a=s.index(' float support=',s.index('bool ClientlessFlightMgr::Begin('));b=s.index(' auto const * profile =',a)
preparation=s[a:b]
a=s.index(' if(!existingMount)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a)
dismount=s[a:b]
a=s.index(' auto floor = sample(current.X, current.Y);');b=s.index(' player->StopMoving();',a)
@@ -31,10 +33,11 @@ struct Position{float x=0,y=0,z=0,o=0;
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
struct Guid{unsigned GetCounter()const{return 84;}};
struct Map{float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map;
struct PositionFullTerrainStatus{bool outdoors=true;};
struct Map{bool outdoors=true;void GetFullTerrainStatusForPosition(int,float,float,float,PositionFullTerrainStatus&t){t.outdoors=outdoors;}float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map;
struct Player:Position{bool mounted=false,fly=false,aura=false;int removals=0;
Guid GetGUID(){return {};}unsigned GetMapId(){return 530;}int GetPhaseShift(){return 0;}Map* GetMap(){return &map;}
bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;}
bool outdoor=true;bool IsOutdoors(){return outdoor;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;}
void RemoveAurasByType(unsigned){++removals;mounted=fly=false;}};
Point PointOf(Position const&p){return {p.x,p.y,p.z};}
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
@@ -60,6 +63,9 @@ struct ClientlessFlightMgr{
std::unordered_map<unsigned,Record>_records;Surface surface{0,300,true};int approached=0;bool seek=false;
Surface Sample(Player*,Record&,float,float){return surface;}
bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&);
bool Prepare(Player*player,Record&record,std::string&error){auto origin=surface;bool following=false;
'''+preparation+r'''
return false;}
bool Admit(Player* player,Record&record,std::string&error,bool following=false){
auto origin=surface;std::vector<std::pair<unsigned,unsigned>>candidates{{123,1}};
'''+admission+r'''
@@ -80,6 +86,13 @@ struct ClientlessFlightMgr{
'''+find_return+approach+r'''
int main(){
ClientlessFlightMgr mgr;Player p;Record r;std::string error;r.Status.Target={100,0,0};
// An indoor actor may walk to a verified outdoor spot before mounting.
p.outdoor=false;assert(mgr.Prepare(&p,r,error)&&mgr._records.at(84).ApproachingDeparture);
mgr._records.clear();travel.starts=0;map.outdoors=false;
assert(!mgr.Prepare(&p,r,error)&&travel.starts==0);map.outdoors=true;p.outdoor=true;
// Open ground beyond the old 40-yard ring remains usable, with a bounded MMAP leg.
travel.nav.PathLength=200;assert(mgr.ApproachDeparture(&p,r,false));
travel.nav.PathLength=15;travel.starts=0;mgr._records.clear();
// A normal supported open-air departure remains valid.
assert(mgr.Admit(&p,r,error));
// Brynli: a native flying mount already suspended ten yards above terrain.
@@ -108,9 +121,10 @@ int main(){
areas.allowed=false;assert(!mgr.Admit(&p,r,error));areas.allowed=true;
travel.nav.Complete=false;assert(!mgr.Admit(&p,r,error));travel.nav.Complete=true;
travel.nav.MaxZDelta=20;assert(!mgr.Admit(&p,r,error));travel.nav.MaxZDelta=1;
travel.nav.PathLength=200;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15;
travel.nav.PathLength=601;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15;
travel.start=false;assert(!mgr.Admit(&p,r,error));travel.start=true;
map.feet=INVALID_HEIGHT;assert(!mgr.Admit(&p,r,error));
mgr.surface.Ground=0;map.feet=NAN;assert(!mgr.Admit(&p,r,error));map.feet=0;mgr.surface.Ground=12;
// A touchdown may use the actual lower floor, never a roof above it.
map.feet=0;p.z=0;mgr.surface.Ground=12;
assert(OpenColumn(&p,0,0,{0,300,true}));narrow=true;assert(!OpenColumn(&p,0,0,{0,300,true}));narrow=false;
@@ -124,6 +138,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-departure-') 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(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-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 flight admission: airborne mount retained, lower-floor MMAP departure, blocked/cliff/missing support rejected.')
@@ -0,0 +1,51 @@
"""Exercise the production landing search, including distant and layered alternatives."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
a=s.index(' bool foundLanding = false;');b=s.index(' sClientlessTravelMgr->CancelTravel(',a)
code=r'''
#include <cmath>
#include <cassert>
#include <string>
#include <algorithm>
#include <array>
using uint32=unsigned;
constexpr float MAX_HEIGHT=100000;constexpr int NAV_GROUND=1;
struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
struct Surface{float Ground=0,Ceiling=500;bool Valid=true;};
struct Map{float GetHeight(int,float,float,float,bool,float){return 20;}}map;
struct Player{int GetPhaseShift(){return 0;}Map*GetMap(){return &map;}unsigned GetMapId(){return 530;}unsigned GetInstanceId(){return 0;}};
struct Record{struct{Position Landing;bool FollowingPlayer=false;}Status;struct{float Clearance=60;}Settings;};
struct Areas{bool allowed=true;bool AllowsSegment(Player*,unsigned,Position const&,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
struct PathGenerator{struct Path{bool complete=true;float FinalDistance=0,PathLength=180;bool IsComplete(){return complete;}};
static inline Path result;static inline bool roofReachable=false;static inline unsigned probes=0;
static Path AnalyzePositions(unsigned,unsigned,unsigned,Position const&a,Position const&,int){++probes;auto p=result;if(a.z==20&&!roofReachable)p.complete=false;return p;}};
bool column=true;float openAt=50;bool water=false;
bool OpenColumn(Player*,float x,float y,Surface const&,float){return column&&std::hypot(x,y)>=openAt;}
Surface Sample(Player*,Record&,float x,float y){return {water?30.f:std::hypot(x,y)<50?20.f:0.f,500,true};}
bool Find(Player*player,Record&record,Position const&target,std::string&error){bool following=false;
'''+s[a:b]+r'''
return true;
}
int main(){Player p;Record record;Position target;std::string error;
// Nearby roof candidates are unusable. A safe outdoor landing 80yd away works.
assert(Find(&p,record,target,error));assert(std::hypot(record.Status.Landing.x,record.Status.Landing.y)>=79&&record.Status.Landing.z==0);
// >5yd level difference is fine only when the native MMAP approach is complete.
openAt=0;PathGenerator::roofReachable=true;assert(Find(&p,record,target,error)&&record.Status.Landing.z==20);
PathGenerator::roofReachable=false;assert(Find(&p,record,target,error)&&record.Status.Landing.z==0);
PathGenerator::result.complete=false;assert(!Find(&p,record,target,error));PathGenerator::result.complete=true;
PathGenerator::result.FinalDistance=2;assert(!Find(&p,record,target,error));PathGenerator::result.FinalDistance=0;
PathGenerator::result.PathLength=601;assert(!Find(&p,record,target,error));PathGenerator::result.PathLength=180;
column=false;assert(!Find(&p,record,target,error));column=true;
areas.allowed=false;assert(!Find(&p,record,target,error));areas.allowed=true;
water=true;assert(!Find(&p,record,target,error));
}
'''
# Define the static aggregate out-of-line for older standalone compilers.
code=code.replace('static inline Path result;', 'static Path result;').replace('bool column=true;', 'PathGenerator::Path PathGenerator::result;\nbool column=true;')
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production landing search: distant/layered alternatives, native final ground approach, water/collision/area rejection.')
+1 -1
View File
@@ -129,6 +129,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-policy-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
print('Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.')
@@ -49,8 +49,8 @@ struct Motion {
int launches=0;std::vector<Position> path;
void MoveSmoothPath(int,Position const* p,std::size_t count,bool){++launches;path.assign(p,p+count);}
};
struct Player {
float x=0; bool spline=true, flying=false;Motion motion;
struct Player:Position {
bool spline=true, flying=false;Motion motion;
void StopMoving(){spline=false;}bool IsFlying()const{return flying;}bool IsSplineEnabled()const{return spline;}
bool IsInWorld()const{return true;}uint32 GetMapId()const{return 0;}
float GetPositionX()const{return x;}float GetPositionY()const{return 0;}float GetPositionZ()const{return 0;}
@@ -94,6 +94,7 @@ struct Graph {
ClientlessTravelNode const* FindNode(std::string const& key){return key==node.Key?&node:nullptr;}
} 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;}
'''
@@ -202,5 +203,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++', '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
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([str(binary)], check=True)
@@ -0,0 +1,30 @@
"""Run the production member restore for guildless, guilded and deleted characters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/Groups/Group.cpp').read_text()
a=s.index('void Group::LoadMemberFromDB(');b=s.index('\nvoid Group::ConvertToLFG',a)
code=r'''
#include <cassert>
#include <string>
#include <vector>
using uint8=unsigned char;
enum class HighGuid{Player};struct ObjectGuid{using LowType=unsigned;unsigned id;ObjectGuid()=default;ObjectGuid(HighGuid,unsigned i):id(i){}};
struct Cache{bool exists=true;unsigned guild=0;bool GetCharacterNameByGuid(ObjectGuid,std::string&n){n="Bot";return exists;}
bool GetPlayerGuildIdByGUID(ObjectGuid,unsigned&g){if(!guild)return false;g=guild;return true;}}cache;auto sCharacterCache=&cache;
constexpr int CHAR_DEL_GROUP_MEMBER=1;struct CharacterDatabasePreparedStatement{void setUInt32(int,unsigned){}};
struct DB{int deletes=0;CharacterDatabasePreparedStatement stmt;auto GetPreparedStatement(int){return &stmt;}void Execute(CharacterDatabasePreparedStatement*){++deletes;}}CharacterDatabase;
struct LFG{unsigned loaded=0;void SetupGroupMember(ObjectGuid,unsigned){++loaded;}}lfg;auto sLFGMgr=&lfg;
struct Group{struct MemberSlot{ObjectGuid guid;std::string name;unsigned guildId;uint8 group,flags,roles;};std::vector<MemberSlot>m_memberSlots;unsigned subgroups=0;
unsigned GetGUID(){return 10;}void SubGroupCounterIncrease(unsigned){++subgroups;}void LoadMemberFromDB(unsigned,uint8,uint8,uint8);};
'''+s[a:b]+r'''
int main(){Group group;group.LoadMemberFromDB(26,2,1,4);assert(CharacterDatabase.deletes==0&&group.m_memberSlots.size()==1);
auto member=group.m_memberSlots[0];assert(member.guildId==0&&member.guid.id==26&&member.roles==4&&member.flags==2&&member.group==1);
cache.guild=123;group.LoadMemberFromDB(27,0,0,1);assert(group.m_memberSlots[1].guildId==123&&lfg.loaded==2&&group.subgroups==2);
cache.exists=false;group.LoadMemberFromDB(28,0,0,1);assert(CharacterDatabase.deletes==1&&group.m_memberSlots.size()==2&&lfg.loaded==2);
}
'''
with tempfile.TemporaryDirectory()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 group restore retains guildless members and their roles; only missing characters are deleted.')
+5 -4
View File
@@ -81,7 +81,7 @@ void Session::CompleteClientlessTeleport(){
}
struct Node {std::string Key;uint32 MapId=530;};
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;};
struct Graph {bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
struct Graph {bool prerequisites=true;bool PortalPrerequisites(Player*,uint32){return prerequisites;}bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
struct Travel {void CancelTravel(uint32){}} travel;
struct Lua {void DismountGroundMount(Player*,const char*){}} lua;
auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua;
@@ -105,6 +105,7 @@ harness += methods
harness += r'''
int main(){
ClientlessActionExecutorMgr mgr;
{Player p;Record r;graph.prerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.prerequisites=true;}
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world);
mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);}
// The global lifecycle may have ACKed a delayed cast before our next update.
@@ -139,7 +140,7 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-use-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.')
@@ -190,7 +191,7 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-definition-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(definition_harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Native portal destination discovery and unsupported/ambiguous spell rejection passed.')
@@ -224,6 +225,6 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-ack-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(ack_harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Production clientless ACK completion sequence increments only after completed near/far transfers.')
@@ -0,0 +1,48 @@
"""Run production remote prerequisite evaluation with native condition-shaped doubles."""
from pathlib import Path
import subprocess,tempfile,re
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/Conditions/ConditionMgr.cpp').read_text()
a=s.index('bool ConditionMgr::IsPlayerMeetingPrerequisiteConditions(');b=s.index('\nbool ConditionMgr::IsObjectMeetingNotGroupedConditions(',a)
method=s[a:b]
names=sorted(set(re.findall(r'\bCONDITION_[A-Z_]+',method)) - {'CONDITION_SOURCE_TYPE_NONE','CONDITION_SOURCE_TYPE_MAX'})
code=r'''
#include <map>
#include <vector>
#include <cassert>
using uint32=unsigned;using uint8=unsigned char;
enum ConditionSourceType{CONDITION_SOURCE_TYPE_NONE, SPELL, CONDITION_SOURCE_TYPE_MAX};
'''+ 'enum Kind{'+','.join(names)+r''',SPATIAL};
struct Player{unsigned level=52;bool quest=false;};
struct ConditionSourceInfo{Player* targets[3];ConditionSourceInfo(Player*a,Player*b,Player*c):targets{a,b,c}{}};
struct Condition{unsigned ElseGroup=0,ReferenceId=0,ScriptId=0,ConditionTarget=1;Kind ConditionType=CONDITION_LEVEL;bool negative=false;
bool isLoaded()const{return true;}
bool Meets(ConditionSourceInfo&i)const{bool v=ConditionType==CONDITION_LEVEL?i.targets[ConditionTarget]->level>=58:i.targets[ConditionTarget]->quest;return negative?!v:v;}};
using ConditionContainer=std::vector<Condition*>;
struct ConditionMgr{std::map<unsigned,ConditionContainer>ConditionStore[CONDITION_SOURCE_TYPE_MAX],ConditionReferenceStore;
bool IsPlayerMeetingPrerequisiteConditions(ConditionSourceType,unsigned,Player const*,uint8)const;};
'''+method+r'''
int main(){ConditionMgr mgr;Player p;Condition level,quest;quest.ConditionType=CONDITION_QUESTREWARDED;
auto check=[&](unsigned mask=2){return mgr.IsPlayerMeetingPrerequisiteConditions(SPELL,65729,&p,mask);};
assert(check());mgr.ConditionStore[SPELL][65729]={&level};assert(!check());p.level=58;assert(check());p.level=52;
// Visiting player is target1 for GO casts. Do not apply player tests to a GO caster.
level.ConditionTarget=0;assert(check());assert(!check(3));level.ConditionTarget=1;
mgr.ConditionStore[SPELL][65729]={&level,&quest};assert(!check());p.quest=true;assert(!check());
quest.ElseGroup=1;assert(check());p.quest=false;assert(!check());
// Remote spatial/script requirements are unknown, not a reason to reject an OR branch.
quest.ConditionType=SPATIAL;assert(check());quest.ElseGroup=0;assert(!check());
quest.ElseGroup=1;quest.ConditionType=CONDITION_QUESTREWARDED;quest.ScriptId=12;assert(check());quest.ScriptId=0;
Condition ref;ref.ReferenceId=42;mgr.ConditionReferenceStore[42]={&level};mgr.ConditionStore[SPELL][65729]={&ref};assert(!check());
p.level=58;assert(check());level.negative=true;assert(!check());p.level=52;assert(check());
// Recursive/broken reference must defer to native use rather than recurse forever.
mgr.ConditionReferenceStore[42]={&ref};assert(check());
}
'''
with tempfile.TemporaryDirectory()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)
# Prerequisites must run before the cached static edge decision.
a=(root/'src/server/game/World/AdventureAreaMgr.cpp').read_text();a=a[a.index('bool AdventureAreaMgr::AllowsEdge('):]
assert a.index('PortalPrerequisites')<a.index('_edgeAccess.find')
print('Production portal prerequisites: level/quest, target roles, AND/OR, negative and reference conditions; no stale permission cache.')
+26
View File
@@ -0,0 +1,26 @@
"""Run the actual logout eligibility gate, without a live server."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
a=s.index('bool Ready(Player const* player)',s.index('std::unordered_map<uint32,bool> Pending;'));b=s.index('\nbool Command(',a)
code=r'''
#include <cassert>
#include <initializer_list>
constexpr int UNIT_STATE_CASTING=1;
struct Player{bool world=true,alive=true,combat=false,transport=false,taxi=false,flying=false,falling=false,teleport=false,casting=false,spline=false;
bool IsInWorld()const{return world;}bool IsAlive()const{return alive;}bool IsInCombat()const{return combat;}bool GetTransport()const{return transport;}
bool IsInFlight()const{return taxi;}bool IsFlying()const{return flying;}bool IsFalling()const{return falling;}bool IsBeingTeleported()const{return teleport;}
bool HasUnitState(int)const{return casting;}bool IsSplineEnabled()const{return spline;}};
'''+s[a:b]+r'''
int main(){Player p;assert(Ready(&p)&&!Ready(nullptr));
for(auto field:{&Player::combat,&Player::transport,&Player::taxi,&Player::flying,&Player::falling,&Player::teleport,&Player::casting,&Player::spline}){
p.*field=true;assert(!Ready(&p));p.*field=false;assert(Ready(&p));
}
p.world=false;assert(!Ready(&p));p.world=true;p.alive=false;assert(!Ready(&p));
}
'''
with tempfile.TemporaryDirectory()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('Native safe logout gate waits for combat, casting, movement, transport, taxi, flight, falling, teleport and resurrection.')
+13 -3
View File
@@ -128,7 +128,7 @@ bool Safe(std::string const& key){return !key.empty();}
namespace Trinity {bool IsValidMapCoord(float,float,float,float=0){return true;}}
'''
helpers = '\n'.join(function(source, marker) for marker in [
' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength('
' char const* PhaseName(', ' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength('
])
methods = '\n'.join(function(source, marker) for marker in [
'bool ClientlessTransportMgr::ResumePassenger(', 'bool ClientlessTransportMgr::SelectResumedDock(',
@@ -190,7 +190,7 @@ int main(){
go.x=50;p.x=51;mgr.Cancel(26);run(mgr,p,abort);assert(abort.Status.Phase==Phase::Riding&&p.transport);
go.x=5;p.x=6;run(mgr,p,abort);assert(abort.Status.Phase==Phase::Failed&&abort.Status.Cancelled&&!p.transport&&p.x==0);
// A timeout aboard never removes the passenger over sea/in midair.
assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Failed&&p.transport);go.base.RemovePassenger(&p);
assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Riding&&timeout.Abort&&timeout.RetryDelay==3000&&p.transport);go.base.RemovePassenger(&p);
// Departure during a boarding transition keeps the Passenger until safe recovery.
p.x=0;go.x=5;go.state.Stopped=true;assert(mgr.Start(&p,"A","B",error));auto&late=mgr._rides.at(26);
for(int i=0;i<10&&!p.transport;i++)mgr.UpdateRide(&p,late,100);
@@ -280,6 +280,16 @@ int main(){
copy1.base.RemovePassenger(&orphan);orphan.map=0;orphan.object=&vessel;vessel.x=100;
vessel.base.AddPassenger(&orphan);orphan.x=101;orphan.m_movementInfo.transport.pos.x=1;
run(resumed,orphan,resumed._rides.at(26));assert(!orphan.transport&&orphan.map==0&&orphan.x==104);
// Lost attachment during both boarding and disembarkation uses recorded departure rescue.
travel.path=false;travel.present=false;
for(auto phase:{Phase::Boarding,Phase::Leaving}){
Player fallen;fallen.object=&vessel;fallen.x=50;fallen.falling=true;
auto ride=adopted;ride.From=stop("A",5,0);ride.To=stop("B",100,104);
ride.Status.Phase=phase;ride.Status.Recovering=false;ride.Abort=false;ride.Elapsed=0;ride.RetryDelay=0;
ride.Point=phase==Phase::Boarding?3:2;ride.Moving=false;
resumed.UpdateRide(&fallen,ride,100);
assert(ride.Status.Recovering&&fallen.teleports==1&&fallen.x==0);
}
std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n";
}
'''
@@ -287,5 +297,5 @@ with tempfile.TemporaryDirectory(prefix='vcore-transport-test-') as directory:
path = Path(directory)
cpp = path / 'transport.cpp'
cpp.write_text(preamble + position + '\n' + offset + world + header + helpers + stubs + methods + animated + tests)
subprocess.run(['g++', '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(path / 'test')], check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(path / 'test')], check=True)
subprocess.run([str(path / 'test')], check=True)
@@ -74,7 +74,16 @@ int main(){Player p;Position anchor;
assert(launches==2&&Distance(first,Passengers[2].Target)>=1.5f);
// Near departure, a running walk is stopped on the deck.
object.dock.RemainingPause=1000;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(!p.spline);
object.entry=999;assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
// Ships without a mesh still recover interrupted exits at a real stop.
object.entry=190536;p.current={9,0,0};auto unsupportedBefore=fallback;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(fallback==unsupportedBefore);
object.dock.RemainingPause=60000;object.dock.Stopped=false;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(fallback==unsupportedBefore);
object.dock.Stopped=true;assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
assert(fallback==unsupportedBefore+1&&p.current[0]==0&&p.transport==&object);
assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000)&&fallback==unsupportedBefore+1);
object.entry=186238;p.current={9,0,0};assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
assert(fallback==unsupportedBefore+2);
Passengers.clear();object.entry=176310;object.dock.RemainingPause=60000;
p.current={4,0,0};p.spline=false;launchOK=false;auto before=fallback;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000)&&Passengers[1].ReturnFailed);
+48
View File
@@ -0,0 +1,48 @@
"""Run the real temporary trace lifecycle and its per-bot log budget."""
from pathlib import Path
import subprocess,tempfile,re
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessTravelTelemetry.h').read_text()
assert 'Remove before development completion' in s
s=re.sub(r'#include "[^\n]+"\n','',s)
code=r'''
#include <string>
#include <cstdint>
#include <ctime>
using uint64=uint64_t;using uint32=unsigned;
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
unsigned logs=0;
#define TC_LOG_INFO(...) (++logs)
constexpr int SKILL_RIDING=1;
struct Guid{unsigned id;unsigned GetCounter()const{return id;}};
struct Group{Guid GetGUID()const{return {1};}Guid GetLeaderGUID()const{return {26};}};
struct Player{unsigned guid=26;Guid GetGUID()const{return {guid};}Group*GetGroup()const{return nullptr;}
unsigned GetMapId()const{return 0;}float GetPositionX()const{return 1;}float GetPositionY()const{return 2;}float GetPositionZ()const{return 3;}
unsigned getLevel()const{return 60;}unsigned GetAreaId()const{return 12;}unsigned GetZoneId()const{return 12;}
bool IsAlive()const{return true;}bool IsOutdoors()const{return true;}bool IsInWater()const{return false;}bool IsFalling()const{return false;}
bool IsInCombat()const{return false;}bool IsInFlight()const{return false;}void*GetTransport()const{return nullptr;}
bool IsBeingTeleported()const{return false;}bool IsMounted()const{return false;}bool CanFly()const{return false;}
unsigned GetSkillValue(int)const{return 225;}bool HasSpell(int)const{return true;}};
'''+s+r'''
#include <cassert>
int main(){using namespace ClientlessTravelTelemetry;Player p;
Note(&p,"route","no journey");assert(logs==0&&Traces().empty());
Begin(&p,"adventure","Elwynn",200);assert(logs==0&&Traces().empty());
Begin(&p,"instance","Deadmines",200);assert(logs==1);
auto id=Traces().at(26).Id;Begin(&p,"instance","Deadmines",200);assert(logs==1&&Traces().at(26).Id==id);
Note(&p,"route","same");Note(&p,"route","same");assert(logs==2);
for(unsigned i=0;i<100;++i)Note(&p,"route",std::to_string(i));assert(logs==40);
for(unsigned i=0;i<100;++i)Begin(&p,"raid",std::to_string(i),200);assert(logs==40);
GameTime::now+=60;Note(&p,"route","new minute");assert(logs==41);
Tick(&p);assert(logs==42);Tick(&p);assert(logs==42);
GameTime::now+=20;Tick(&p); // unchanged full state is deduplicated, too
assert(logs==42);
Begin(&p,"rest","inn",300);assert(Traces().empty());
Begin(&p,"raid","Karazhan",400);assert(!Traces().empty());Clear(&p);assert(Traces().empty());
}
'''
with tempfile.TemporaryDirectory()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('Temporary travel traces: instance/raid scope, stable journey ID, deduplication, 40/min budget across goal changes and logout cleanup.')