Enforce reloadable AdventureBot area and travel access policies
This commit is contained in:
1 parent
0173a434f4
commit
321a28640c
31 files changed
+4869
-21
No files matched your search
@@ -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')
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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*);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
'''
|
||||
|
||||
Reference in new issue
Block a user