Fix AdventureBot travel recovery, area eligibility and safe build logout
This commit is contained in:
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.
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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 ↦}
|
||||
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 ↦}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.')
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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);
|
||||
|
||||
@@ -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.')
|
||||
Reference in new issue
Block a user