Enforce reloadable AdventureBot area and travel access policies

This commit is contained in:
Nico committed 2026-10-29 09:44:49 +01:00
1 parent 0173a434f4
commit 321a28640c
31 files changed
+4869 -21

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Export the local Cataclysm DBC geography as an editable world-DB catalog.
Does not connect to a database. Existing operator edits survive repeated imports.
"""
import argparse
import struct
from pathlib import Path
def dbc(path, fields):
data = path.read_bytes()
magic, count, actual, size, strings_size = struct.unpack_from('<4s4I', data)
if magic != b'WDBC' or actual != fields or size != fields * 4:
raise ValueError(f'Unexpected Cataclysm DBC layout: {path}')
if len(data) != 20 + count * size + strings_size:
raise ValueError(f'Truncated or unsupported DBC: {path}')
strings = data[20 + count * size:]
rows = [struct.unpack_from('<' + 'I' * fields, data, 20 + i * size)
for i in range(count)]
def name(offset):
if offset >= len(strings):
raise ValueError(f'Invalid string offset in {path}')
return strings[offset:].split(b'\0', 1)[0].decode('utf-8')
return rows, name
def quote(value):
# Hex strings avoid dependence on session SQL backslash escaping modes.
return "CONVERT(X'" + value.encode('utf-8').hex() + "' USING utf8mb4)"
def export(directory):
maps, map_name = dbc(directory / 'Map.dbc', 20)
areas, area_name = dbc(directory / 'AreaTable.dbc', 26)
map_ids = {r[0] for r in maps}
area_ids = {r[0] for r in areas}
if any(r[2] and r[2] not in area_ids for r in areas):
raise ValueError('AreaTable contains missing parent areas')
sql = ["-- Generated from local Cataclysm 4.3.4 DBC; do not overwrite operator edits.",
"-- All access policies are AdventureBot-only; never player restrictions.",
"CREATE TABLE IF NOT EXISTS vcore_adventure_map_catalog (",
" map_id INT UNSIGNED NOT NULL PRIMARY KEY,",
" display_name VARCHAR(255) NOT NULL,",
" map_type TINYINT UNSIGNED NOT NULL,",
" expansion TINYINT UNSIGNED NOT NULL,",
" notes VARCHAR(255) NOT NULL DEFAULT ''",
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS vcore_adventure_area_catalog (",
" area_id INT UNSIGNED NOT NULL PRIMARY KEY,",
" map_id INT UNSIGNED NOT NULL,",
" parent_area_id INT UNSIGNED NOT NULL DEFAULT 0,",
" display_name VARCHAR(255) NOT NULL,",
" allowed_factions TINYINT UNSIGNED DEFAULT NULL COMMENT 'NULL inherit; 0 closed; 1 Alliance; 2 Horde; 3 both',",
" min_level TINYINT UNSIGNED DEFAULT NULL COMMENT 'Recommended level band, NULL inherit/unknown',",
" max_level TINYINT UNSIGNED DEFAULT NULL COMMENT 'Recommended level band, not an upper access limit',",
" max_level_gap TINYINT UNSIGNED DEFAULT NULL COMMENT 'Allowed levels below min_level; NULL inherit, default 5',",
" prefer_flight TINYINT UNSIGNED DEFAULT NULL COMMENT 'NULL inherit; 0 normal; 1 prefer legal flight',",
" notes VARCHAR(255) NOT NULL DEFAULT '',",
" KEY parent_area (parent_area_id), KEY map_area (map_id)",
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"START TRANSACTION;"]
for r in sorted(maps):
sql.append('INSERT IGNORE INTO vcore_adventure_map_catalog '
'(map_id,display_name,map_type,expansion) VALUES '
f'({r[0]},{quote(map_name(r[6]))},{r[2]},{r[16]});')
# Native AreaTable also contains obsolete areas whose Map row is absent.
# Retain their identity explicitly; they must not become usable destinations.
for map_id in sorted({r[1] for r in areas} - map_ids):
sql.append('INSERT IGNORE INTO vcore_adventure_map_catalog '
'(map_id,display_name,map_type,expansion,notes) VALUES '
f"({map_id},'Missing Map.dbc entry',255,0,'Unavailable native map');")
alliance = {1, 12, 141, 3524, 3525, 1519, 1537, 1657, 3557,
4714, 4755, 4774, 4706, 5351}
horde = {14, 85, 215, 3430, 3433, 1497, 1637, 1638, 3487,
4707, 4720, 4737}
outland = {3483, 3521, 3519, 3518, 3522, 3523, 3520, 3703}
cataclysm = {616, 4922, 5034, 5042, 5095, 5389}
for r in sorted(areas):
area, map_id, parent = r[:3]
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:
faction, note = '0', 'Vashjir closed to all AdventureBots, including child areas'
if map_id not in map_ids:
faction, note = '0', 'Unavailable native map'
flight = '1' if area in outland | cataclysm or (map_id == 571 and parent == 0) else 'NULL'
sql.append('INSERT IGNORE INTO vcore_adventure_area_catalog '
'(area_id,map_id,parent_area_id,display_name,allowed_factions,prefer_flight,notes,min_level,max_level) '
f'SELECT {area},{map_id},{parent},{quote(area_name(r[11]))},{faction},{flight},{quote(note)},'
'z.min_level,z.max_level FROM (SELECT 1) seed '
f'LEFT JOIN vcore_adventure_zone z ON z.zone_id={area} AND z.enabled=1;')
# Level bands are seeded only on INSERT, including deliberately NULL edits.
sql.append('COMMIT;')
return '\n'.join(sql) + '\n'
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('dbc_directory', type=Path)
parser.add_argument('output_sql', type=Path)
args = parser.parse_args()
args.output_sql.write_text(export(args.dbc_directory), encoding='utf-8')
+285
View File
@@ -1,3 +1,288 @@
# WICHTIGSTER NEUER BEFUND: Freiflug scheitert an fehlendem Terrain-Fallback
Native `clientless flight probe 128 1751.49 -4389.67` (09:20CETca) ergab
valid0 ground0 ceiling0. DIREKT davor in aktuellemConsoleLog:
Map file /home/wow_admin/wow_cata_server/data/maps/7192840.map does not exist!
Karte719lautlokalerMapDBC=Hyjal,Parent1(Kalimdor). PhysischeBasiskachel
0012840.map sowie001_28_40.vmtile/001.vmtree sind vorhanden.
ClientlessFlightMgr::Sample(Zeile~166) benutztPhasingHandler::GetTerrainMapId
undverlangtTerrainMgr::ExistMapAndVMap(terrain,x,y) fürdiesesChildTerrain.
BeiFehlenreturn{} bevor eigentlicheHeight/CeilingChecks. Normales
TerrainInfo::GetGrid(281ff) lädtBasiskarteundnutztChildnurdann,wennesfürdiesenOrt
eineChildGridMapgibt;ansonstenbleibtBasiskarte. UnsereExtravorprüfungignoriert
diesenFallback undverhindertsoFlügeauchmit225Reiten/Lizenzen/Mounts.
NichtPhase169ändern(istStandard)! Nichtblind719Kachelnfälschen/kopieren oder
Kollisionsprüfungenabschalten. NötigwäreeineTerrainprüfung mitdemtatsächlich
verwendetenKachelfallback,danachweiterVMAP/Bounds/Clearanceprüfen.
ZweiterBefundbleibtGraph-AnschlussnurperMMAPundfinalerCrossMapFallbackungeeignet.
KeineCodeänderung/DBwrites/IntentänderungeninderDiagnose. NutzerTeilbefund
soebenkommentiert;jetztkurzabschließendzusammenfassen.
---
# Neueste Diagnose: Horde-Navigation/Fliegen, keine Änderungen umgesetzt
Nutzer bat Horde-Navigation anzusehen, höherlevelige solltenfliegenkönnen.
55Horde online,22ab60 nativStatusangefragt (09:18CET):19ActionCompleted/
PhaseOnSite,1Failed(Volakmar128,L80,Naxxramasgoal571),1Idle(Gruppeninteraktion),
1Moving. Allestanden gespeichertnochinOrgrimmar nachUmsiedlung, vieleerfüllen
legitimStadtintents. Logs zeigenvorallembeiLowlevels wiederholteMMAP/Graph-
Startprobleme anOrgrimmarsHöhenplattformen.
KonkreteCodeLücke: PlanAdaptiveRoute startetGraph nurüberlokalenMMAP-Weg zum
Startnode (inklDeferredPartial), keinFlugvomaktuellenStandortzumeinemStartnode.
FreieFlugalternativen beginnen erst INNERHALB desGraphs. TryFallbackFlight in
ClientlessActionExecutorMgr.cpp1890 prüft nurGetTravelGoal(finalesZiel) und
brichtbeiandererMap sofortab. Volakmar128,L80,hat225Reiten,90267+54197 und
HordeFlugmounts32243/44/45,willNaxxramas571,bleibtinOrgrimmar anStart-MMAP
hängen,keinFlightState. Grundrouten fehlen,aberFlugzukünftigzumgeeigneten
legalenAbreiseknoten muss dieseAnschlusslücke überbrücken. Keine pauschale
Flugsperre inOrgrimmar ausdiesenDatenbewiesen. NativeFlightSurfaceProbe128
bei1751.49,-4389.67 zuletztgesendet.
22HighLevels gespeichertmeist225; Aldren32undTalzul97habenKEINcharacter_skills
RIDING762Datensatz,obwohlLizenzen/Mountspells und geladeneProfileGroundMount1.
DBistgespeicherterZustand, liveSkillnochNICHTausgelesen,nichtalsdefinitiven
aktuellenSkillbugverkaufen. Reconcile SetSkill nurwennCanGroundMount true;
KontrollederLaufzeitvorbereitung/speicherung sinnvoll. StoredAldrenProfil
beginntfalseLegacy,weiterhintenmovementm überschreibtesaufaktuelltrue.
Artefakte:/tmp/vcore-horde-flight-guids.txt, ...flight-offset.txt,
/tmp/vcore-horde-flight-diagnosis.txt. NativeConsoleweiter
/tmp/vcore-area-start-verified-20261029.log,ServerPID247021. SourceWIP
unverändert,keineConfig/DBwrites/IntentsänderungenwährenddieserDiagnose;
nurreadonlyStatus/FlightSurfaceprobe. NutzerkurzerklärteTeilbefundekommentiert.
NochkeineImplementierungfürdieseLückeangefordert; aufDiagnoseabschließen.
---
# Neuester Auftrag:110AdventureBots in sichere Hauptstädte umgesiedelt
Nutzer bat alleBots nachSturmwind/Orgrimmar zu versetzen, um alteStrandungen zu
beseitigen. OhneCode/Build/Serverrestart umgesetzt: TestLoader kurz perConfig
Enabled0+reloadconfig+populationreload pausiert,110nativeClientlessLogouts,
DBOffline geprüft, nur explizitgenerierte readyBots inCharacterDB verlagert.
55Allianz aufnativeGameTeleStormwind954(Map0,Zone1519,-8833.07,622.778,93.9317),
55Horde aufOrgrimmar703(Map1,Zone1637,1569.97,-4397.41,16.0472).
Gespeicherte Taxi-/Transport-/Instanzfelder bereinigt, alteInstanceVisits und
BotCorpse/Geist-Auren8326/20584 entfernt, Health>=1, keineWiederbelebungsschwäche.
KeineProfile/Intents/Tagespläne geändert. LoaderEnabledOriginal1 und50/5/50/5
wiederhergestellt, normalesgestaffeltesEinloggen. ServerPID247021 bliebgleich.
Audit/engePositionsbackups /tmp/vcore-capital-relocation-20261029/,
Skript /tmp/vcore-capital-relocate.py, AblaufLog /tmp/vcore-capital-relocate.log.
State normal_loader_restored. NachSQL alle110fraktionsrichtiginHauptstädten,
0alteReisezustände. Livepositionsprobe26Area5148(Sturmwind),32Area1637(Orgrimmar)
beideerlaubt. NormaleTagespläne dürfen dieBots wiederlegalweiterreisenlassen.
AbschlussNativePopulation110/110,50/5/50/5,0pending,errorleer geprüft.
GMreloadconfig meldetErfolg nurGlobalGM, nichtConsole -> nichtdaraufwarten,
ConsoleFIFO viaanschließendenpopulationreload mitenabledStatus prüfen.
KeineandererenSpielerangefasst, keinCommit/Push. VorigeSource-WIP bleibtbewahrt.
---
# ABSCHLUSS: finaler Testserver geprüft,110Bots online
Gebietskatalog-Integration fertig gebaut/installiert. LetzterStart
/tmp/vcore-area-start-verified-20261029.log, PID247021, tmuxvcore-worldserver.
Ready-Prozess-Port8085 geprüft. NativePopulation110,50/5/50/5,0pending,errorleer.
FinalerTagesplanaudit2640Slots,keineleerenZiele,0Verstöße inMetadaten;
298Slots ohneexpliziteGebiets-ID werdennativüberKoordinatengeprüft.
FinaleLiveproben: Allianz26Tiefenbahn erlaubt,Horde32Sturmwindportal verboten,
Vashjir26verboten. KeinADVENTURE_DISABLED/Assert/SQLKollationsfehler imletztenStart.
Nachweise doc/clientless/checkpoints/2026-10-29-area-catalog/ inklfinal-server-smoke.log.
ZehnTestsbestanden. KeineweiterenArbeitenvorAbschlussnötig. Serverlaufenlassen.
NochkeinCommit/Push. AbschließendNutzermelden: Funktionenaktiv,Reloadkommando,
Build/Tests/110Botsgeprüft;Gameplayvisuellnichtabgenommen. AlteTaxi-Hänger und
kalte/großeDebug-Routen(mehrereSekunden)bleibengesonderteThemen.
---
# Aktuell29.10.2026: Gebietskatalog LAUFZEIT-INTEGRATION implementiert
Nutzerauftrag: Bots und Tagesplaner müssen Gebietskatalog verwenden; keine
Locations in gesperrten/zu hochstufigen Gebieten. Frühere Katalog-only-Notizen
unten sind HISTORISCH, nicht als aktuelles Arbeitsverbot verstehen!
Implementiert: AdventureAreaMgr + getestete AdventureAreaPolicy. Snapshots aus
WorldDB, Eltern-Fraktionsschnittmenge, MinLevel+Gap(default5), bekannte Bereiche,
unbekannte Freilandbereiche unter85 gesperrt, native Flugpräferenzen. Neue/alte
Tagespläne über Allowed/PickLocation/Generate prüfen und illegale Ziele persistent
ersetzen, Intents/Stunden bewahren. AreaRevision bewirkt Revalidierung nachReload.
AdventureLocationMgr bezieht Levelgewichte nur noch aus neuem Katalog.
Node-ACL vcore_clientless_node_access für alle Nodearten, explizit4Allianz-Nodes
(Tiefenbahn, SW-Ruttheran-Schiff). Automatische Taximasken aus Creature-Spawn-NPCflag
COALESCE(c.npcflag,t.npcflag) und NPC-Fraktionsvorlagen (auch neutral).314TaxiNodes
zugeordnet,141ungeklärt und zu. Masken werden im Manager abgeleitet, nicht mit
manuellen Masken verwechselt. Optional flightmaster_spawn_id für konkreteBindung.
Tabellen-Kollation utf8mb4_unicode_ci passend zu bestehenden Nodekeys!
TravelGraph alle Suchpfade, virtuelle Brücken/Ein-/Ausstiege, cachedMMAPgeometry,
Executor vorRouteStep und realenRecordedSplines, TravelMgr Diagnose+Ausführung,
Taxi-DBC-Strecken, FreeFlight Planung+Segmente und nativeTransportStarts prüfen.
Gruppenreise: JourneyFollow Portalziel ebenfalls prüfen, da dieser Pfad native
Portale direkt verwendet. Neues Kommando `clientless adventure area <guid> [node]`.
`clientless adventure reload` lädt Snapshot+Locations+Graph, ungültigeArea-Daten
lassen bisherigenSnapshot bestehen. KeinNeubuild/Restart für spätereDatenedits.
GeometrieCache zwischen Fraktionen/Levels und nativeTerrainSwaps geteilt,
Berechtigungen bleiben live. Terrainsharedptrs halten, sonst massivesRemoteMap-
Neuladen. Cachegröße begrenzt mit teilweiserEviction. DijkstraFlight-Vorprüfung
über sichereKostenuntergrenze40+0.4*Distanz vermeidet unnötigeTerrainproben,
keine Kandidaten/Graphverbindungen einfach abgeschnitten.
Quellcode erfolgreichgebaut mit bestehenderDebugCMake, letztesBuild
/tmp/vcore-area-build-verified.log, Installation /tmp/vcore-area-install-verified.log.
ZehnTestprogramme bestanden, nativeLivechecks einschließlichbeiderNeutralitäts-
seiten, Vashjir, HordeTram/Schiff, neutralerCapitalPortalnode, DalaranLevel60,
NodeEdit+Reload+Restore und absichtlichungültigerSnapshot+Restore bestanden.
Alle110Bots imvorigenLaufgeladen:2640Planslots,0leereZiele,0Metadatenverstöße;
298Slots ohneGebietsmetadaten werden imProduktionsplaner überKoordinatengeprüft.
Nachweise doc/clientless/checkpoints/2026-10-29-area-catalog/; Doku area-catalog.md.
LETZTER TESTSERVERSTART: neuer frischer tmux vcore-worldserver mit
/tmp/vcore-area-start-verified-20261029.log (nach finalesPortalFollow-Update).
BeiWiederaufnahme zuerstReady+Prozess+Port8085 und Loaderstatus prüfen!
Worldserver/Bots laufenlassen. Zieleweiter50/5/50/5. KeineProduktion, keinPush,
keinCommit. Branch feature/clientless-player-core Basis0173a434f4, erwarteteWIP
Katalogdaten+Integration+Tests+Docs. SQL02 und03 explizit gitadd-f, da*.sqlignoriert.
Grenzen: keinevisuelleAbnahme, gespeicherteTaxi-Hänger/gestrandeteBots nicht
teleportiert/behoben. Bewegungssampling40yards statt mathematischerGrenzbeweis.
Kalte/großeDebug-Routen teilsmehrereSekunden, weiterePerformancearbeitbleibt.
KnownMaps719/736 fehlenweiter. EinfrüherTestshutdownmitzuvordeaktiviertemKatalog
hatteGetMapAssertnachHalting; nachfolgendeShutdownsnichtreproduziert, Rohlog
/tmp/vcore-area-start-20261029.log. SQLKollationsfehlerseitdembehoben.
Nächste Schritte für Abschluss dieserTurnarbeit: finalenServerstartverifizieren,
Nativeprobe26/32undPopulationstatusnochmalsprüfen, finalePID/Loaderstandhier
kurzergänzen. KeineweiterenCodeänderungennötig. DannkurzerdeutscherAbschlussbericht
mitFeatures, Build/10Tests/Livechecks, Reloadkommando und verbleibendenGrenzen.
---
# Aktuell29.10.2026: Gebietskatalog angelegt, Integration steht AUS
Nachtbeobachtung beendet07:00CET; automatischer Wächter lief trotz Turnunterbrechung
bis Deadline. Nutzer erhielt Bericht nach07:27. Server lief ohne Neustart,
110Bots. Befunde Taxi-Hänger, Fraktionsrouten, Todesschleifen, Navigation.
Vorheriger Nachtauftrag unten ist abgeschlossen, NICHT erneut ausführen.
Neuester Nutzerauftrag zieht vor jeder weiteren Implementierung einen ohne
Neukompilierung/Installation bearbeitbaren Gebietskatalog vor. Dafür ausschließlich
Datenbasis angelegt, NOCH KEINE Runtime-Regeln oder C++-Änderungen!
WorldDB: vcore_adventure_area_catalog(3206AreaTable-Gebiete inkl2931Untergebiete),
vcore_adventure_map_catalog(207Karten inkl1fehlender nativer Map alsTyp255).
Migration sql/custom/world/2026_10_29_02_adventure_area_catalog.sql bereits auf TEST
angewandt; reproduzierbarer DBC-Exporter contrib/clientless/export_area_catalog.py.
Bestehende78Levelbereiche aus vcore_adventure_zone übernommen. Doku
doc/clientless/area-catalog.md erklärt Felder, SQL-Änderungen und offene Integration.
Erneutimport identisch, keine fehlenden Eltern/Karten, keine Zyklen, Vashjir-Chain
geprüft. Native Spieldaten/C++/laufender Server unverändert. Kein Build nötig,
kein Restart, kein Commit/Push. Bestehende SESSION-RESUME-WIP erhalten.
Vereinbarte spätere Integration: Gebiet einschließlich Vorfahren über Node über
Verbindung; Fraktionsmasken0=zu,1=Allianz,2=Horde,3=beide,NULL=erben. Kind/neutraler
Node darf Elternsperre nicht aufheben. Gebiete weit über Botlevel meiden auch bei
Transit; DefaultLevelGap5, max_level ist KEIN Verbot für höherstufige Heimkehrer.
Vashjir5146 komplett zu inkl4815/5144/5145. Startregionen/Hauptstädte fraktionsgebunden.
Scherbenwelt/Nordend/Cata-Flugpräferenz nur bei nativer Erlaubnis/Fähigkeit.
Karte530 enthält auch Startergebiete -> nicht pauschal Outland-Fluggebiet!
Alle Nodes müssen individuelle Fraktionsfreigabe erhalten können. Flugknoten
automatisch aus zugeordneten Flugmeister-NPC-Fraktionsvorlagen ableiten, neutrale
beiden erlauben, unklare nicht automatisch neutral. Tiefenbahn und Schiff
Sturmwind-Teldrassil für Horde sperren. Hauptstadtportale Ziel-Fraktion beachten.
Start/Ziel/Zwischenstationen, virtuelle Graphverbindungen, lokale Wege und Executor
absichern. Bot in gesperrtem Gebiet braucht sicheren Ausweg. Runtime-Katalog-
Reload erst in nächster Integration einbauen; bisheriger adventure reload wertet
neue Tabellen NICHT aus. Eine erstmalige Anbindung benötigt Build/Installation,
danach dürfen Katalogänderungen keinen Build/Restart brauchen.
---
# AKTIVER NACHTAUFTRAG: Bis29.10.2026 07:00CET beobachten und DANN berichten
Nutzer geht schlafen und bittet, Bots frei ihren Intents folgen zu lassen,
regelmäßig Logs auf Fehler/Auffälligkeiten zu prüfen und um07:00 mitteleuropäisch
Beobachtung zu beenden und Bericht zu geben. CET/Berlin+01:00 heute korrekt,
Deadline2026-10-29T07:00:00+01:00 (06:00UTC). NICHT frühzeitig mit Abschlussbericht
enden. Aktiven Turn bis Deadline betreuen, neue Benutzersteuerung berücksichtigen.
Worldserver/Bots nach07:00 NICHT stoppen, nur Beobachtung beenden.
Aktueller Commit0173a434f4, Branch feature/clientless-player-core, Quellcode sauber
zu Auftragsbeginn; nur diese Resume-Notiz geändert. KEINE Features/Fixes/Commits/
Deployments während Beobachtung. Keine Produktion, keine Subagents. Native Test-
Crash-Recovery ist gemäß bestehenden Serverregeln erlaubt, nicht bei Warnungen.
Worldserver PID214924, tmux vcore-worldserver, Installation server/debug/bin,
Console /tmp/vcore-performance-retry-start1.log.110Bots50/5/50/5 normaler Tagesplan,
Mapworker4. Gruppenfindungsmanipulation um01:00 abgelaufen und Helfer inaktiv.
NativeStatus-Befehle sind nur lesend. Fehlende Karten719/736 und Transportprobleme
(Lunna49, falsche Fraktionsroute Falaeriel54) schon VORHER bekannt, getrennt werten.
Persistenter Wächter gestartet in eigener tmux-Session
vcore-overnight-monitor-20261029. Skript
/tmp/vcore-overnight-20261029/monitor.py, Parameter --until2026-10-29T07:00:00+01:00
--out/tmp/vcore-overnight-20261029 --recover. NICHT doppelt starten oder State
überschreiben! Console /tmp/vcore-overnight-20261029/monitor-console.log.
Liest minütlich ab aktuellem Dateiende Server.log, DBErrors.log,
adventurebot-navigation.log (inkl.Rotation) und Console. Native Population alle5min,
alle15min Intent-/Transportstatus und gespeicherte110Botpositionen/Fertigkeiten
(nur gewählte Characterfelder, KEINE Credentials). CPU/RSS minütliche Deltas.
DB-Abfragenfehler werden nur protokolliert, kein Eingriff ins Spielverhalten.
Watch-Skript geprüft mit2Einzelzyklen, letzter01:51CET/110Charaktere/kein Fehler.
Dateien: status.json (kurz), state.json (alle Daten), events.jsonl (Auffälligkeiten),
characters.jsonl (15min-Snapshots), report.md (laufende Zusammenfassung, final um07).
Script zählt native Ereignisse und Warnungen getrennt. Fehlende Maps separat;
Transit>15min und wiederholtes gespeichertes Transguid>30min sind Kandidaten,
keine automatisch bestätigten Bugs. Server-/Nav-/DB-Fehler werden gesammelt,
CPC_PERF/SlowRoutes ebenfalls. Gespeicherte Positionen können90s alt sein.
Bei Prozessverlust: Wächter startet NUR exakt diese Testinstallation, höchstens3
Versuche je Recovery, jeweils frische Readiness/Portkontrolle, ca10s zwischen
Versuchen. Mehr als3Crash-Episoden/30min -> Recovery aus. Nicht bei Warnungen neu
starten! Prozess vorhanden aber keine CPC-/Populationantwort>10min -> Event
possible_hang; dann selbst diagnostizieren, nicht blind Neustart. Unbekannte
Prozessmengen werden nicht angerührt. Bei07 stoppt Watcher und schreibt Endbericht;
Server bleibt an. Falls der Turn unterbrochen wurde, vorhandene Dateien lesen,
bis07 weiter beobachten oder bei Wiederaufnahme nach07 Ergebnis berichten.
Nächste Schritte: status.json nach erstem Minutenzyklus prüfen, dann regelmäßig
(ca5min) selbst neue Events/Counter/Population/Performance auswerten; Tool-Waits
höchstens60s, sinnvoll clock.sleep60000 oder functions.wait yield<=60000 verwenden.
Nicht einfach zurückgeben, bevor07. Am Morgen aussagekräftige Befunde mit Uhrzeiten,
Crash-/Neustartzahl, Botzahl, Datenfehlern/Transport-Kandidaten/Leistung berichten;
keine visuellen Gameplay-Verifikationen behaupten. Rohbericht ggf. nach
Doc-Checkpoint kopieren, kein Commit ohne Nutzerauftrag.
Zusatz zur Nacht: Exakte kategorisierte Nav-Auswertung per
`python3 /tmp/vcore-overnight-20261029/audit-navigation.py`; inkrementell über
aktuelle/rotierte Navlogs, Timestampfilter01:51:59..07:00. Zustand navigation-audit.json.
Um07 unbedingt zuletzt ausführen: Hauptwächter normalisiert Timestamps nicht,
seine auf300begrenzten Warnsignaturen laufen früh über. Ereigniszähler/CPU/Population
und Warnkategorien bleiben korrekt, für Navgründe/Severity/Actors Audit verwenden!
Manuelle Notizen manual-observations.md.
Zusätzlicher rein lesender sample-actions.py (gleiches/tmp-Verzeichnis) sammelt
110native Aktionszustände und markiert lange Taxi-Legs ohne Complete zwischen
Stichproben. Erstes Sample02:33:59,18Taxi-Legs; nächste ca02:49, dann15min.
Dateien actions.jsonl, taxi-candidates.jsonl. Verdacht vor allem2301(608Map0->23Map1),
807(6Map0->213Map530), außerdemGarenden62auf2108(sameMap1). Siehe manuelle
Notizen; mögliche fehlende Teleport-ACKs nicht als bewiesen ausgeben. Nicht fixen! Stand02:50:110stabil, keine Neustarts,
Nav-ERROR bisher alles RECOVERY_FAILED; viele normale WARN/Retry-Pfadabbrüche.
2neue DBErrors sind fehlende creature_text für Stormwind Royal Guard42218,
keine SQL-Verbindungsfehler. Letzter Snapshot02:07 keine gespeicherten Passagiere.
Keine Eingriffe in Bot-/Serververhalten.
Neuste relevante Befunde: Darilwin127(Worgen27) inUnterstadt beiCaptainDonaldAdams60
in sehr schneller Tod/Reclaim-Schleife; RuhezielStormwindBench, solo. Details und
andere Betroffene in manual-observations.md, Zähler nicht blind als eindeutige
Todesanzahl werten. ZweiteAktionsprobe02:50:17Taxi-Legs seit02:33:59gleich, ohne
Complete in Console dazwischen. VieleMapwechsel2301/807/766/767, zusätzlich62sameMap2108.
NächsteActions-Probe ca03:05, dann15min nach letztem actions.jsonlZeitstempel.
110online/keineRecovery/CPUca25%main/RSS4.4GiB. Watch läuft unverändert bis07!
---
# Aktuell: Performance-Verbesserungen implementiert und mit110Bots gemessen
Nutzer beauftragte geringere Last/bessere Mehrkernnutzung im Hinblick auf spätere
+158
View File
@@ -0,0 +1,158 @@
# Veränderbarer Gebietskatalog der AdventureBots
Der Katalog liegt in der **World-Datenbank**, nicht in C++ oder in den Nodes.
Migration: `sql/custom/world/2026_10_29_02_adventure_area_catalog.sql`.
Sie setzt die bestehende Migration `2026_10_29_00_adventure_population.sql`
mit `vcore_adventure_zone` voraus. Erneutes Einspielen erhält vorhandene Einträge.
## Stand und Geltungsbereich
Die Regeln sind in Tagesplanung, TravelGraph und Bewegungsausführung angebunden.
`clientless adventure reload` lädt Gebiets- und Nodefreigaben sowie Location- und
TravelGraph-Kataloge im laufenden Betrieb neu. Spätere Änderungen benötigen
keinen Build, keine Installation und keinen Neustart. Ungültige Gebietskataloge
(z.B. Elternzyklen oder unzulässige Masken) werden vor Austausch des aktiven
Snapshots abgewiesen.
Neue Tagespläne wählen nur zulässige Locations. Bestehende Tagespläne werden
beim Einloggen und nach erfolgreichem Reload geprüft: Unzulässige Ziele werden
ersetzt und gespeichert; Intents und Stunden bleiben erhalten. Sind keine
zulässigen Ziele vorhanden, bleibt das Ziel leer, statt eine Sperre zu umgehen.
Die aktuelle Mikroaktion oder ein bereits gestarteter nativer Transport wird
nicht mitten in einer Bewegung gewaltsam abgebrochen. Neue Reiseabschnitte
werden erneut geprüft. Ein bereits in einem gesperrten Gebiet stehender Bot wird
nicht automatisch teleportiert; diese Regeln ersetzen keine eigene Bergungslogik.
Die Regeln betreffen ausschließlich autonome AdventureBots. Sie sperren keine
Gebiete für menschliche Spieler und verändern weder DBC noch NPC-Fraktionen.
## Inhalt
`vcore_adventure_map_catalog` enthält Karten-ID, Anzeigename, Kartentyp und
Erweiterung. Kartentyp 255 markiert alte Gebiete ohne vorhandenen Map.dbc-Eintrag.
Solche Gebiete sind ausdrücklich gesperrt. Dungeon-/Raid-/Schlachtfeldkarten
bleiben gekennzeichnet und werden nicht als normale offene Welt behandelt.
`vcore_adventure_area_catalog` enthält alle Gebiete und Untergebiete aus der
lokalen Cataclysm-4.3.4-AreaTable.dbc, mit Namen, Karten-ID und Elterngebiet.
Die Namen sind hier deutsch; die stabile Referenz ist immer die numerische ID.
| Spalte | Bedeutung |
| --- | --- |
| `area_id` | Native AreaTable-ID, auch für übergeordnete Zonen |
| `parent_area_id` | Übergeordnetes Gebiet; 0 bedeutet Wurzelgebiet |
| `allowed_factions` | NULL: erben; 0: gesperrt; 1: Allianz; 2: Horde; 3: beide |
| `min_level`, `max_level` | Empfohlener Levelbereich; NULL: erben/unbekannt |
| `max_level_gap` | Erlaubter Abstand unter der Mindeststufe; NULL: erben, Standard 5 |
| `prefer_flight` | NULL: erben; 0: normale Reisegewichtung; 1: erlaubte Flüge bevorzugen |
| `notes` | Frei bearbeitbare Begründung/Notiz |
Die Mindeststufe ist eine untere Zugangsgrenze mit der angegebenen Toleranz.
`max_level` sperrt höherstufige Bots nicht aus ihren Heimatgebieten aus.
Die vorhandenen, manuell gepflegten Levelbereiche aus `vcore_adventure_zone`
werden beim Import übernommen. Unbekannte Bereiche bleiben NULL, statt einen
nicht belegten sicheren Levelbereich vorzutäuschen. Unbekannte Freilandbereiche sind unter Stufe85 gesperrt.
## Verbindliche Hierarchie
Alle Vorfahren eines Gebiets sind zu berücksichtigen. Fraktionsfreigaben werden
geschnitten: Ein Kind mit Freigabe 3 kann einen gesperrten Elternbereich nicht
freigeben. Dasselbe gilt anschließend für einen neutralen Node in einem
fraktionsgesperrten Gebiet. Ein Node darf zusätzliche Einschränkungen besitzen,
aber die Gebietsfreigabe nicht erweitern. Die geltende Reihenfolge ist
**Karte vorhanden → Gebiet samt Vorfahren → Node → Verbindung/Transport**.
NULL bedeutet Vererbung, ausdrücklich 0 bedeutet bei Fraktionen Sperre und bei
Flugpräferenz normale Gewichtung. Flugpräferenzen ersetzen keine Reitfähigkeit,
Fluglizenzen, nativen Flugverbote oder Kollisionsprüfung. Sichere Endpunkte
allein reichen nicht für sichere Wege: Zwischengebiete müssen ebenfalls geprüft
werden. Ein bereits in einem gesperrten Gebiet stehender Bot braucht einen
gesonderten sicheren Ausweg, statt die Sperre für reguläre Reisen aufzuheben.
## Voreinstellungen
- Rassische Startregionen und Hauptstädte haben Fraktionsfreigaben. Darunter
Dun Morogh/Elwynn/Teldrassil/Azur- und Blutmythosinseln/Gilneas für Allianz
sowie Durotar/Tirisfal/Mulgore/Immersangwald/Geisterlande/Kezan/Verlorene Inseln
für Horde. Die zugehörigen Hauptstädte sind ebenfalls eingetragen.
- Vashj'ir (5146) ist für alle gesperrt. Tang'tharwald (4815), Schimmernde Weiten
(5144) und Abyssische Tiefen (5145) erben diese Sperre über die echte DBC-Hierarchie.
- Die Scherbenweltgebiete einschließlich Shattrath, Nordend sowie Hyjal,
Tiefenheim, Uldum, Schattenhochland und Tol Barad bevorzugen erlaubtes Fliegen.
Native Flugverbote haben stets Vorrang. Karte 530 wird bewusst nicht pauschal
als Scherbenwelt behandelt: Sie enthält auch die Blutelfen-/Draenei-Startgebiete.
Tiefenbahn und Schiff Sturmwind–Rut'theran haben explizite Allianz-Nodefreigaben.
Hauptstadtportale werden über ihre Zielgebiete gesperrt; auch neutrale Quellnodes
können diese Sperre nicht umgehen.
## Nodefreigaben
Migration: `sql/custom/world/2026_10_29_03_adventure_node_access.sql`.
`vcore_clientless_node_access` erlaubt für jeden Node eine zusätzliche
`allowed_factions`-Maske mit gleicher Bedeutung wie bei Gebieten. NULL bedeutet
keine manuelle Einschränkung. Neue Nodes können über INSERT ergänzt werden.
Die Tabelle enthält initial alle vorhandenen Nodekeys.
Flugknoten werden bei jedem Reload automatisch einem Flugmeister im Umkreis von
100 Yards zugeordnet. Maßgeblich sind die Fraktionsvorlagen der NPCs und ihre
Feindseligkeit gegenüber Allianz/Horde; neutrale Flugmeister erlauben beide.
Unklare Zuordnungen bleiben gesperrt. `flightmaster_spawn_id` ermöglicht eine
explizite Zuordnung zu einem bestimmten NPC-Spawn innerhalb dieses Umkreises.
Manuelle Masken können native NPC-Freigaben weiter einschränken, nicht erweitern.
Die tatsächlich berechnete Maske ist im Diagnosebefehl als `nodeMask` sichtbar.
```sql
UPDATE vcore_clientless_node_access SET allowed_factions=1
WHERE node_key='mein_node';
```
Nach einer Änderung an Gebieten oder Nodes in der Serverkonsole:
```text
clientless adventure reload
clientless adventure area 26 taxi_11
```
Die Diagnose zeigt für den angegebenen Online-Bot das aufgelöste Gebiet,
Fraktionsmaske, Levelgrenze, Flugpräferenz, Nodefreigabe und Gesamterlaubnis.
Ohne Nodekey wird die aktuelle Position geprüft. Die Zahl26 ist hier nur ein
Beispiel für Arthoran; der Befehl funktioniert mit jedem Online-AdventureBot.
Gespeicherte Bodenwege und lokale MMAP-Wege werden an ihren Wegpunkten und
zusätzlich mit maximal40Yards Abstand geprüft. Flugtaxiwege berücksichtigen ihre
nativen Wegpunkte, freie Flüge ihre geplanten und tatsächlich gestarteten
Abschnitte. Virtuelle Graphverbindungen verwenden dieselben Prüfungen, auch bei
zwischengespeicherter Weggeometrie. Diese räumliche Abtastung ist keine mathematisch
lückenlose Prüfung jedes einzelnen Grenzpunkts. Schiffs-/Tiefenbahnfahrten prüfen
beide Haltestellen und Nodefreigaben statt einen Bodenweg quer durch das Meer.
## Bearbeitung ohne Neukompilierung
Beispiel: Gebiet und seine unmittelbar untergeordneten Gebiete ansehen:
```sql
SELECT * FROM vcore_adventure_area_catalog
WHERE area_id=5146 OR parent_area_id=5146;
```
Beispiel: Flugpräferenz und Leveltoleranz eines Gebiets bearbeiten:
```sql
UPDATE vcore_adventure_area_catalog
SET prefer_flight=1, max_level_gap=5, notes='Flugreisen bevorzugen'
WHERE area_id=616;
```
Nicht die generierte SQL-Datei erneut einspielen, um vorhandene Zeilen zu ändern:
`INSERT IGNORE` bewahrt absichtlich Betreiberanpassungen. Stattdessen gezielte
UPDATEs verwenden. Die alte Leveltabelle dient nur noch als Importquelle; der laufende Server
verwendet die Levelbereiche dieses Katalogs. Änderungen der alten Tabelle
werden nicht automatisch in diesen Katalog kopiert.
Die Ausgangsdaten lassen sich ohne DB-Verbindung reproduzieren:
```bash
python3 contrib/clientless/export_area_catalog.py /pfad/zu/dbc/deDE /tmp/area-catalog.sql
```
@@ -0,0 +1,10 @@
TrinityCore rev. 0173a434f49d+ 2026-10-29 01:36:35 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
pool=1 faction=alliance persistent=0 target=5 online=5 pending=0 saved=5
pool=2 faction=horde persistent=1 target=50 online=48 pending=0 saved=50
pool=3 faction=horde persistent=0 target=5 online=5 pending=0 saved=5
[?2004hTC>Adventure loader enabled=1 schema=1 busy=0 pools=50/5/50/5 known=110 rotations=0 manager=110 error=''
pool=0 faction=alliance persistent=1 target=50 online=50 pending=0 saved=50
pool=1 faction=alliance persistent=0 target=5 online=5 pending=0 saved=5
pool=2 faction=horde persistent=1 target=50 online=50 pending=0 saved=50
pool=3 faction=horde persistent=0 target=5 online=5 pending=0 saved=5
PID247021; TCP8085 listening; no ADVENTURE_DISABLED, assertion or SQL collation error in this startup.
@@ -0,0 +1,16 @@
[
"Area access GUID 26: revision=1 map=0 area=1497 factionMask=2 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=2 allowed=0",
"Area access GUID 26: revision=1 map=0 area=5342 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=1 allowed=1",
"Area access GUID 26: revision=1 map=0 area=5144 factionMask=0 minLevel=80 maxLevel=82 gap=5 knownLevel=1 preferFlight=0 nodeMask=2 allowed=0",
"Area access GUID 32: revision=1 map=369 area=2257 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=1 allowed=0",
"Area access GUID 32: revision=1 map=0 area=4411 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=1 allowed=0",
"Area access GUID 32: revision=1 map=0 area=1519 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=3 allowed=0",
"Area access GUID 26: revision=1 map=530 area=3703 factionMask=3 minLevel=60 maxLevel=85 gap=5 knownLevel=1 preferFlight=1 nodeMask=3 allowed=1",
"Area access GUID 32: revision=1 map=530 area=3703 factionMask=3 minLevel=60 maxLevel=85 gap=5 knownLevel=1 preferFlight=1 nodeMask=3 allowed=1",
"Area access GUID 26: revision=1 map=571 area=4564 factionMask=3 minLevel=68 maxLevel=85 gap=5 knownLevel=1 preferFlight=1 nodeMask=3 allowed=0",
"Adventure catalogs reloaded.",
"Area access GUID 26: revision=2 map=0 area=5342 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=0 allowed=0",
"Area access GUID 26: revision=3 map=0 area=5342 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=1 allowed=1",
"Adventure catalog reload failed; check migration.",
"Area access GUID 26: revision=3 map=0 area=5342 factionMask=1 minLevel=1 maxLevel=85 gap=5 knownLevel=1 preferFlight=0 nodeMask=1 allowed=1"
]
@@ -0,0 +1,7 @@
{
"online_generated_bots": 110,
"checked_day_slots": 2640,
"empty_locations": 0,
"positions_without_declared_area": 298,
"violations": []
}
@@ -0,0 +1,29 @@
test_adventure_area_policy.py
Area inheritance, faction intersection, level limits, flight overrides and invalid data passed.
test_adventure_area_segment.py
Segment geography cache, faction isolation, reload permissions, lazy completion and terrain swaps passed.
test_adventure_behavior_policy.py
24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed.
test_adventure_day_persistence.py
Production day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed
test_adventure_intent_state.py
Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.
test_graph_probe_cache.py
Graph probe cache: exact directed/map/instance keys, successful geometry only, invalidation and bounded storage passed
test_free_flight_planner.py
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
test_flight_policy.py
Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.
test_adventure_group_plan.py
Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.
Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
@@ -0,0 +1,88 @@
# Gebietskatalog: technische Abnahme vom 29.10.2026
Branch `feature/clientless-player-core`, Basiscommit `0173a434f4`, Änderungen
noch nicht committet. Ausschließlich dedizierter Testserver und dessen Datenbank.
## Implementierung
- Gebietskatalog und Vorfahren: Fraktionsschnittmenge, Mindestlevel mit
konfigurierbarer Toleranz, ererbte Flugpräferenz; unbekannte Freiland-Levelbereiche
unter Stufe85 gesperrt. Keine obere Sperre für Heimkehrer über dem Gebietslevel.
- Tagesplanung prüft Metadaten und native Gebietsauflösung der Zielkoordinaten.
Beim Laden/Reload nur illegale Ziele ersetzen; Intents/Stunden bleiben bestehen.
Die neuen Levelbereiche ersetzen die alte Tabelle als Laufzeitquelle.
- Nodefreigaben für sämtliche Nodearten, automatische Flugmeisterzuordnung aus
nativen NPC-Fraktionsvorlagen einschließlich Spawn-NPCflag-Overrides. Neutrale
Meister bleiben beidseitig erreichbar; ungeklärte Zuordnungen werden gesperrt.
- TravelGraph, virtuelle Brücken/Ein-/Ausstiege, tatsächliche MMAP-/aufgezeichnete
Bodenwege, Flugtaxiabschnitte und freie Flüge berücksichtigen Zugangsregeln.
Tiefenbahn und Sturmwind–Rut'theran-Schiff explizit Allianz. Hauptstadtportale
prüfen ihr Zielgebiet, einschließlich direkter Portalfolge bei Spielergruppen.
- `clientless adventure reload` übernimmt Regeln ohne Build/Installation/Restart.
Ein ungültiger Gebietssnapshot ersetzt den bisherigen nicht.
- Geometrieproben werden zwischen Bots wiederverwendet, inklusive nativer
Terrain-Swaps. Berechtigungen bleiben von Fraktion/Level/aktuellem Katalog
abhängig. Die Fluggraphsuche verwirft vor Terrainproben mathematisch sicher
Verbindungen, die eine schon gefundene Route nicht verbessern können.
## Nachweise
Build mit unveränderter Debug-CMake-Konfiguration erfolgreich. Letztes Buildlog
`/tmp/vcore-area-build-verified.log`; Installation jeweils erst nach beendetem
Worldserver. Zehn Testprogramme bestanden (siehe `tests.log`), darunter echte
Produktionsfunktionen für Planwiederherstellung, verbotene virtuelle Routen trotz
Geometriecache, Segmentcache/Fraktionswechsel/Reload, Flugpolitik und Gruppenreise.
`live-access.json` dokumentiert native Serverdiagnosen:
- Arthoran: Unterstadt verboten, Eisenschmiede erlaubt, Vashj'ir verboten.
- Aldren: Tiefenbahn und Allianzschiff verboten.
- Neutraler Portalnode nach Sturmwind: nodeMask3, dennoch Horde verboten.
- Neutraler Shattrath-Flugknoten: beide Fraktionen erlaubt.
- Dalaran: Arthoran mit Stufe60 wegen Mindestlevel68/Toleranz5 ausgeschlossen.
- Taxi6 im laufenden Betrieb gesperrt, per Reload wirksam, Originalwert wieder
hergestellt und erneut erlaubt. Kein Build/Restart zwischen diesen Schritten.
- Absichtlich ungültige Flugpräferenz2: Reload abgewiesen, gültiger Snapshot
weiter verwendet; DB-Originalwert anschließend wiederhergestellt.
`plans-110.json`: Alle110 Bots geladen, 2640 persistierte Tagesplanblöcke geprüft,
keine leeren Ziele und keine Verstöße in den hinterlegten Gebietszuordnungen.
298Slots hatten keine explizite Gebiets-ID; diese Zahl umfasst Mehrfachnutzung
derselben Location. Ihre Koordinaten prüft der Produktionsplaner nativ, sie sind
nicht Teil der zusätzlichen Metadatenprüfung dieses SQL-Audits. Instanzeingänge
können als Metadatum die Instanz-Area führen, obwohl ihr tatsächlicher Worldpoint
außerhalb liegt; Kartentypen sind deshalb im Audit berücksichtigt.
## Grenzen und aufgetretene Probleme
Keine visuelle Gameplay-Abnahme. Wegpunkte werden mit höchstens40Yards Abstand
geprüft, nicht jeder mathematische Punkt einer Gebietsgrenze. Bereits laufende
native Flüge/Transporte werden bei Reload nicht gewaltsam abgebrochen. Bereits
gestrandete Bots werden nicht teleportiert. Die früher beobachteten Taxi-Hänger
und Todesschleifen sind damit nicht als generell behoben nachgewiesen.
141der455 aktiven Taxinodes konnten keinem benutzbaren statischen Flugmeister
zugeordnet werden und bleiben zunächst geschlossen;314 wurden zugeordnet.
Explizite NPC-Spawn-Bindungen sind über die Nodezugangstabelle möglich.
Ein anfänglicher SQL-Kollationskonflikt wurde durch die zur bestehenden
Nodetabelle passende `utf8mb4_unicode_ci`-Kollation behoben. In einem frühen
Testshutdown mit zuvor deaktivierten Botkatalogen erschien ein `GetMap`-Assert
nach `Halting process`; bei den nachfolgenden Shutdowns nicht reproduziert.
Rohlog dafür: `/tmp/vcore-area-start-20261029.log`. Kein solcher Assert während
des anschließenden Laufs mit110Bots beobachtet.
Kalte und große Routenprüfungen verursachen im Debug-Build weiterhin einzelne
mehrsekündige Pausen. Die Caches und Kostenuntergrenze begrenzen Zusatzarbeit,
ersetzen aber keine gesonderte weitere Performancearbeit. Bekannte fehlende
Mapdateien719/736 und Worlddatenfehler bleiben unabhängig davon vorhanden.
## Letzter installierter Stand
Nach dem abschließenden Gruppenportal-Guard und der nativen NPCflag-Override-
Korrektur erneut gebaut und installiert. Frischer Start in
`/tmp/vcore-area-start-verified-20261029.log`, PID247021. Ready-Meldung, Prozess,
Port8085 und native Population110/0pending/errorleer geprüft. Alle vier Pools
50/5/50/5 vollständig online. Erneuter Audit2640Slots ohne leere Ziele oder
Metadatenverstöße. Frische Zugriffsproben siehe `final-server-smoke.log`.
Keine ADVENTURE_DISABLED-, Assertion- oder SQL-Kollationsmeldung in diesem Start.
+4
View File
@@ -42,6 +42,10 @@ not rewritten. Existing stored day plans are retained.
## Geography and daily plans
The live access and flight rules now use the [editable area catalog](area-catalog.md),
including inherited faction restrictions and minimum levels. The older leveling
table below remains an import source, rather than the runtime authority.
Apply `sql/custom/world/2026_10_29_00_adventure_population.sql`. Its editable
`vcore_adventure_zone` catalog describes outdoor leveling bands and capital
accessibility for Cataclysm. Reapplying preserves operator edits. Unknown outdoor
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,21 @@
-- Per-node restrictions further narrow the enclosing area's permissions.
-- NULL means no manual restriction. Taxi nodes additionally require a usable
-- flightmaster derived from native creature faction templates at catalog reload.
CREATE TABLE IF NOT EXISTS vcore_clientless_node_access (
node_key VARCHAR(100) NOT NULL PRIMARY KEY,
allowed_factions TINYINT UNSIGNED DEFAULT NULL COMMENT 'NULL automatic/inherit; 0 closed; 1 Alliance; 2 Horde; 3 both',
flightmaster_spawn_id INT UNSIGNED DEFAULT NULL COMMENT 'Optional explicit taxi NPC binding; NULL nearest within 100 yards',
notes VARCHAR(255) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO vcore_clientless_node_access(node_key,allowed_factions,notes) VALUES
('tram_sw',1,'Alliance Deeprun Tram'),
('tram_if',1,'Alliance Deeprun Tram'),
('ship_sw_br',1,'Alliance Stormwind-Ruttheran ship'),
('ship_ruttheran_br',1,'Alliance Stormwind-Ruttheran ship');
-- Preserve operator overrides; expose every existing node for later editing.
INSERT IGNORE INTO vcore_clientless_node_access(node_key)
SELECT node_key FROM vcore_clientless_travel_node;
-- The interior tram map has no outdoor leveling band to inherit.
UPDATE vcore_adventure_area_catalog
SET allowed_factions=1,min_level=1,max_level=85,notes='Alliance Deeprun Tram interior'
WHERE area_id=2257 AND allowed_factions IS NULL AND min_level IS NULL;
+241
View File
@@ -0,0 +1,241 @@
#include "AdventureAreaMgr.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessTravelGraphMgr.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "PhaseShift.h"
#include "TerrainMgr.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <limits>
AdventureAreaMgr* AdventureAreaMgr::instance()
{
static AdventureAreaMgr mgr;
return &mgr;
}
bool AdventureAreaMgr::EnsureLoaded() { return _loaded || Reload(); }
bool AdventureAreaMgr::Applies(Player const* player) const
{
return player && sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
}
bool AdventureAreaMgr::Reload()
{
// Build a complete new snapshot before replacing the currently working one.
auto rows = WorldDatabase.Query("SELECT area_id,map_id,parent_area_id,allowed_factions,min_level,max_level,max_level_gap,prefer_flight FROM vcore_adventure_area_catalog");
if (!rows) return false;
std::unordered_map<unsigned, AdventureAreaPolicy::Area> areas;
do
{
auto f = rows->Fetch();
AdventureAreaPolicy::Area a;
a.Map=f[1].GetUInt32(); a.Parent=f[2].GetUInt32();
int* values[]{&a.Factions,&a.MinLevel,&a.MaxLevel,&a.LevelGap,&a.Flight};
for (unsigned i=0;i<5;++i) *values[i]=f[i+3].IsNull() ? -1 : f[i+3].GetUInt8();
if (a.Factions>3 || a.MinLevel>85 || a.MaxLevel>85 || a.LevelGap>85 || a.Flight>1 ||
(a.MinLevel>=0 && a.MaxLevel>=0 && a.MinLevel>a.MaxLevel)) return false;
areas.emplace(f[0].GetUInt32(),a);
} while(rows->NextRow());
std::unordered_map<uint32,uint8> maps;
auto mapRows=WorldDatabase.Query("SELECT map_id,map_type FROM vcore_adventure_map_catalog");
if (!mapRows) return false;
do { auto f=mapRows->Fetch(); maps.emplace(f[0].GetUInt32(),f[1].GetUInt8()); } while(mapRows->NextRow());
std::unordered_map<unsigned,AdventureAreaPolicy::Result> resolved;
for(auto const& pair:areas)
{
auto policy=AdventureAreaPolicy::Resolve(areas,pair.first);
if(!policy.Valid || !maps.count(pair.second.Map)) return false;
resolved.emplace(pair.first,policy);
}
struct Master { uint32 Spawn,Map,Faction; Position Pos; };
std::vector<Master> masters;
auto npcs=WorldDatabase.Query("SELECT c.guid,c.map,c.position_x,c.position_y,c.position_z,t.faction FROM creature c JOIN creature_template t ON t.entry=c.id WHERE (COALESCE(c.npcflag,t.npcflag) & 8192)<>0");
if(!npcs) return false;
do
{
auto f=npcs->Fetch(); Master m;
m.Spawn=f[0].GetUInt32();m.Map=f[1].GetUInt16();m.Pos.Relocate(f[2].GetFloat(),f[3].GetFloat(),f[4].GetFloat());m.Faction=f[5].GetUInt16();
masters.push_back(m);
} while(npcs->NextRow());
auto maskFor=[&](uint32 faction)
{
auto npc=sFactionTemplateStore.LookupEntry(faction);
auto alliance=sFactionTemplateStore.LookupEntry(1),horde=sFactionTemplateStore.LookupEntry(2);
if(!npc || !alliance || !horde) return 0u;
return unsigned(!npc->IsHostileTo(alliance) && !alliance->IsHostileTo(npc)) |
(unsigned(!npc->IsHostileTo(horde) && !horde->IsHostileTo(npc))<<1);
};
std::unordered_map<std::string,uint8> nodes;
auto nodeRows=WorldDatabase.Query("SELECT n.node_key,n.node_type,w.map,w.position_x,w.position_y,w.position_z,a.allowed_factions,a.flightmaster_spawn_id FROM vcore_clientless_travel_node n JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key LEFT JOIN vcore_clientless_node_access a ON a.node_key=n.node_key WHERE n.enabled=1 AND w.enabled=1");
if(!nodeRows) return false;
unsigned taxiKnown=0,taxiUnknown=0;
do
{
auto f=nodeRows->Fetch(); std::string key=f[0].GetString();
unsigned mask=f[6].IsNull()?3:f[6].GetUInt8();
if(mask>3) return false;
if(f[1].GetString()=="taxi")
{
uint32 map=f[2].GetUInt16(),binding=f[7].IsNull()?0:f[7].GetUInt32();
Position pos;pos.Relocate(f[3].GetFloat(),f[4].GetFloat(),f[5].GetFloat());
float nearest=std::numeric_limits<float>::max(); unsigned automatic=0;
for(auto const& m:masters)
{
if(m.Map!=map || (binding && binding!=m.Spawn)) continue;
float d=pos.GetExactDist(&m.Pos);
if(d>100.0f) continue;
if(d+0.5f<nearest) { nearest=d;automatic=maskFor(m.Faction); }
else if(std::fabs(d-nearest)<=0.5f) automatic|=maskFor(m.Faction);
}
mask &= automatic;
if(automatic) ++taxiKnown;else ++taxiUnknown;
}
nodes.emplace(key,uint8(mask));
} while(nodeRows->NextRow());
_areas.swap(areas);_resolved.swap(resolved);_mapTypes.swap(maps);_nodes.swap(nodes);
_positions.clear();_edgeAccess.clear();_loaded=true;++_revision;
TC_LOG_INFO("server.adventurebot", "AREA_CATALOG revision=%u areas=%u nodes=%u taxiResolved=%u taxiClosedUnknown=%u",_revision,uint32(_areas.size()),uint32(_nodes.size()),taxiKnown,taxiUnknown);
return true;
}
AdventureAreaPolicy::Result AdventureAreaMgr::Area(uint32 id) const
{
auto it=_resolved.find(id);return it==_resolved.end()?AdventureAreaPolicy::Result{}:it->second;
}
bool AdventureAreaMgr::AllowsArea(Player const* player,uint32 id) const
{
if(!Applies(player)) return true;
auto a=_areas.find(id);if(a==_areas.end()) return false;
auto map=_mapTypes.find(a->second.Map);
if(map==_mapTypes.end() || map->second==255) return false;
return Area(id).Allows(player->GetTeamId()==TEAM_ALLIANCE?1:2,player->getLevel(),map->second==0);
}
uint32 AdventureAreaMgr::AreaAt(Player const* player,uint32 map,Position const& pos)
{
// Exact coordinates and native terrain swaps; clear on catalog reload.
// Geography is shared across bots with the same native terrain swaps. A
// per-character key would repeat all catalog probes for every login.
float xyz[]{pos.GetPositionX(),pos.GetPositionY(),pos.GetPositionZ()};
std::string key(sizeof(map)+sizeof(xyz),'\0');
std::memcpy(key.data(),&map,sizeof(map));
std::memcpy(key.data()+sizeof(map),xyz,sizeof(xyz));
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
auto it=_positions.find(key);if(it!=_positions.end())return it->second;
uint32 zone=0,area=0;
// LoadTerrain stores weak references. Retain the terrain while checking
// remote locations instead of repeatedly loading/unloading whole maps.
auto inserted=_terrains.emplace(map,nullptr);
if(inserted.second)inserted.first->second=sTerrainMgr.LoadTerrain(map);
if(auto const& terrain=inserted.first->second)terrain->GetZoneAndAreaId(player->GetPhaseShift(),map,zone,area,pos);
if(!area)area=zone;
if(_positions.size()>=100000)
for(unsigned i=0;i<2048 && !_positions.empty();++i)_positions.erase(_positions.begin());
_positions.emplace(std::move(key),area);return area;
}
bool AdventureAreaMgr::AllowsPosition(Player const* player,uint32 map,Position const& pos)
{
if(!Applies(player))return true;
if(!std::isfinite(pos.GetPositionX()) || !std::isfinite(pos.GetPositionY()) || !std::isfinite(pos.GetPositionZ()))return false;
return EnsureLoaded() && AllowsArea(player,AreaAt(player,map,pos));
}
bool AdventureAreaMgr::PreferredFlight(Player const* player,uint32 map,Position const& pos)
{
return Applies(player) && EnsureLoaded() && Area(AreaAt(player,map,pos)).PreferFlight;
}
bool AdventureAreaMgr::AllowsNode(Player const* player,ClientlessTravelNode const& node)
{
if(!Applies(player))return true;
if(!EnsureLoaded())return false;
auto mask=_nodes.find(node.Key);
if(mask==_nodes.end() || !(mask->second & (player->GetTeamId()==TEAM_ALLIANCE?1:2)))return false;
Position pos;pos.Relocate(node.X,node.Y,node.Z);
return AllowsPosition(player,node.MapId,pos);
}
bool AdventureAreaMgr::AllowsSegment(Player const* player,uint32 map,Position const& from,Position const& to)
{
if(!Applies(player))return true;
if(!EnsureLoaded())return false;
float distance=from.GetExactDist(&to);
if(!std::isfinite(distance) || distance>200000)return false;
unsigned count=std::max(1u,unsigned(std::ceil(distance/40.0f)));
if(count>5000)return false;
float xyz[]{from.GetPositionX(),from.GetPositionY(),from.GetPositionZ(),to.GetPositionX(),to.GetPositionY(),to.GetPositionZ()};
std::string key(sizeof(map)+sizeof(xyz),'\0');
std::memcpy(key.data(),&map,sizeof(map));std::memcpy(key.data()+sizeof(map),xyz,sizeof(xyz));
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
if(_segments.size()>=131072 && !_segments.count(key))
for(unsigned i=0;i<2048 && !_segments.empty();++i)_segments.erase(_segments.begin());
auto& probe=_segments[key];
// A prior geometry probe may have stopped at the first area forbidden for
// another bot. Check the recorded areas, then lazily finish only as needed.
for(uint32 area:probe.Areas)if(!AllowsArea(player,area))return false;
while(probe.NextSample<=count)
{
unsigned i=probe.NextSample++;
float t=float(i)/count;Position p;
p.Relocate(from.GetPositionX()+(to.GetPositionX()-from.GetPositionX())*t,
from.GetPositionY()+(to.GetPositionY()-from.GetPositionY())*t,
from.GetPositionZ()+(to.GetPositionZ()-from.GetPositionZ())*t);
uint32 area=AreaAt(player,map,p);
if(std::find(probe.Areas.begin(),probe.Areas.end(),area)==probe.Areas.end())probe.Areas.push_back(area);
if(!AllowsArea(player,area))return false;
}
return true;
}
bool AdventureAreaMgr::AllowsEdge(Player const* player,ClientlessTravelEdge const& edge)
{
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;
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;
bool allowed=true;
if(edge.Type=="taxi")
{
uint32 path=0,cost=0;
// Both endpoints have already passed native flightmaster permissions.
if(from->Key.compare(0,5,"taxi_") || to->Key.compare(0,5,"taxi_"))return false;
char *sourceEnd=nullptr,*targetEnd=nullptr;
auto source=std::strtoul(from->Key.c_str()+5,&sourceEnd,10),target=std::strtoul(to->Key.c_str()+5,&targetEnd,10);
if(!source || !target || *sourceEnd || *targetEnd || source>UINT32_MAX || target>UINT32_MAX)return false;
sObjectMgr->GetTaxiPath(uint32(source),uint32(target),path,cost);
if(!path || path>=sTaxiPathNodesByPath.size())return false;
auto const& points=sTaxiPathNodesByPath[path];
for(std::size_t i=0;i<points.size() && allowed;++i)
{
auto p=points[i];if(!p){allowed=false;break;}
Position next;next.Relocate(p->Loc.X,p->Loc.Y,p->Loc.Z);
allowed=AllowsPosition(player,p->ContinentID,next);
if(i && points[i-1] && points[i-1]->ContinentID==p->ContinentID)
{
auto previous=points[i-1];Position start;start.Relocate(previous->Loc.X,previous->Loc.Y,previous->Loc.Z);
allowed=allowed && AllowsSegment(player,p->ContinentID,start,next);
}
}
}
// Native transports have no land path across the sea. Their two dock nodes
// and explicit node restrictions decide access; portal destinations likewise.
if(edge.Type=="ground")
{
Position previous;previous.Relocate(from->X,from->Y,from->Z);
if(auto points=sClientlessTravelGraphMgr->GetEdgePoints(edge.DatabaseId))
for(std::size_t i=0;i<points->size() && allowed;++i)
{
auto const& p=(*points)[edge.Reverse?points->size()-1-i:i];Position next;next.Relocate(p.X,p.Y,p.Z);
allowed=p.MapId==from->MapId && AllowsSegment(player,p.MapId,previous,next);previous=next;
}
Position end;end.Relocate(to->X,to->Y,to->Z);
allowed=allowed && from->MapId==to->MapId && AllowsSegment(player,to->MapId,previous,end);
}
if(_edgeAccess.size()>=100000)_edgeAccess.clear();
_edgeAccess.emplace(std::move(key),allowed);return allowed;
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef VCORE_ADVENTURE_AREA_MGR_H
#define VCORE_ADVENTURE_AREA_MGR_H
#include "Common.h"
#include "AdventureAreaPolicy.h"
#include "Position.h"
#include <string>
#include <memory>
#include <unordered_map>
#include <vector>
class Player;
class TerrainInfo;
struct ClientlessTravelNode;
struct ClientlessTravelEdge;
class TC_GAME_API AdventureAreaMgr
{
public:
static AdventureAreaMgr* instance();
bool Reload();
bool EnsureLoaded();
bool Applies(Player const* player) const;
bool AllowsArea(Player const* player, uint32 area) const;
bool AllowsPosition(Player const* player, uint32 map, Position const& position);
bool AllowsNode(Player const* player, ClientlessTravelNode const& node);
bool AllowsEdge(Player const* player, ClientlessTravelEdge const& edge);
bool AllowsSegment(Player const* player, uint32 map, Position const& from, Position const& to);
bool PreferredFlight(Player const* player, uint32 map, Position const& position);
AdventureAreaPolicy::Result Area(uint32 id) const;
uint32 AreaAt(Player const* player, uint32 map, Position const& position);
uint32 Revision() const { return _revision; }
int NodeFactions(std::string const& key) const
{ auto it=_nodes.find(key);return it==_nodes.end()?-1:it->second; }
private:
bool _loaded = false;
uint32 _revision = 0;
std::unordered_map<unsigned, AdventureAreaPolicy::Area> _areas;
std::unordered_map<unsigned, AdventureAreaPolicy::Result> _resolved;
std::unordered_map<uint32, uint8> _mapTypes;
std::unordered_map<std::string, uint8> _nodes;
std::unordered_map<std::string, uint32> _positions;
std::unordered_map<uint32, std::shared_ptr<TerrainInfo>> _terrains;
std::unordered_map<std::string, bool> _edgeAccess;
struct SegmentProbe
{
unsigned NextSample = 0;
std::vector<uint32> Areas;
};
// Static geography shared across factions/levels; permissions remain live.
std::unordered_map<std::string, SegmentProbe> _segments;
};
#define sAdventureAreaMgr AdventureAreaMgr::instance()
#endif
@@ -0,0 +1,51 @@
#ifndef VCORE_ADVENTURE_AREA_POLICY_H
#define VCORE_ADVENTURE_AREA_POLICY_H
#include <unordered_map>
#include <unordered_set>
namespace AdventureAreaPolicy
{
struct Area
{
unsigned Map = 0, Parent = 0;
int Factions = -1, MinLevel = -1, MaxLevel = -1, LevelGap = -1, Flight = -1;
};
struct Result
{
bool Valid = false;
unsigned Factions = 3, MinLevel = 0, MaxLevel = 0, LevelGap = 5;
bool KnownLevel = false, PreferFlight = false;
bool Allows(unsigned faction, unsigned level, bool outdoor) const
{
return Valid && (Factions & faction) && (!outdoor || KnownLevel || level >= 85) &&
level + LevelGap >= MinLevel;
}
};
inline Result Resolve(std::unordered_map<unsigned, Area> const& areas, unsigned id)
{
Result result;
std::unordered_set<unsigned> seen;
bool flightSet = false, gapSet = false, bandSet = false;
while (id)
{
auto it = areas.find(id);
if (it == areas.end() || !seen.insert(id).second) return {};
auto const& area = it->second;
if (area.Factions >= 0) result.Factions &= unsigned(area.Factions);
if (!gapSet && area.LevelGap >= 0) { result.LevelGap = unsigned(area.LevelGap); gapSet = true; }
if (!flightSet && area.Flight >= 0) { result.PreferFlight = area.Flight != 0; flightSet = true; }
if (area.MinLevel >= 0)
{
result.KnownLevel = true;
// Child bands can tighten, never weaken, an ancestor's minimum.
if (unsigned(area.MinLevel) > result.MinLevel) result.MinLevel = unsigned(area.MinLevel);
if (!bandSet) { result.MaxLevel = area.MaxLevel < 0 ? 0 : unsigned(area.MaxLevel); bandSet = true; }
}
id = area.Parent;
}
result.Valid = !seen.empty();
return result;
}
}
#endif
+52 -2
View File
@@ -1,3 +1,5 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureEncounterMgr.h"
@@ -245,8 +247,12 @@ bool AdventureBehaviorMgr::Allowed(Player * player, AdventureLocation const & lo
auto map = sMapStore.LookupEntry(location.Point.MapId);
unsigned side=player->GetTeamId()==TEAM_ALLIANCE?1:2;
unsigned capital=AdventureGroupPolicy::CapitalFaction(location.ZoneId?location.ZoneId:location.AreaId);
Position position; position.Relocate(location.Point.X,location.Point.Y,location.Point.Z);
return map && map->MapType==MAP_COMMON && (!capital || capital==side) &&
sAdventureLocationMgr->Weight(location,intent)>0 && (!location.Faction || location.Faction==side);
sAdventureLocationMgr->Weight(location,intent)>0 && (!location.Faction || location.Faction==side) &&
(!location.AreaId || sAdventureAreaMgr->AllowsArea(player,location.AreaId)) &&
(!location.ZoneId || sAdventureAreaMgr->AllowsArea(player,location.ZoneId)) &&
sAdventureAreaMgr->AllowsPosition(player,location.Point.MapId,position);
}
std::string AdventureBehaviorMgr::PickLocation(Player * player, Record & record, std::string const & intent)
@@ -304,6 +310,21 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
while (rows->NextRow());
if (loaded == 24)
{
// Preserve existing intent/hour blocks, repair only destinations made
// illegal by catalog edits, and persist the replacement immediately.
auto tx=WorldDatabase.BeginTransaction();
unsigned repaired=0;
for(unsigned h=0;h<24;++h)
{
auto& slot=record.Plan[h];auto location=sAdventureLocationMgr->Find(slot.Location);
if(location && Allowed(player,*location,slot.Intent)) continue;
slot.Location=PickLocation(player,record,slot.Intent);auto key=slot.Location;
WorldDatabase.EscapeString(key);
tx->PAppend("UPDATE vcore_adventure_day SET location_key='%s' WHERE character_guid=%u AND local_date=%u AND hour_slot=%u",key.c_str(),guid,date,h);
++repaired;
}
if(repaired)WorldDatabase.CommitTransaction(tx);
record.AreaRevision=sAdventureAreaMgr->Revision();
WorldDatabase.PExecute("DELETE FROM vcore_adventure_day WHERE character_guid=%u AND local_date<>%u",guid,date);
return;
}
@@ -337,6 +358,7 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
tx->PAppend("REPLACE INTO vcore_adventure_day (character_guid,local_date,hour_slot,intent,location_key) VALUES (%u,%u,%u,'%s','%s')", guid, date, h, record.Plan[h].Intent.c_str(), key.c_str());
}
WorldDatabase.CommitTransaction(tx);
record.AreaRevision=sAdventureAreaMgr->Revision();
TC_LOG_INFO("server.adventurebot", "DAY_PLAN guid=%u date=%u slots=24", guid, date);
}
@@ -1258,6 +1280,13 @@ 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;
if(r.AreaRevision!=sAdventureAreaMgr->Revision())
{
if(r.Schedule) {Generate(player,r,DateOf(Local(GameTime::GetGameTime())));r.Hour=-1;}
r.AreaRevision=sAdventureAreaMgr->Revision();
auto current=sAdventureLocationMgr->Find(r.Location);
if(current && !Allowed(player,*current,r.Intent))r.Dirty=true;
}
if(sAdventureEncounterMgr->Active(player))
{
if(r.Instance.Active)FinishInstance(player,r);
@@ -1454,7 +1483,8 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
if (op == "repairimport") return sAdventureLocationMgr->Import(handler,true);
if (op == "reload")
{
bool ok = sAdventureLocationMgr->Reload();
bool ok = sAdventureAreaMgr->Reload();
if (ok) ok = sAdventureLocationMgr->Reload() && sClientlessTravelGraphMgr->Reload();
handler->SendSysMessage(ok ? "Adventure catalogs reloaded." : "Adventure catalog reload failed; check migration.");
return ok;
}
@@ -1466,6 +1496,26 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
handler->SendSysMessage("Use an online clientless character GUID.");
return false;
}
if(op=="area")
{
if(!sAdventureAreaMgr->EnsureLoaded())return false;
std::string key;in>>key;
Position position=*player;uint32 map=player->GetMapId();
bool allowed=true;
if(!key.empty())
{
auto node=sClientlessTravelGraphMgr->FindNode(key);
if(!node){handler->SendSysMessage("Unknown travel node.");return false;}
position.Relocate(node->X,node->Y,node->Z);map=node->MapId;
allowed=sAdventureAreaMgr->AllowsNode(player,*node);
}
uint32 area=sAdventureAreaMgr->AreaAt(player,map,position);auto policy=sAdventureAreaMgr->Area(area);
allowed=allowed && sAdventureAreaMgr->AllowsPosition(player,map,position);
handler->PSendSysMessage("Area access GUID %u: revision=%u map=%u area=%u factionMask=%u minLevel=%u maxLevel=%u gap=%u knownLevel=%u preferFlight=%u nodeMask=%d allowed=%u",guid,
sAdventureAreaMgr->Revision(),map,area,policy.Factions,policy.MinLevel,policy.MaxLevel,policy.LevelGap,
policy.KnownLevel?1:0,policy.PreferFlight?1:0,key.empty()?-1:sAdventureAreaMgr->NodeFactions(key),allowed?1:0);
return true;
}
auto i = _records.find(guid);
if (i == _records.end())
{
@@ -59,6 +59,7 @@ private:
std::string PersonalIntent,PersonalLocation;
time_t PersonalDeadline=0;
time_t Deadline=0;
uint32 AreaRevision=0;
uint32 Date=0, Timer=0, MicroTimer=0, GestureTimer=0, RetryTimer=0;
int Hour=-1;
AdventureBehaviorPolicy::Day Plan;
@@ -1,4 +1,5 @@
#include "AdventureGroupTravelMgr.h"
#include "AdventureAreaMgr.h"
#include "AdventureGroupTravelPolicy.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTravelGraphMgr.h"
@@ -264,6 +265,9 @@ bool AdventureGroupTravelMgr::Route(Player* player,Record& record,std::string co
bool AdventureGroupTravelMgr::JourneyFollow(Player* player,Record& record,Leader const& leader)
{
auto now=GameTime::GetGameTime();if(leader.Kind==Journey::None || now-leader.Began>=1800)return false;
// Goal is the native fixed portal destination recorded by SampleLeader,
// not the player's later position. Following must not bypass area rules.
if(leader.Kind==Journey::Portal && !sAdventureAreaMgr->AllowsPosition(player,leader.GoalMap,leader.Goal))return false;
if(leader.Kind==Journey::Taxi)return Route(player,record,leader.To);
if(leader.Kind==Journey::Transport)
{
@@ -1,3 +1,4 @@
#include "AdventureAreaMgr.h"
#include "AdventureLocationMgr.h"
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.h"
@@ -23,6 +24,7 @@ bool AdventureLocationMgr::EnsureLoaded()
bool AdventureLocationMgr::Reload()
{
if (!sAdventureAreaMgr->EnsureLoaded()) return false;
_locations.clear();
_types.clear();
_workstations.clear();
@@ -54,13 +56,11 @@ bool AdventureLocationMgr::Reload()
}
while (rows->NextRow());
}
if(WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_zone'"))
if(auto rows=WorldDatabase.Query("SELECT zone_id,min_level,max_level FROM vcore_adventure_zone WHERE enabled=1"))do
{
auto f=rows->Fetch();
for(auto& l:_locations)if(l.ZoneId==f[0].GetUInt32() || (!l.ZoneId && l.AreaId==f[0].GetUInt32()))
{l.MinLevel=f[1].GetUInt8();l.MaxLevel=f[2].GetUInt8();}
}while(rows->NextRow());
for (auto& l : _locations)
{
auto area=sAdventureAreaMgr->Area(l.AreaId?l.AreaId:l.ZoneId);
if(area.KnownLevel) { l.MinLevel=uint8(area.MinLevel); l.MaxLevel=uint8(area.MaxLevel); }
}
if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance'"))
if(auto rows=WorldDatabase.Query("SELECT i.location_key,a.target_map FROM vcore_adventure_instance i JOIN areatrigger_teleport a ON a.ID=i.entry_trigger WHERE i.enabled=1")) do
{
@@ -1,3 +1,4 @@
#include "AdventureAreaMgr.h"
#include "Group.h"
#include "ObjectAccessor.h"
#include "ClientlessActionExecutorMgr.h"
@@ -729,6 +730,12 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
if (anchorRadius <= 0.0f)
anchorRadius = 1000.0f;
Position catalogGoal;catalogGoal.Relocate(point->X,point->Y,point->Z);
if(!sAdventureAreaMgr->AllowsPosition(player,point->MapId,catalogGoal))
{
record.Status.State=ClientlessActionExecutionState::Failed;
record.Status.Detail="location denied by area catalog";record.Active=false;return false;
}
record.FinalMapId = point->MapId;
record.FinalX = point->X;
record.FinalY = point->Y;
@@ -957,6 +964,26 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
ClientlessTravelRouteStep const& step = record.Route[record.RouteStep];
auto const* accessNode=sClientlessTravelGraphMgr->FindNode(step.NodeKey);
Position accessPosition;accessPosition.Relocate(step.X,step.Y,step.Z);
bool access=accessNode?sAdventureAreaMgr->AllowsNode(player,*accessNode):
sAdventureAreaMgr->AllowsPosition(player,step.MapId,accessPosition);
if(access && record.RouteStep>0 && step.EdgeDatabaseId)
{
ClientlessTravelEdge edge;
edge.DatabaseId=step.EdgeDatabaseId;edge.FromNode=record.Route[record.RouteStep-1].NodeKey;
edge.ToNode=step.NodeKey;edge.Type=step.EdgeType;edge.Reverse=step.EdgeReverse;
// Virtual road entries have no persistent source node; their remaining
// physical path is checked independently before each movement launch.
if(sClientlessTravelGraphMgr->FindNode(edge.FromNode))access=sAdventureAreaMgr->AllowsEdge(player,edge);
}
if(!access)
{
record.Status.State=ClientlessActionExecutionState::Failed;
record.Status.Detail="route denied by area/node catalog";record.Active=false;
return false;
}
TC_LOG_INFO("server.adventurebot.navigation",
"EDGE_START guid=%u name='%s' action='%s' step=%u/%u node='%s' nodeType='%s' "
"edgeType='%s' edgeDbId=%u reverse=%u recovery=%u",
@@ -1798,6 +1825,13 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
recordedDistance += std::sqrt(dx * dx + dy * dy + dz * dz);
}
for(std::size_t i=1;i<path.size();++i)
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),path[i-1],path[i]))
{
record.Status.State=ClientlessActionExecutionState::Failed;
record.Status.Detail="recorded path denied by area catalog";record.Active=false;return false;
}
sClientlessLuaEngine->PrepareGroundMount(player, std::max(recordedDistance,anticipatedDistance), "recorded-ground-edge");
player->GetMotionMaster()->MoveSmoothPath(
@@ -1,3 +1,4 @@
#include "AdventureAreaMgr.h"
#include "ClientlessFlightMgr.h"
#include <cstring>
#include "ClientlessLuaEngine.h"
@@ -115,6 +116,7 @@ bool ClientlessFlightMgr::Enabled(Player const * player) const
bool ClientlessFlightMgr::Preferred(Player const * player, uint32 map, Position const & position) const
{
if (sAdventureAreaMgr->PreferredFlight(player,map,position)) return true;
if(_planningPlayer==player)return _planningPreferredZones.count(AreaZone(player,map,position).second)!=0;
std::istringstream list(sConfigMgr->GetStringDefault("Clientless.Flight.PreferredZones", "210 4197"));
uint32 zone = AreaZone(player, map, position).second, candidate;
@@ -143,6 +145,7 @@ float ClientlessFlightMgr::Estimate(Player const * player, uint32 map, Position
// distance threshold or zone preference. Start still validates the flight.
float minimum = recovery ? 0.0f : preferred ? 60.0f : std::max(100.0f, planning?_planningMinDistance:sConfigMgr->GetFloatDefault("Clientless.Flight.MinDistance", 600));
if (! std::isfinite(distance) || distance < minimum || (!recovery && distance > 20000)) return Infinity();
if (!sAdventureAreaMgr->AllowsSegment(player,map,from,to)) return Infinity();
return recovery ? distance : preferred ? 40 + distance * 0.4f : 180 + distance * 1.5f;
}
@@ -185,6 +188,8 @@ bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::strin
{return Begin(player,target,error,false);}
bool ClientlessFlightMgr::Follow(Player* player,Position const& target,std::string& error)
{
if (!player || !sAdventureAreaMgr->AllowsPosition(player,player->GetMapId(),target))
{error="group flight target denied by area catalog";return false;}
auto found=_records.find(player->GetGUID().GetCounter());
if(found!=_records.end() && (found->second.Status.State==ClientlessFlightState::Mounting || found->second.Status.State==ClientlessFlightState::Flying))
{
@@ -229,6 +234,8 @@ bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record)
auto point=Mix(current,goal,std::min(1.0f,length/std::max(0.1f,Distance(current,goal))));
if(turn!=0){float horizontal=Horizontal(current,point);point.X=current.X+std::cos(heading+turn)*horizontal;point.Y=current.Y+std::sin(heading+turn)*horizontal;}
point.Z+=climb;
Position candidatePos;candidatePos.Relocate(point.X,point.Y,point.Z);
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidatePos))continue;
if(!Corridor(current,point,goal,settings,sample,ray,true))continue;
float candidate=Distance(point,goal)+std::fabs(turn)*4+climb;
if(candidate<score){score=candidate;best=point;}
@@ -255,6 +262,8 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
error = "free flight unavailable in current player/location state";
return false;
}
if (!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,target))
{error="flight corridor denied by area catalog";return false;}
auto * vmaps = VMAP::VMapFactory::createOrGetVMapManager();
if (! vmaps->isLineOfSightCalcEnabled() || ! vmaps->isHeightCalcEnabled())
{
@@ -463,6 +472,9 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
Fail(player, record, "no safe bounded flight corridor; holding position mounted");
return false;
}
Position catalogNext;catalogNext.Relocate(next.Next.X,next.Next.Y,next.Next.Z);
if(!record.Returning && !sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,catalogNext))
{Fail(player,record,"flight segment denied by area catalog");return false;}
if (! record.Returning) record.Breadcrumbs.push_back(* player);
record.Segment.Relocate(next.Next.X, next.Next.Y, next.Next.Z, player->GetOrientation());
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
@@ -5,6 +5,7 @@
#include <cstdint>
#include <cstring>
#include <unordered_map>
#include <vector>
// World-thread only. Store successful STATIC ground geometry, never Player paths,
// transient failures, Detour objects or tile pointers. Clear on graph reload.
@@ -12,7 +13,7 @@ class ClientlessGraphProbeCache
{
public:
using Key=std::array<std::uint32_t,8>;
struct Value {float Length,Final,Direct;};
struct Value {float Length,Final,Direct;std::vector<std::array<float,3>> Points;};
static Key MakeKey(std::uint32_t map,std::uint32_t instance,float ax,float ay,float az,float bx,float by,float bz)
{
Key key{map,instance};float coordinates[]{ax,ay,az,bx,by,bz};
@@ -25,11 +26,11 @@ public:
if(found==_values.end()){++Misses;return nullptr;}
++Hits;return &found->second;
}
void Record(Key const& key,bool complete,float length,float final,float direct)
void Record(Key const& key,bool complete,float length,float final,float direct, std::vector<std::array<float,3>> points = {})
{
if(!complete || !std::isfinite(length) || length<0 || !std::isfinite(final) || final>1.5f || final<0 || !std::isfinite(direct))return;
if(_values.size()>=32768)_values.clear();
_values.insert_or_assign(key,Value{length,final,direct});
_values.insert_or_assign(key,Value{length,final,direct,std::move(points)});
}
void Clear(){_values.clear();Hits=Misses=0;}
std::size_t Size()const{return _values.size();}
@@ -1,3 +1,5 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "Chat.h"
#include "ClientlessPlayerMgr.h"
@@ -388,6 +390,9 @@ bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std:
{
if (!player || !player->IsInWorld() || player->GetTransport() || !HasConnection(from, to))
{ error = "Transport stops are not recorded/compatible, or player is already aboard."; return false; }
auto a=sClientlessTravelGraphMgr->FindNode(from),b=sClientlessTravelGraphMgr->FindNode(to);
if(sAdventureAreaMgr->Applies(player) && (!a || !b || !sAdventureAreaMgr->AllowsNode(player,*a) || !sAdventureAreaMgr->AllowsNode(player,*b)))
{error="transport denied by area/node catalog";return false;}
auto const& source = _stops.at(from);
if (source.Map != player->GetMapId() || Distance(*player, source.Points.front().Pos) > 3.0f)
{ error = "Player has not reached the shared departure node."; return false; }
@@ -1,3 +1,4 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
@@ -286,9 +287,9 @@ std::string ClientlessTravelGraphMgr::FindNearestStartNode(
for (auto const& pair : _nodes)
{
ClientlessTravelNode const& node = pair.second;
if (node.MapId != player->GetMapId())
continue;
if (!sAdventureAreaMgr->AllowsNode(player,node)) continue;
float const dx = node.X - player->GetPositionX();
float const dy = node.Y - player->GetPositionY();
@@ -330,6 +331,9 @@ bool ClientlessTravelGraphMgr::PlanRoute(
return false;
}
if (!sAdventureAreaMgr->AllowsNode(player,_nodes.at(destinationNode)))
{ error="destination node denied by area/node catalog"; return false; }
if (startRadius <= 0.0f)
startRadius = 500.0f;
@@ -386,6 +390,7 @@ bool ClientlessTravelGraphMgr::PlanRoute(
for (ClientlessTravelEdge const& edge : edgeItr->second)
{
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
if (blockedEdgeIds &&
edge.DatabaseId &&
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
@@ -573,6 +578,14 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
return false;
}
if (!sAdventureAreaMgr->AllowsNode(player,destinationItr->second))
{ error="destination node denied by area/node catalog"; return false; }
if (locationGoal)
{
Position goal;goal.Relocate(locationGoal->X,locationGoal->Y,locationGoal->Z);
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
{ error="destination location denied by area catalog"; return false; }
}
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z)
{
@@ -677,6 +690,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
ClientlessTravelNode const& node = pair.second;
if (node.MapId != player->GetMapId())
continue;
if (!sAdventureAreaMgr->AllowsNode(player,node)) continue;
float const dx = node.X - player->GetPositionX();
float const dy = node.Y - player->GetPositionY();
@@ -744,6 +758,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
ClientlessTravelNode const& node = pair.second;
if (node.MapId != player->GetMapId())
continue;
if (!sAdventureAreaMgr->AllowsNode(player,node)) continue;
float dx = node.X - player->GetPositionX();
float dy = node.Y - player->GetPositionY();
float dz = node.Z - player->GetPositionZ();
@@ -774,6 +789,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (edge.Type != "ground" || !edge.DatabaseId ||
(edge.FromNode != near.second && edge.ToNode != near.second))
continue;
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
if (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId))
continue;
std::string key = std::to_string(edge.DatabaseId) + (edge.Reverse ? "R" : "F");
@@ -887,9 +903,21 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
// from Arthoran's current position. No Player/WorldObject is required.
if (targetMap != from.MapId)
return VcorePositionPathResult{};
auto allowedPath=[&](std::vector<std::array<float,3>> const& points)
{
if(!sAdventureAreaMgr->Applies(player))return true;
if(points.empty())return false;
for(std::size_t i=1;i<points.size();++i)
{
Position p,q;p.Relocate(points[i-1][0],points[i-1][1],points[i-1][2]);q.Relocate(points[i][0],points[i][1],points[i][2]);
if(!sAdventureAreaMgr->AllowsSegment(player,from.MapId,p,q))return false;
}
return true;
};
auto key=ClientlessGraphProbeCache::MakeKey(from.MapId,player->GetInstanceId(),from.X,from.Y,from.Z,x,y,z);
if(auto cached=_positionProbes.Find(key))
{
if(!allowedPath(cached->Points))return VcorePositionPathResult{};
VcorePositionPathResult result;result.Result=VcorePositionPathResult::Status::Complete;
result.PathLength=cached->Length;result.FinalDistance=cached->Final;result.DirectDistance=cached->Direct;
return result;
@@ -897,7 +925,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
auto started=std::chrono::steady_clock::now();++trace.Calls;
auto result=PathGenerator::AnalyzePositions(from.MapId, from.MapId,player->GetInstanceId(),a,b,NAV_GROUND);
trace.NavUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-started).count();
_positionProbes.Record(key,result.IsComplete(),result.PathLength,result.FinalDistance,result.DirectDistance);
std::vector<std::array<float,3>> points;
for(auto const& p:result.Points)points.push_back({p.x,p.y,p.z});
_positionProbes.Record(key,result.IsComplete(),result.PathLength,result.FinalDistance,result.DirectDistance,points);
if(!allowedPath(points))return VcorePositionPathResult{};
return result;
};
@@ -937,6 +968,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (edge.Type != "ground" || !edge.DatabaseId ||
(blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)))
continue;
if (_nodes.at(edge.FromNode).MapId!=goalMap || _nodes.at(edge.ToNode).MapId!=goalMap) continue;
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
std::string const identity = std::to_string(edge.DatabaseId) +
(edge.Reverse ? "R" : "F");
if (!scannedDirected.insert(identity).second)
@@ -1177,6 +1210,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
{
for (ClientlessTravelEdge const& edge : edgeItr->second)
{
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
if (portalArrival && edge.Type != "portal")
continue;
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
@@ -1211,12 +1245,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
{
auto const& target=pair.second;
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
State next{target.Key,false};std::string key=stateKey(next);
auto old=distance.find(key);
float dx=target.X-origin.X,dy=target.Y-origin.Y,dz=target.Z-origin.Z;
// Even a preferred legal flight costs at least this much.
// Skip expensive terrain sampling when it cannot improve the
// route already found; this preserves the full graph search.
float lowerBound=40.0f+std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
if (!sAdventureAreaMgr->AllowsNode(player,target)) continue;
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
if (!std::isfinite(cost)) continue;
State next{target.Key,false};std::string key=stateKey(next);
float score=current.Cost+cost;
auto old=distance.find(key);if(old!=distance.end()&&old->second<=score)continue;
if(old!=distance.end()&&old->second<=score)continue;
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[currentKey]+1;
previous[key]={currentKey,current.Value.Node,"free_flight",0,false};
open.push({score,next});
@@ -1263,6 +1305,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
if (edge.Type != "ground" || !edge.DatabaseId ||
(blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)))
continue;
if(_nodes.at(edge.FromNode).MapId!=from.MapId || _nodes.at(edge.ToNode).MapId!=from.MapId)continue;
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
auto pts = _edgePoints.find(edge.DatabaseId);
if (pts == _edgePoints.end() || pts->second.size() < 3)
continue;
@@ -1380,6 +1424,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
for (auto const& key : neighborhood->second)
{
ClientlessTravelNode const& to = _nodes.at(key);
if (!sAdventureAreaMgr->AllowsNode(player,to)) continue;
if (to.Key == from.Key || to.MapId != from.MapId)
continue;
@@ -1819,6 +1864,10 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
return false;
}
Position goal;goal.Relocate(destinationX,destinationY,destinationZ);
if (!sAdventureAreaMgr->AllowsPosition(player,destinationMapId,goal))
{ error="destination location denied by area catalog"; return false; }
if (startRadius <= 0.0f)
startRadius = 500.0f;
@@ -1861,6 +1910,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
ClientlessTravelNode const& node = pair.second;
if (node.MapId != player->GetMapId())
continue;
if (!sAdventureAreaMgr->AllowsNode(player,node)) continue;
float const dx = node.X - player->GetPositionX();
float const dy = node.Y - player->GetPositionY();
@@ -1895,6 +1945,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
for (ClientlessTravelEdge const& edge : edgeItr->second)
{
if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue;
if (blockedEdgeIds &&
edge.DatabaseId &&
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
@@ -1925,6 +1976,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
for (auto const& pair : _nodes)
{
ClientlessTravelNode const& node = pair.second;
if (!sAdventureAreaMgr->AllowsNode(player,node)) continue;
if (node.MapId != destinationMapId)
continue;
@@ -1,3 +1,4 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessLuaEngine.h"
@@ -108,6 +109,13 @@ bool ClientlessTravelMgr::AnalyzePath(
auto const& points = path.GetPath();
diagnostics.PointCount = uint32(points.size());
for(std::size_t i=1;i<points.size();++i)
{
Position a,b;a.Relocate(points[i-1].x,points[i-1].y,points[i-1].z);b.Relocate(points[i].x,points[i].y,points[i].z);
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),a,b))
{diagnostics.Calculated=false;diagnostics.Reason="area-catalog-denied";return false;}
}
if (!points.empty())
{
@@ -284,6 +292,12 @@ bool ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record)
}
auto const& points = path.GetPath();
for(std::size_t i=1;i<points.size();++i)
{
Position a,b;a.Relocate(points[i-1].x,points[i-1].y,points[i-1].z);b.Relocate(points[i].x,points[i].y,points[i].z);
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),a,b))
{record.Status.State=ClientlessTravelState::PathRejected;return false;}
}
uint32 const executionType = uint32(path.GetPathType());
// VCORE_C814A_NAVIGATION_FIX: confirm the execution path independently.
float executionLength = 0.0f;
@@ -0,0 +1,52 @@
"""Exercise production area inheritance and access rules in compiled C++."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[2]
harness = r'''
#include "AdventureAreaPolicy.h"
#include <cassert>
#include <iostream>
using namespace AdventureAreaPolicy;
int main()
{
std::unordered_map<unsigned,Area> areas;
areas[1]={0,0,2,1,10,-1,-1}; // Horde starter region
areas[2]={0,1,3,-1,-1,-1,-1}; // neutral subarea cannot override parent
auto child=Resolve(areas,2);
assert(!child.Allows(1,85,true));
assert(child.Allows(2,1,true));
areas[3]={0,0,0}; // Vashjir
areas[4]={0,3,3,80,82};
assert(!Resolve(areas,4).Allows(1,85,true));
assert(!Resolve(areas,4).Allows(2,85,true));
areas[5]={571,0,-1,68,72,5,1};
auto north=Resolve(areas,5);
assert(!north.Allows(1,62,true));
assert(north.Allows(1,63,true));
assert(north.Allows(2,85,true)); // max band is not an upper access cap
assert(north.PreferFlight);
areas[6]={571,5,-1,1,10,-1,0}; // cannot weaken parent level minimum
assert(!Resolve(areas,6).Allows(1,20,true));
assert(!Resolve(areas,6).PreferFlight);
areas[7]={0,0}; // unknown outdoor band is conservative
assert(!Resolve(areas,7).Allows(1,20,true));
assert(Resolve(areas,7).Allows(1,85,true));
assert(Resolve(areas,7).Allows(1,20,false)); // native instance access is separate
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
// A reloaded parent restriction changes the exact same child without edits.
areas[1].Factions=0;
assert(!Resolve(areas,2).Allows(2,85,true));
std::cout << "Area inheritance, faction intersection, level limits, flight overrides and invalid data passed.\n";
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-area-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([str(binary)], check=True)
@@ -0,0 +1,68 @@
"""Run the production segment probe with terrain and player doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[2]
source = (root / 'src/server/game/World/AdventureAreaMgr.cpp').read_text()
start = source.index('bool AdventureAreaMgr::AllowsSegment(')
end = source.index('bool AdventureAreaMgr::AllowsEdge(', start)
code = r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
using uint32=unsigned;
struct Position {float x=0,y=0,z=0;
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}
float GetExactDist(Position const* p)const{return std::hypot(x-p->x,y-p->y,z-p->z);}
void Relocate(float a,float b,float c){x=a;y=b;z=c;}
};
struct Phase{std::map<unsigned,int> swaps;auto const& GetVisibleMapIds()const{return swaps;}};
struct Player{bool bot=true,denied=false;Phase phase;auto const& GetPhaseShift()const{return phase;}};
class AdventureAreaMgr {
public:
struct SegmentProbe {unsigned NextSample=0;std::vector<uint32> Areas;};
std::unordered_map<std::string,SegmentProbe> _segments;
unsigned probes=0;bool closed=false;
bool Applies(Player const* p){return p&&p->bot;}
bool EnsureLoaded(){return true;}
bool AllowsArea(Player const* p,unsigned area){return area!=2 || (!closed&&!p->denied);}
unsigned AreaAt(Player const*,unsigned,Position const& p){++probes;return p.x>=40&&p.x<=80?2:1;}
bool AllowsSegment(Player const*,unsigned,Position const&,Position const&);
};
'''+source[start:end]+r'''
int main(){
AdventureAreaMgr mgr;Player p;Position a,b;b.x=120;
assert(mgr.AllowsSegment(&p,0,a,b));unsigned first=mgr.probes;
assert(first==4);
assert(mgr.AllowsSegment(&p,0,a,b)&&mgr.probes==first);
// Same static geography, different faction/level permission.
Player forbidden;forbidden.denied=true;
assert(!mgr.AllowsSegment(&forbidden,0,a,b)&&mgr.probes==first);
// Reload changes permissions without invalidating the geometry cache.
mgr.closed=true;assert(!mgr.AllowsSegment(&p,0,a,b)&&mgr.probes==first);
mgr.closed=false;assert(mgr.AllowsSegment(&p,0,a,b));
// A denied first probe stops early; another bot can finish the same probe.
mgr._segments.clear();mgr.probes=0;
assert(!mgr.AllowsSegment(&forbidden,0,a,b)&&mgr.probes==2);
assert(mgr.AllowsSegment(&p,0,a,b)&&mgr.probes==4);
// A new terrain swap and a different map cannot reuse the old probe.
p.phase.swaps.emplace(123,0);assert(mgr.AllowsSegment(&p,0,a,b)&&mgr.probes==8);
assert(mgr.AllowsSegment(&p,1,a,b)&&mgr.probes==12);
b.x=300000;assert(!mgr.AllowsSegment(&p,0,a,b));
p.bot=false;assert(mgr.AllowsSegment(&p,0,a,b));
std::cout<<"Segment geography cache, faction isolation, reload permissions, lazy completion and terrain swaps passed.\n";
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-area-segment-') as folder:
cpp = Path(folder) / 'test.cpp'
binary = Path(folder) / 'test'
cpp.write_text(code)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
@@ -19,7 +19,7 @@ struct Guid{unsigned GetCounter(){return 99;}};
struct Player{Guid GetGUID(){return {};}};
struct Field{unsigned n=0;std::string s;unsigned GetUInt8(){return n;}unsigned GetUInt32(){return 0;}std::string GetString(){return s;}};
struct Rows{std::vector<std::array<Field,3>> data;unsigned index=0;Field*Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
struct Tx{unsigned slots=0,deletes=0;void PAppend(char const*,unsigned){++deletes;}void PAppend(char const*,unsigned,unsigned,unsigned,char const*,char const*){++slots;}};
struct Tx{unsigned slots=0,deletes=0,repairs=0;void PAppend(char const*,char const*,unsigned,unsigned,unsigned){++repairs;}void PAppend(char const*,unsigned){++deletes;}void PAppend(char const*,unsigned,unsigned,unsigned,char const*,char const*){++slots;}};
struct DB{void EscapeString(std::string&){}
std::shared_ptr<Rows> rows;std::shared_ptr<Tx> saved;unsigned oldCleanup=0;
auto PQuery(char const*,unsigned,unsigned){if(rows)rows->index=0;return rows;}
@@ -28,8 +28,12 @@ struct DB{void EscapeString(std::string&){}
}WorldDatabase;
struct Profile{std::unordered_map<std::string,float>Personality,ActivityAffinity;}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct AdventureLocation{} location;
struct Locations{bool valid=true;auto Find(std::string const&){return valid?&location:nullptr;}} locations;auto sAdventureLocationMgr=&locations;
struct Areas{unsigned Revision(){return 1;}} areas;auto sAdventureAreaMgr=&areas;
struct AdventureBehaviorMgr{
struct Record{uint32 Date=0;Day Plan;std::mt19937 Random{9};};
struct Record{uint32 Date=0,AreaRevision=0;Day Plan;std::mt19937 Random{9};};
bool Allowed(Player*,AdventureLocation const&,std::string const&){return true;}
int picks=0;float Affinity(std::unordered_map<std::string,float>const&,std::string const&,float);
bool Known(std::string const& s){return s=="social" || s=="adventure" || s=="profession" || s=="rest" || s=="leisure" || s=="economy" || s=="instance" || s=="raid" || s=="groupformation";}
std::string PickLocation(Player*,Record&,std::string const& intent){++picks;return intent+"_"+std::to_string(picks);}
@@ -46,6 +50,12 @@ int main(){
for(unsigned h=0;h<24;++h)assert(r.Plan[h].Location=="saved_"+std::to_string(h));
// New login uses the same stored slots instead of rerolling the day.
AdventureBehaviorMgr::Record relog;mgr.Generate(&p,relog,20261029);assert(relog.Plan[23].Location==r.Plan[23].Location && mgr.picks==0);
// A catalog change repairs all illegal destinations and persists them,
// preserving each hour's intent instead of rerolling the entire day.
locations.valid=false;mgr.Generate(&p,relog,20261029);
assert(WorldDatabase.saved && WorldDatabase.saved->repairs==24 && relog.AreaRevision==1);
for(unsigned h=0;h<24;++h){assert(relog.Plan[h].Intent==(h%2?"social":"adventure"));assert(relog.Plan[h].Location!="saved_"+std::to_string(h));}
locations.valid=true;
// Missing/incomplete current day is completely replaced, deleting older dates.
rows->data.resize(4);mgr.Generate(&p,r,20261030);
assert(r.Date==20261030 && mgr.picks>0 && WorldDatabase.saved->slots==24 && WorldDatabase.saved->deletes==1);
@@ -74,6 +74,7 @@ enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}
bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight;
struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua;
struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
struct ChatHandler{void SendSysMessage(char const*){}template<class...T>void PSendSysMessage(char const*,T...){}};
'''+h+r'''
AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGroupTravelMgr m;return &m;}
@@ -111,6 +112,11 @@ int main(){
state.Kind=AdventureGroupTravelMgr::Journey::Portal;state.Generation=2;state.SourceMap=1;state.GoalMap=2;state.Goal={100,0,0};state.Began=++GameTime::now;mgr.Observe(&a,500);
state.Generation=3;state.SourceMap=2;state.GoalMap=3;state.Goal={200,0,0};mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==2 && mgr._records.at(26).Pending.front().GoalMap==2);
a.mapid=2;a.m_positionX=100;mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==1 && mgr._records.at(26).Pending.front().GoalMap==3);
// A fixed portal destination forbidden by the area catalog cannot be used,
// even when the follower is traveling with a player group leader.
access.allowed=false;
assert(!mgr.JourneyFollow(&a,mgr._records.at(26),mgr._records.at(26).Pending.front()));
access.allowed=true;
// A blocked/unrecognized trip cannot grant unlimited separation protection.
mgr._records.at(26).Pending.clear();state.Kind=AdventureGroupTravelMgr::Journey::None;state.Generation=4;leader.mapid=0;leader.m_positionX=1000;a.mapid=0;a.m_positionX=0;executor.status.State=ClientlessActionExecutionState::Completed;
for(unsigned i=0;i<1200;++i){if(i%2==0)++GameTime::now;mgr.Observe(&a,500);}assert(mgr.Separated(&a));leader.m_positionX=10;mgr.Observe(&a,500);assert(!mgr.Separated(&a));
@@ -48,6 +48,7 @@ struct Graph{
} graph;auto sClientlessTravelGraphMgr=&graph;
struct AdventureLocation{struct{uint32 MapId=0;float X=0,Y=0,Z=0;}Point;bool IsArea=false;uint32 AreaId=12;} location;
struct Locations{auto Find(std::string const&){return &location;}} locations;auto sAdventureLocationMgr=&locations;
struct Areas{unsigned Revision(){return 0;}} areas;auto sAdventureAreaMgr=&areas;
enum class ClientlessTravelState{Moving,Arrived};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Arrived;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
class AdventureBehaviorMgr {
@@ -57,7 +58,7 @@ public:
bool Enabled=true,Schedule=false,Queued=false,Dirty=false;
Phase State=Phase::Travel;
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation;
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
time_t Deadline=1000;uint32 AreaRevision=0,Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
struct{bool Active=false;}Social;
struct{bool Active=false;}Recovery;
struct{bool Active=false;}Instance;
@@ -66,6 +67,7 @@ public:
};
std::unordered_map<uint32,Record> _records{{26,{}}};
int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0;
bool Allowed(Player*,AdventureLocation const&,std::string const&){return true;}
bool AdoptGroup(Player*,Player*){return false;}
void Update(Player*,uint32);
void EnterPlayerInstance(Player*);
+10
View File
@@ -24,6 +24,11 @@ using namespace ClientlessFlightPath;
#define TC_LOG_WARN(...) ((void)0)
constexpr int SKILL_RIDING=762,MAP_COMMON=0,SPELL_AURA_MOUNT_RESTRICTIONS=1;
struct Position{float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}};
struct Player;
struct AreaAccess {bool allowed=true,preferred=false;
bool AllowsSegment(Player const*,uint32,Position const&,Position const&)const;
bool PreferredFlight(Player const*,uint32,Position const&)const;
};
Point PointOf(Position const&p){return {p.x,p.y,p.z};}float Infinity(){return std::numeric_limits<float>::infinity();}
struct Guid{uint32 GetCounter()const{return 26;}};
struct Player{
@@ -38,6 +43,9 @@ struct Player{
void StopMoving(){++stops;}void InterruptNonMeleeSpells(bool){++interrupts;}
float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 50;}
};
bool AreaAccess::AllowsSegment(Player const*,uint32,Position const&,Position const&)const{return allowed;}
bool AreaAccess::PreferredFlight(Player const*,uint32,Position const&)const{return preferred;}
AreaAccess access;auto sAdventureAreaMgr=&access;
struct Mount{uint32 DisplayId=32235,Weight=100;};
struct Profile{bool CanFreeFly=true;std::vector<Mount> FlightMounts{{}};} profile;
struct Lua{auto GetAdventureProfile(uint32){return &profile;}} lua;
@@ -73,6 +81,8 @@ harness+=r'''
int main(){
ClientlessFlightMgr mgr;Player p;Position a,b;b.x=1000;
float roadBias=mgr.Estimate(&p,0,a,b);assert(roadBias>1000&&std::isfinite(roadBias));
access.allowed=false;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));access.allowed=true;
access.preferred=true;assert(mgr.Estimate(&p,0,a,b)<roadBias);access.preferred=false;
p.license=false;assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));p.license=true;
p.cold=false;assert(!std::isfinite(mgr.Estimate(&p,571,a,b)));p.cold=true;
p.skill=150;assert(!mgr.Enabled(&p));p.skill=225;
+22 -2
View File
@@ -42,8 +42,16 @@ struct Flight {
}
} flight;
auto sClientlessFlightMgr=&flight;
struct VcorePositionPathResult {enum class Status{Complete,Partial};Status Result=Status::Partial;float FinalDistance=0,PathLength=0,DirectDistance=0;bool IsComplete()const{return Result==Status::Complete;}};
struct PathGenerator {static inline unsigned probes=0;static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){++probes;float d=dist(a,b);return {available && d<=maxDistance?VcorePositionPathResult::Status::Complete:VcorePositionPathResult::Status::Partial,0,d,d};}};
struct AreaAccess {
bool active=true;std::unordered_set<std::string> blocked;float forbiddenMin=1e9,forbiddenMax=1e9;
bool Applies(Player const*)const{return active;}
template<class Node>bool AllowsNode(Player const*,Node const& n)const{return !blocked.count(n.Key);}
template<class Edge>bool AllowsEdge(Player const*,Edge const& e)const{return !blocked.count(e.FromNode)&&!blocked.count(e.ToNode);}
bool AllowsPosition(Player const*,uint32,Position const& p)const{return !(p.x>=forbiddenMin&&p.x<=forbiddenMax);}
bool AllowsSegment(Player const*,uint32,Position const& a,Position const& b)const{return std::max(a.x,b.x)<forbiddenMin||std::min(a.x,b.x)>forbiddenMax;}
} access;auto sAdventureAreaMgr=&access;
struct VcorePositionPathResult {struct Point{float x,y,z;};enum class Status{Complete,Partial};Status Result=Status::Partial;float FinalDistance=0,PathLength=0,DirectDistance=0;std::vector<Point> Points;bool IsComplete()const{return Result==Status::Complete;}};
struct PathGenerator {static inline unsigned probes=0;static inline bool available=false; static inline float maxDistance=10000;static VcorePositionPathResult AnalyzePositions(uint32,uint32,uint32,Position a,Position b,uint32){++probes;float d=dist(a,b);return {available && d<=maxDistance?VcorePositionPathResult::Status::Complete:VcorePositionPathResult::Status::Partial,0,d,d,{{a.x,a.y,a.z},{b.x,b.y,b.z}}};}};
std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;}
'''
cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+'''
@@ -122,6 +130,18 @@ std::unordered_set<std::string> forbidden{"A->B"};
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,&forbidden)&&!done);
cached._positionProbes.Clear();cached._bridgeNeighborhoods.clear();PathGenerator::available=false;
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&!done);
// Catalog restrictions apply even to geometry cached for a previously legal route.
PathGenerator::available=true;cached._positionProbes.Clear();cached._bridgeNeighborhoods.clear();
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
calls=PathGenerator::probes;
access.forbiddenMin=60;access.forbiddenMax=90;
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&!done);
assert(PathGenerator::probes==calls);
access.forbiddenMin=access.forbiddenMax=1e9;
assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
access.blocked.insert("B");
assert(!cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
access.blocked.clear();
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
}
'''