Fix AdventureBot group preparation, public portals and flight recovery
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

Add preparation outings and equipment checks with a five-minute return to the meeting point. Correct static portal visitor casting and level conditions, with native interaction diagnostics. Recover suspended flying mounts, walk to verified takeoff sites, and validate landing and return volumes. Include SQL migrations, targeted regression tests and live validation checkpoints.
This commit is contained in:
Nico committed 2026-10-29 18:21:43 +01:00
1 parent 2841ee973b
commit 54d77d1e4d
60 files changed
+1607 -33

No files matched your search

+276
View File
@@ -1,3 +1,279 @@
# Commit checkpoint: group preparation, public portals and flight recovery
User requested committing all current changes. This checkpoint combines the
preparation microactions and five-minute gathering rule, public portal SQL fixes
and diagnostics, and safe flight departure/landing/return recovery, including
targeted tests and native validation artifacts. Latest successful build and live
state are recorded below. Earlier "No commit" statements describe their original
work-in-progress checkpoints; consult git log for the resulting local commit.
The known gathering-point overlap and formation/mount-follow observations remain
separate follow-up work. No push or server restart is part of this commit task.
# Latest: flight departure/landing repair finished and live-verified
Final build+install /tmp/vcore-flight-safe-return-final-build.log and-install.log
succeeded. Current serverPID317961, tmuxvcore-worldserver undergdb; console
/tmp/vcore-flight-safe-return-final-start.log. Freshready+8085/8087 verified.
Eight targeted flight tests pass. Final native evidence: Brynli84 resumed native
mount, completed54.32yd/3segments, landed18:17:12 and free_flight edge to
portal_at_4455_target completed recovery0; next route step began normally.
Tholgar92 completed396.61yd/19segments/zeroavoidances landed18:17:44. Earlier
real MMAP8.18yd departure under overhang + >1km flight also recorded.
Safe nearby return-landing search0/8/16/24/40yd,80yd3D bound,fullColumn+Corridor+
area checks added after direct hover column proved blocked. Complete production
helper tests cover blocked column/safe neighbor/no candidate/catalog denial.
Ground approach abort does not interrupt unrelated combat spells. Ground/air
start, five-ray landing volume, short low final descent and native feet touchdown
fixes are active. No teleport or profile/DB/plan overrides in this flight task.
Docs flight-departure-repair.md and checkpoints/2026-10-29-flight-departure,
including final-live.log. Complete final intent journey/visual acceptance remains
unverified; intermediate flight legs are verified. No commit/push. Existing WIP
from prior tasks preserved. Separate known OutdoorPvP shutdown destructor SIGSEGV
still occurs during graceful shutdown; gdb captures preserved. Original runtime
vector assertion unresolved. No active finite flight observer remains.
# Latest: Outland flight departure investigation and repair
Diagnosed84Brynli: mounted nativeCanFly1 but suspended~10ydaboveground,
oldcodeinsistedgroundstart.92Tholgar: actualfootfloor35.852,skyquery48.203
(overheadlayer). Neithercasewater. Added nativeprobe feet/liquid/mountflags.
Implemented airborne profile-mount reuse (no dismount), checked MMAP ground
walk to nearby open takeoff if on a lower supporting floor, cancel/timeout
safety, shared-trail identity preservation, per-action flight_origin blacklist
that preserves ground alternatives. Verified live both resumed/tookoff.
Then discovered short final approach oscillation + skyfloor touchdown/hover
return anchor issue. Added collision-checked short low landing, feet-level
support/body check, five-ray volume for takeoff AND landing candidates and
verified ground return anchor beneath resumed hover. No teleport/height fudge.
Eight targeted flight tests PASS (production handler doubles + real planner).
Build/install latest /tmp/vcore-flight-departure-volume-build.log and-install.log.
TestserverPID315309, tmuxvcore-worldserver undergdb, latest console
/tmp/vcore-flight-departure-volume-start.log. Freshready+8085/8087 verified.
Native logins84/92 requested; final live observer /tmp/vcore-flight-departure-volume-live.log.
Docs flight-departure-repair.md, checkpoints/2026-10-29-flight-departure.
No profile/DB/plan overrides for this flight task. Prior uncommitted work
preserved. No commit/push. Separate known shutdown OutdoorPvP destructor
SIGSEGV repeatedly captured by gdb; original runtime vector assertion still
unresolved (not this task). Latest final live findings to append below.
# Latest: public portal repair completed
Migrations03/04 applied: visitor level58 condition and twelve static public
portal templates use playerCast1. Source only diagnostics: GameObject cast-fail
WARN + GM clientless portal object use guid spawn (native click, no bypass).
Ten available spawns transferred successfully including Shattrath/Hellfire,
Dalaran and Darkmoon returns; two Darkmoon entrances gated by inactive event3.
See public-portal-repair.md and checkpoints/2026-10-29-public-portals.
Three targeted tests pass; existing ACK harness updated for delayed guard.
Build/install succeeded; latest serverPID304210, gdb/tmuxvcore-worldserver,
console /tmp/vcore-public-portals-fixed-start.log, fresh ready+8085/8087 checked.
Temporary game_tele fixtures and specific corren/mahuwa profile files removed,
profiles restored via luareload and schedule on issued for83/102. Rootloglevel5
restored. Arthoran override original; no human character changed. No commit.
Separate shutdown OutdoorPvP destructor SIGSEGV stack captured; original runtime
vector<unsigned int> assertion unresolved. Earlier checkpoints below historic.
# Latest: crash investigation 16:19
Server aborted SIGABRT16:19:22; libstdc++ const vector<unsigned int>::operator[] bounds assertion. Immediately preceding VMAP530.vmtree missing warning, but file exists3.46MB; exact cause unconfirmed, no crash dump. Evidence preserved /tmp/vcore-crash-20261029-1619/. Recovered under gdb batch to capture all-thread backtrace on next failure, PID299009, dedicated tmux unchanged. Console /tmp/vcore-crash-watch-start-20261029.log; fresh ready in newly generated Server.log and8085/8087 listeners verified. No code/config changes. Unfinished manual transport recordings may need restart; no persistent recorded docks discarded.
# Latest: Blasted Lands portal activation
Native imports219963/211359/211298 active, edges1758-1760. Source ACL Alliance1/Horde2. Adventure reload issued. See blasted-lands-portals.md. Server unexpectedly already absent before first import (no shutdown by agent); recovered PID295252, fresh ready and listeners8085/8087 verified. Console /tmp/vcore-portal-enable-start-20261029.log. No binary/source changes. Gameplay traversal pending. Existing WIP preserved.
# Aktueller Auftrag: Fünf-Minuten-Sammelregel implementieren
Nutzer hat Regel inzwischen ausdrücklich beauftragt. Source ergänzt:
AdventureGroupPolicy::Gathering(groupformation,deadline,now): letzte300s,
exakt am Deadlineende wieder false. Record.Gathering/GatherRetryAt und
AdventureBehaviorMgr::UpdateGathering vor normalem OnSite/Micro-Verhalten.
Laufender Fußausflug erhält StopRequested und retraced tatsächlich gelaufene
Checkpoints; andere Micros beendet. Danach lokaler MMAP-Rückweg zum Anker,
Hold bei3.5m, gescheiterter Weg10sRetry. Kein Warten über Intentdeadline hinaus,
kein Teleport. Reisen/Kampf/Transport/Intentwechsel haben Vorrang.
Micro/LeisureAvailable/adminmicro verhindern neue Aktionen im Sammelfenster;
Gruppenchat/Einladungen bleiben normal aktiv. Regel ausDeadline abgeleitet,
gilt auch für manuelle und nach Neustart wiederhergestellte Pläne.
Neue tests/clientless/test_adventure_group_gather.py: echter Handler mitDoubles,
Grenzen301/300/1/0s, Ausflugabbruch, Vorbereitungsende, Retry, Hold und Release.
Dieser Test plus leisure+intent_state erfolgreich. Breitere Checks ebenfalls erfolgreich (insgesamt sieben Tests).
Build2 /tmp/vcore-group-gather-build2.log erfolgreich, installiert nach sauberem
Shutdown. NeuerTestserverPID290160, tmuxvcore-worldserver, Konsole:
/tmp/vcore-group-gather-start-20261029.log. Freshready+Ports8085/8087 bestätigt.
SiebengezielteTests erfolgreich /tmp/vcore-group-gather-test-results.json.
LiveCutoff14:55 beobachtet: GROUP_GATHER_START/READYmehrererBots;
Arthoran26 vorCutoff explizitgroup_search_walk gestartet (keinScheduleoverride),
beiCutoffRückweg4Legs+LEISURE_COMPLETE, dannGROUP_GATHER_READY bestätigt.
Testdeadline15:00 unverändert. LaufendeBotpopulation gestaffelt wiederhergestellt.
Native14:56StatusArthoran26:microleer,waiting at group meeting point;
adminmicroprofession_tools imFenster abgelehnt.42verschiedeneBots bereits
START+READY. Noch kein nativer15:00Wechselbeobachtet, Deadlinefreigabegetestet.
Dokumentation group-preparation.md und checkpoints/2026-10-29-group-gather/.
Keine neuen Planänderungen für dieseRegel erforderlich.
SQL02 der vorherigenKorrektur ist wegenRepoIgnore jetzt mit git add -N -f als
Intent-to-add sichtbar; Inhalt angewendet, noch kein Commit/Push.
Alle vorhandenen Source-/Dokumentations-WIP sind eigene vorherige Änderungen,
nicht verwerfen. Siehe ältere Checkpoints unten.
---
# Laufende Korrektur: sichtbare Vorbereitung während Gruppensuche
Nutzer beobachtete: Bots sammeln sich in Gilded Rose, keine sichtbare
Vorbereitung/Stadtsuche. Diagnose: walk nur8-12m/budget18m; profession_tools
DBzugeordnet, aber Available UND Update verboten außerhalbprofession;
Gruppenmitglieder FollowMember verhinderte eigene Micros.
Implementiert (noch nicht committed):
- Groupwalk70/110mRadien,220mPfadbudget,Minspan65m,Endpunkt muss draußen sein;
Weg darf von Inn nach draußen führen, bleibt in Hauptstadt+250mAnkerumkreis,
vollständigeMMAP-Korridore und Rückweg unverändert.
- ProfessionPolicy::IntentAllows erlaubt nurprofession_tools zusätzlich für
groupformation; Available/Update benutzen dieselbe Regel. Keine Berufsausübung
währendGruppensuche freigeschaltet.
- Botgruppenmitglieder folgen währendForming nicht zwangsweise Leader,
können eigene Micros machen; normales Folgen außerhalbForming bleibt.
- SQL2026_10_29_02_adventure_group_preparation_walk.sql angewendet:
group_search_walk120-240s statt35-65; VorherwertBackup
/tmp/vcore-group-preparation-action-before.tsv.
6gezielte Tests erfolgreich, Ergebnisse /tmp/vcore-group-preparation-test-results.json.
Build2 erfolgreich /tmp/vcore-group-preparation-build2.log; installiert.
AlterTestserverPID280452 sauber gestoppt. NeuerStart ready und PID287554
bestätigt; Ports8085/8087 lauschen. Konsole:
/tmp/vcore-group-preparation-start-20261029.log. Native Logs bestätigen Aelion29
Stadtgang (14Wegpunkte) sowieAldren32 span70m/8Wegpunkte und195sDauer.
Arthoran26 profession_tools per micro-Befehl geprüft: SOCIAL_START und
SOCIAL_CUE CheckGear20/Listen4 bestätigt; kein Scheduleoverride gesetzt. Loader1 mit50/5/50/5; persistierte Testpläne14Gruppensuche/15InstanzRaid
bleiben erhalten. Externe Ports intern8085/8087 behalten.
Nutzerkurzfrage: ob5minvorIntentwechselSammelpflicht existiert. Geantwortet:
NEIN, nur Rückweg nach einzelnen Suchgängen, keine feste5minRegel. Eine solche
Regel NICHT implementiert/noch nicht zusätzlich beauftragt. Nutzerunterbrechung
hat bereits ausgelösten Shutdown nicht rückgängig gemacht; Server daher zunächst
wieder in Betrieb nehmen, dann vorherige Korrektur live prüfen.
Kein Commit/Push. Vorherige SESSION-RESUME-Änderungen sind eigeneroperativerWIP,
nicht verwerfen. Sämtliche übrigen Sourceänderungen dieserGruppensucheKorrektur.
---
# Gruppensuche-Test vorbereitet und vollständig aktiv (2026-10-29)
Nutzer verlangt Wiederholung des Tests: Bots in Fraktionshauptstädte,
aktueller Stundenblock Gruppensuche, nächster Instanz/Raid gemischt.
Zusatzentscheidung Nutzer: Bots ohne gültiges Instanzziel vom Test ausnehmen.
94Teilnehmer (49Allianz,45Horde);16Bots unter15 ausgeschlossen und nicht verändert.
110Bots insgesamt; Loader wiederEnabled1 mit50/5/50/5, läuft normal.
Teilnehmer nativ abgemeldet, scoped DB-Positionen nach Sturmwind/Orgrimmar,
Reise-/Ghost-/performative Instanzreste dieser94 bereinigt; Allitester unberührt.
Tagesplan persistent20261029:14:00-15:00 groupformation, Sammelpunkte
Allianzpoi_guard_22 /Hordepoi_guard_179.15:00-16:00 80instance+14raid.
Raidfähige Bots jeFraktion abwechselndRaid/Instanz; alle Ziele VOR Änderung
vom nativen eligibility-Check als own1 bestätigt. Mindestlevel15, Fraktionen
und Gebietskatalog bleiben gültig. Keine Profil-/Level-/Codeänderung.
Andere22Tagesplanblöcke nach Snapshotvergleich unverändert (187geänderteSlots,
einer der188Zielslots hatte schon den gewünschten Wert).
Abschluss:94/94online undlautDBinrichtigerHauptstadt;94nativeRuntimeStatus
bestätigen intentgroupformation+schedule1. GROUP_CHANNEL-Meldungen beobachtet.
Sämtliche Snapshots, Auswahl, Phasenstatus, Runtimechecks unter:
/tmp/vcore-capital-group-test-20261029/ (state.json Phase ready).
Skripte /tmp/vcore-capital-group-test.py (Nativevalidation),
/tmp/vcore-capital-group-test-apply.py (abgeschlossen; nicht blind erneut starten).
TestserverPID280452, Ports8085/8087; kein Neustart nötig/ausgeführt.
Keine VPS-Änderung, keine laufende Netzwerkbeobachtung, kein Commit/Push.
---
# Normaler Botbetrieb wieder aktiviert
Nutzer bestätigt externen Login nach Portumstellung und verlangt normale
Botanmeldung. Installierte Config Loader.Enabled wieder1; reload config und
clientless population reload durchgeführt. Staffelung aktiv, erste erfolgreiche
Logins Arthoran26 und Aelion29 bestätigt; DBonline_bots2 bei erster Prüfung.
Ziele unverändert50persistente+5temporäre jeFraktion (gesamt110).
WorldserverPID280452/tmuxvcore-worldserver bleibt aktiv, Ports8085/8087.
Konsole /tmp/vcore-instance-8087-start-20261029.log. Keine laufende
Netzwerkbeobachtung. Alte Hinweise 'Bots offline halten' unten sind historisch
und durch diese Nutzeranweisung aufgehoben. Kein Code/Commit/Push.
---
# Testserver für zweite öffentliche Verbindung auf8087 umgestellt
Auf Nutzerauftrag NUR installierte Testserver-Config angepasst:
WorldServerPort8085 unverändert, InstanceServerPort von8086 auf8087.
Sauber heruntergefahren, neu gestartet, frischer ready-Start bestätigt,
PID280452, tmux vcore-worldserver, Konsole:
/tmp/vcore-instance-8087-start-20261029.log.
Listener0.0.0.0:8085 und0.0.0.0:8087 bestätigt;8086 lokal nicht mehrListener.
Realmlist2 unverändert: cata.viano-software.de:8086, local192.168.56.10.
Keine VPS/Auth-DB-Änderung durchgeführt. Nutzer richtet VPS:8087->VM:8087 ein,
bisher VPS:8086->VM:8085 bleibt. Externer End-to-End-Test noch nicht erfolgt.
Bots bleiben offline (DBonline_bots0), Loader.Enabled0; keine Reaktivierung.
Vorbereitete Beobachter /tmp/vcore-dual-port-watch.py und
/tmp/vcore-until-login-watch.py auf8085/8087 und dynamische PID angepasst,
NICHT gestartet. Keine Codeänderung, kein Build nötig, kein Commit/Push.
---
# Dritter Verbindungstest: Allitester erfolgreich online
Auf Nutzerwunsch maximal120s oder bis erfolgreichem Login überwacht. Bei erster
Prüfung13:27:11MEZ Allitester21 online1 (nativer Onlineflag wird nach Map-Add
und LoginVerifyWorld gesetzt). Daher Beobachter automatisch sofort beendet.
Beide Ports hatten ESTAB-Verbindungen:8085 über10.0.0.3,8086 über192.168.56.10.
CharakterMap0/Zone1519, Sturmwind. Kein laufender Beobachter.
Artefaktverzeichnis in /tmp/vcore-until-login-last-dir; dort metadata,
samples.jsonl und finished.json mit erfolgreichem Login als Abbruchgrund.
Bots bleiben offline, Loader.Enabled0; Wiederaktivierung nicht angeordnet.
---
# Zweiter externer Verbindungstest abgeschlossen
Nutzertest mit beiden Ports8085/8086 lief13:16:39-13:18:39MEZ (120.24s),
automatisch beendet, Beobachter nicht mehr aktiv. Verzeichnis steht in
/tmp/vcore-dual-port-last-dir; dort metadata/samples/server-new/summary/finished.
274Socket-Samples, beide PortsLISTEN; Verbindung nur auf8085 beobachtet,
keine8086Verbindung in Samples. Allitester21 weiterhinonline0. Im Testfenster
nur CPC_PERF, keine neue Login-Abbruchmeldung (voriger Versuch hatte fünf
fehlgeschlagene World-Socket-Verbindungen). Kein Paketmitschnitt: Abwesenheit
in Socket-Samples ist kein Beweis für ausbleibende SYN-Pakete.
Externe Portzuordnung/Zieladresse weiterhin verdächtig: Realmlist2 annonciert
cata.viano-software.de:8086, Worldserver intern8085/Instance8086. Keine
Netzwerk-/Realmänderung vorgenommen. Bots bleiben offline, Loader.Enabled0.
Keine laufende Überwachung. Nutzer hat Wiederaktivierung noch nicht angeordnet.
---
# Externer Verbindungstest: Beobachtung beendet
Fünfminuten-Fenster auf Nutzerwunsch ab13:05:43MEZ, automatische Abschaltung
13:10:46MEZ (letzte ss-Abfrage verlängerte Abschluss um ca3s). Beide Beobachter
beendet. Ergebnis /tmp/vcore-five-minute-connection-test/summary.json,
Samples und Serverlog-Deltas im selben Verzeichnis; Anfangsphase im bisherigen
/tmp/vcore-connection-watch.log. TCP durchgehendESTAB, kein beobachteter Abbruch,
RTT rund40-50ms, zuletzt alle2132gesendetenBytes bestätigt. Kein Paketmitschnitt.
Bots bleiben OFFLINE, Loader.Enabled bleibt0. NICHT automatisch reaktivieren:
Nutzer hat nur Ende der Überwachung angeordnet. Worldserver bleibt aktiv.
Keine weitere laufende Netzwerkbeobachtung.
---
# Betriebszustand: externer Verbindungstest vorbereitet
Auf Nutzerwunsch alle Bots ausgeloggt, DB und native Loaderstatus bestätigen0
online/pending, manager0. Test-worldserver PID270857 läuft weiter, TCP8085 offen.
Installierte Config Clientless.Adventure.Loader.Enabled vorübergehend von1 auf0,
per reload config und clientless population reload wirksam. Poolziele50/5/50/5
unverändert. NICHT vor Ende des Nutzertests reaktivieren! Nutzer gibt Startsignal.
Passiver TCP-Socket-Beobachter: /tmp/vcore-connection-watch.py, PID-Datei
/tmp/vcore-connection-watch.pid, Log /tmp/vcore-connection-watch.log (max2h).
Paketmitschnitt nicht verfügbar: CAP_NET_RAW fehlt; sudo benötigt Authentifizierung.
Socketzustände/Queues/TCP-Infos und Serverlogs sind ohne Root verfügbar.
Nach Test nur auf entsprechende Nutzeranweisung Loader wieder aktivieren und
Beobachter beenden. Originalzustand in /tmp/vcore-external-connection-test-state.txt.
Bordnavigation inzwischen committed als2841ee973b; nachfolgender Text historisch.
---
# Aktuelle Aufgabe: freie Bordnavigation (2026-10-29)
Vorheriger Schritt lokal committed als49712aadfe. Bordnavigation implementiert
+14
View File
@@ -0,0 +1,14 @@
# Portals to the Blasted Lands
Enabled on the test world database on 2026-10-29 using the existing native importer:
```text
clientless portal object import 219963
clientless portal object import 211359
clientless portal object import 211298
clientless adventure reload
```
Stormwind spawn 219963 (entry 195141, spell 65728); Orgrimmar spawns 211359 and 211298 (entry 195142, spell 65729). The coincident Orgrimmar spawn 218904 is intentionally not duplicated in the graph. Source node ACLs are 1 (Alliance) and 2 (Horde), respectively, in `vcore_clientless_node_access`. Destination nodes retain the shared Blasted Lands area policy. Native spell destinations are used, with one-way click edges and no invented teleport. Existing graph enabled flags are preserved by repeated imports.
Bindings verified in the database (edges 1758–1760); native importer confirms active graph bindings. End-to-end traversal is pending gameplay verification. No C++ changes or binary installation. The server was already stopped when activation began; recovered with a fresh startup, ready marker and both 8085/8087 listeners verified. Console: `/tmp/vcore-portal-enable-start-20261029.log`.
@@ -0,0 +1,4 @@
Flight surface map=530 x=3358.734 y=1369.172 valid=1 ground=504.148 ceiling=916.971
Flight departure guid=84 position=(3358.734 1369.172 514.129) nearFeet=-1087.028 rawWater=-100000.000 liquidStatus=0 liquidLevel=0.000 mounted=1 canFly=1 flying=0 falling=0
TC> Flight surface map=530 x=-3650.900 y=316.897 valid=1 ground=48.203 ceiling=238.089
Flight departure guid=92 position=(-3650.900 316.897 35.852) nearFeet=35.852 rawWater=-100000.000 liquidStatus=0 liquidLevel=0.000 mounted=0 canFly=0 flying=0 falling=0
@@ -0,0 +1,80 @@
2026-10-29_18:17:06 INFO [server.adventurebot.navigation] FLIGHT_RETURN_LANDING guid=84 target=(3353.078 1374.829 505.070)
2026-10-29_18:17:06 INFO [server.adventurebot.navigation] FLIGHT_RESUME_AIRBORNE guid=84 ground=504.148 z=514.129
2026-10-29_18:17:09 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=1 avoid=0 handoff=0 target=(3337.33 1356.26 514.13) detail='terrain-smoothed cruise'
2026-10-29_18:17:09 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=2 avoid=0 handoff=0 target=(3315.92 1343.35 506.48) detail='diagonal descent'
2026-10-29_18:17:09 INFO [server.adventurebot.navigation] FLIGHT_RETURN_LANDING guid=92 target=(-3337.327 1810.882 148.003)
2026-10-29_18:17:09 INFO [server.adventurebot.navigation] FLIGHT_RESUME_AIRBORNE guid=92 ground=148.003 z=176.404
2026-10-29_18:17:09 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=1 avoid=0 handoff=0 target=(-3355.93 1794.18 176.40) detail='terrain-smoothed cruise'
2026-10-29_18:17:11 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=3 avoid=0 handoff=1 target=(3313.32 1341.79 505.55) detail='landing approach'
2026-10-29_18:17:12 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=2 avoid=0 handoff=0 target=(-3374.52 1777.47 165.90) detail='diagonal descent'
2026-10-29_18:17:12 INFO [server.adventurebot.navigation] FLIGHT_LANDED guid=84 segments=3 avoidances=0 travelled=54.32 error=0.00
2026-10-29_18:17:13 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=3 avoid=0 handoff=1 target=(-3386.43 1766.78 159.18) detail='diagonal descent'
2026-10-29_18:17:13 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=4 avoid=0 handoff=1 target=(-3397.90 1756.48 152.71) detail='diagonal descent'
2026-10-29_18:17:13 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=84 name='Brynli' edgeType='free_flight' edgeDbId=0 node='portal_at_4455_target' recovery=0
2026-10-29_18:17:13 INFO [server.adventurebot.navigation] FLIGHT_DEPARTURE_WALK guid=84 destination=(3305.935 1343.281 505.359) path=8.00
2026-10-29_18:17:14 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=5 avoid=0 handoff=1 target=(-3410.40 1745.25 145.65) detail='diagonal descent'
2026-10-29_18:17:15 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=6 avoid=0 handoff=1 target=(-3426.64 1730.66 136.48) detail='diagonal descent'
2026-10-29_18:17:15 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=1 avoid=1 handoff=0 target=(3284.50 1356.15 520.37) detail='obstacle avoidance'
2026-10-29_18:17:21 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=7 avoid=0 handoff=0 target=(-3445.24 1713.96 125.98) detail='diagonal descent'
2026-10-29_18:17:21 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=2 avoid=1 handoff=0 target=(3259.65 1358.87 526.62) detail='obstacle avoidance'
2026-10-29_18:17:22 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=8 avoid=0 handoff=1 target=(-3457.14 1703.27 119.26) detail='diagonal descent'
2026-10-29_18:17:22 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=3 avoid=0 handoff=1 target=(3254.94 1379.45 514.08) detail='diagonal descent'
2026-10-29_18:17:23 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=9 avoid=0 handoff=1 target=(-3470.06 1691.66 111.97) detail='diagonal descent'
2026-10-29_18:17:23 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=4 avoid=0 handoff=1 target=(3246.04 1394.36 506.79) detail='diagonal descent'
2026-10-29_18:17:26 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=10 avoid=0 handoff=0 target=(-3488.66 1674.95 101.47) detail='diagonal descent'
2026-10-29_18:17:26 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=5 avoid=0 handoff=0 target=(3233.22 1415.82 496.29) detail='diagonal descent'
2026-10-29_18:17:27 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=11 avoid=0 handoff=0 target=(-3507.26 1658.25 101.47) detail='terrain-smoothed cruise'
2026-10-29_18:17:27 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=6 avoid=0 handoff=0 target=(3220.40 1437.29 485.79) detail='diagonal descent'
2026-10-29_18:17:29 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=12 avoid=0 handoff=1 target=(-3518.95 1647.75 101.47) detail='terrain-smoothed cruise'
2026-10-29_18:17:30 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=13 avoid=0 handoff=1 target=(-3497.67 1666.86 90.97) detail='diagonal descent'
2026-10-29_18:17:30 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=7 avoid=0 handoff=0 target=(3207.58 1458.75 475.29) detail='diagonal descent'
2026-10-29_18:17:37 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=14 avoid=0 handoff=0 target=(-3516.27 1650.16 90.97) detail='terrain-smoothed cruise'
2026-10-29_18:17:37 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=8 avoid=0 handoff=0 target=(3194.76 1480.21 464.79) detail='diagonal descent'
2026-10-29_18:17:37 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=15 avoid=0 handoff=1 target=(-3490.93 1672.92 80.47) detail='diagonal descent'
2026-10-29_18:17:39 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=9 avoid=0 handoff=1 target=(3186.56 1493.95 458.07) detail='diagonal descent'
2026-10-29_18:17:40 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=16 avoid=0 handoff=0 target=(-3509.53 1656.21 66.03) detail='diagonal descent'
2026-10-29_18:17:40 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=10 avoid=0 handoff=0 target=(3173.74 1515.41 447.57) detail='diagonal descent'
2026-10-29_18:17:41 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=17 avoid=0 handoff=1 target=(-3523.95 1643.25 54.82) detail='diagonal descent'
2026-10-29_18:17:41 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=11 avoid=0 handoff=1 target=(3163.15 1533.14 438.89) detail='diagonal descent'
2026-10-29_18:17:42 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=18 avoid=0 handoff=1 target=(-3535.84 1632.58 45.60) detail='diagonal descent'
2026-10-29_18:17:42 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=12 avoid=0 handoff=1 target=(3154.43 1547.73 431.75) detail='diagonal descent'
2026-10-29_18:17:43 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=13 avoid=0 handoff=1 target=(3146.10 1561.69 424.93) detail='diagonal descent'
2026-10-29_18:17:43 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=92 n=19 avoid=0 handoff=1 target=(-3536.55 1631.94 45.04) detail='landing approach'
2026-10-29_18:17:44 INFO [server.adventurebot.navigation] FLIGHT_LANDED guid=92 segments=19 avoidances=0 travelled=396.61 error=0.00
2026-10-29_18:17:44 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=14 avoid=0 handoff=0 target=(3133.28 1583.15 414.43) detail='diagonal descent'
2026-10-29_18:17:46 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=15 avoid=0 handoff=1 target=(3148.76 1557.23 406.11) detail='diagonal descent'
2026-10-29_18:17:47 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=16 avoid=0 handoff=1 target=(3133.95 1582.03 397.24) detail='diagonal descent'
2026-10-29_18:17:48 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=17 avoid=0 handoff=1 target=(3123.94 1598.79 389.04) detail='diagonal descent'
2026-10-29_18:17:49 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=18 avoid=0 handoff=1 target=(3115.62 1612.72 382.23) detail='diagonal descent'
2026-10-29_18:17:58 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=19 avoid=0 handoff=0 target=(3128.44 1591.25 371.73) detail='diagonal descent'
2026-10-29_18:17:59 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=20 avoid=0 handoff=1 target=(3114.97 1613.80 361.76) detail='diagonal descent'
2026-10-29_18:18:00 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=21 avoid=0 handoff=1 target=(3103.77 1632.56 352.58) detail='diagonal descent'
2026-10-29_18:18:01 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=22 avoid=0 handoff=1 target=(3094.85 1647.49 345.28) detail='diagonal descent'
2026-10-29_18:18:02 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=23 avoid=0 handoff=1 target=(3111.53 1619.57 337.93) detail='diagonal descent'
2026-10-29_18:18:03 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=24 avoid=0 handoff=1 target=(3094.22 1648.54 331.10) detail='diagonal descent'
2026-10-29_18:18:04 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=25 avoid=0 handoff=1 target=(3085.52 1663.11 323.98) detail='diagonal descent'
2026-10-29_18:18:05 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=26 avoid=0 handoff=1 target=(3075.48 1679.92 315.75) detail='diagonal descent'
2026-10-29_18:18:08 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=27 avoid=0 handoff=0 target=(3088.30 1658.45 305.25) detail='diagonal descent'
2026-10-29_18:18:14 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=28 avoid=0 handoff=0 target=(3075.48 1679.92 294.75) detail='diagonal descent'
2026-10-29_18:18:15 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=29 avoid=0 handoff=1 target=(3065.84 1696.06 286.85) detail='diagonal descent'
2026-10-29_18:18:17 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=30 avoid=0 handoff=1 target=(3057.32 1710.33 279.88) detail='diagonal descent'
2026-10-29_18:18:17 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=31 avoid=0 handoff=1 target=(3048.02 1725.90 272.26) detail='diagonal descent'
2026-10-29_18:18:20 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=32 avoid=0 handoff=0 target=(3060.84 1704.44 261.76) detail='diagonal descent'
2026-10-29_18:18:20 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=33 avoid=0 handoff=1 target=(3043.78 1732.99 254.72) detail='diagonal descent'
2026-10-29_18:18:23 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=34 avoid=0 handoff=0 target=(3030.96 1754.46 244.22) detail='diagonal descent'
2026-10-29_18:18:24 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=35 avoid=0 handoff=1 target=(3048.03 1725.89 237.20) detail='diagonal descent'
2026-10-29_18:18:25 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=36 avoid=0 handoff=1 target=(3030.35 1755.48 230.68) detail='diagonal descent'
2026-10-29_18:18:26 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=37 avoid=0 handoff=1 target=(3021.46 1770.37 223.39) detail='diagonal descent'
2026-10-29_18:18:27 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=38 avoid=0 handoff=1 target=(3038.73 1741.45 216.54) detail='diagonal descent'
2026-10-29_18:18:30 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=39 avoid=0 handoff=1 target=(3021.38 1770.51 209.75) detail='diagonal descent'
2026-10-29_18:18:31 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=40 avoid=0 handoff=0 target=(3008.56 1791.97 199.25) detail='diagonal descent'
2026-10-29_18:18:32 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=41 avoid=0 handoff=1 target=(2999.30 1807.46 191.67) detail='diagonal descent'
2026-10-29_18:18:33 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=42 avoid=0 handoff=1 target=(3014.27 1782.40 182.93) detail='diagonal descent'
2026-10-29_18:18:37 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=43 avoid=0 handoff=0 target=(3001.45 1803.86 169.35) detail='diagonal descent'
2026-10-29_18:18:52 WARN [server.adventurebot.navigation] FLIGHT_RETURN guid=84 reason='flight segment made no confirmed progress'
2026-10-29_18:18:53 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=44 avoid=1 handoff=0 target=(3014.27 1782.40 182.93) detail='obstacle avoidance'
2026-10-29_18:18:54 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=45 avoid=1 handoff=0 target=(3001.45 1803.86 193.43) detail='obstacle avoidance'
2026-10-29_18:18:55 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=46 avoid=1 handoff=0 target=(3012.12 1786.00 202.17) detail='obstacle avoidance'
2026-10-29_18:18:56 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=47 avoid=1 handoff=0 target=(3021.38 1770.51 209.75) detail='obstacle avoidance'
2026-10-29_18:18:57 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=48 avoid=1 handoff=0 target=(3034.20 1749.04 220.25) detail='obstacle avoidance'
2026-10-29_18:18:59 INFO [server.adventurebot.navigation] FLIGHT_SEGMENT guid=84 n=49 avoid=1 handoff=0 target=(3025.91 1762.92 227.04) detail='obstacle avoidance'
@@ -0,0 +1,10 @@
{
"test_flight_departure": 0,
"test_flight_handoff": 0,
"test_free_flight_planner": 0,
"test_free_flight": 0,
"test_flight_area_query": 0,
"test_flight_terrain_fallback": 0,
"test_flight_policy": 0,
"test_group_flight_follow": 0
}
@@ -0,0 +1 @@
Native terrain query map/phase, area/zone ordering and cache passed.
@@ -0,0 +1 @@
Production flight admission: airborne mount retained, lower-floor MMAP departure, blocked/cliff/missing support rejected.
@@ -0,0 +1 @@
Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.
@@ -0,0 +1 @@
Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.
@@ -0,0 +1 @@
Missing phase tile uses native base terrain; an existing phase tile with missing collision stays forbidden
@@ -0,0 +1 @@
Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.
@@ -0,0 +1 @@
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
@@ -0,0 +1 @@
Moving-target flight: bounded phased corridors, obstacle hold, ceiling rejection, aerial formation hold, safe landing and return fallback passed
@@ -0,0 +1,132 @@
Adventure GUID 26: enabled=1 schedule=1 phase=1 intent='groupformation' location='poi_guard_22' micro='' deadline=1793282400 detail='travelling to poi_guard_22'
LEISURE_WALK_PLAN guid=26 kind='walk' span=110.05 outbound=7 origin=(-8867.00 673.63 97.90) goal=(-8826.98 571.17 94.50)
GROUP_GATHER_START guid=58 deadline=1793282400
GROUP_GATHER_START guid=29 deadline=1793282400
GROUP_GATHER_READY guid=29
GROUP_GATHER_START guid=26 deadline=1793282400
GROUP_GATHER_START guid=55 deadline=1793282400
GROUP_GATHER_START guid=32 deadline=1793282400
GROUP_GATHER_READY guid=32
GROUP_GATHER_START guid=59 deadline=1793282400
GROUP_GATHER_START guid=31 deadline=1793282400
GROUP_GATHER_READY guid=31
GROUP_GATHER_START guid=36 deadline=1793282400
GROUP_GATHER_START guid=35 deadline=1793282400
GROUP_GATHER_READY guid=35
GROUP_GATHER_START guid=38 deadline=1793282400
GROUP_GATHER_READY guid=38
GROUP_GATHER_START guid=40 deadline=1793282400
GROUP_GATHER_READY guid=36
GROUP_GATHER_READY guid=40
GROUP_GATHER_START guid=42 deadline=1793282400
GROUP_GATHER_READY guid=42
GROUP_GATHER_START guid=39 deadline=1793282400
GROUP_GATHER_READY guid=39
GROUP_GATHER_READY guid=58
LEISURE_COMPLETE guid=26 kind='walk' legs=4
GROUP_GATHER_READY guid=26
GROUP_GATHER_START guid=44 deadline=1793282400
GROUP_GATHER_READY guid=44
GROUP_GATHER_READY guid=59
GROUP_GATHER_START guid=43 deadline=1793282400
GROUP_GATHER_READY guid=43
GROUP_GATHER_READY guid=55
GROUP_GATHER_START guid=45 deadline=1793282400
GROUP_GATHER_READY guid=45
GROUP_GATHER_START guid=41 deadline=1793282400
GROUP_GATHER_READY guid=41
GROUP_GATHER_START guid=46 deadline=1793282400
GROUP_GATHER_READY guid=46
GROUP_GATHER_START guid=63 deadline=1793282400
GROUP_GATHER_READY guid=63
GROUP_GATHER_START guid=48 deadline=1793282400
GROUP_GATHER_READY guid=48
GROUP_GATHER_START guid=47 deadline=1793282400
GROUP_GATHER_READY guid=47
GROUP_GATHER_START guid=62 deadline=1793282400
GROUP_GATHER_READY guid=62
GROUP_GATHER_START guid=52 deadline=1793282400
GROUP_GATHER_READY guid=52
GROUP_GATHER_START guid=51 deadline=1793282400
GROUP_GATHER_READY guid=51
GROUP_GATHER_START guid=139 deadline=1793282400
GROUP_GATHER_READY guid=139
GROUP_GATHER_START guid=53 deadline=1793282400
GROUP_GATHER_READY guid=53
GROUP_GATHER_START guid=54 deadline=1793282400
GROUP_GATHER_READY guid=54
GROUP_GATHER_START guid=56 deadline=1793282400
GROUP_GATHER_READY guid=56
GROUP_GATHER_START guid=61 deadline=1793282400
GROUP_GATHER_READY guid=61
GROUP_GATHER_START guid=64 deadline=1793282400
GROUP_GATHER_READY guid=64
GROUP_GATHER_READY guid=36
GROUP_GATHER_START guid=60 deadline=1793282400
GROUP_GATHER_READY guid=60
GROUP_GATHER_START guid=69 deadline=1793282400
GROUP_GATHER_READY guid=69
GROUP_GATHER_START guid=68 deadline=1793282400
GROUP_GATHER_READY guid=68
GROUP_GATHER_START guid=71 deadline=1793282400
GROUP_GATHER_READY guid=71
GROUP_GATHER_START guid=70 deadline=1793282400
GROUP_GATHER_READY guid=70
GROUP_GATHER_START guid=73 deadline=1793282400
GROUP_GATHER_READY guid=73
GROUP_GATHER_START guid=72 deadline=1793282400
GROUP_GATHER_READY guid=72
GROUP_GATHER_START guid=67 deadline=1793282400
GROUP_GATHER_READY guid=67
GROUP_GATHER_START guid=74 deadline=1793282400
GROUP_GATHER_READY guid=74
GROUP_GATHER_START guid=75 deadlinWARNING: Spell 84729 effect 0 has an unsupported misc value (9) for SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE
GROUP_GATHER_READY guid=75
GROUP_GATHER_READY guid=52
GROUP_GATHER_START guid=77 deadline=1793282400
GROUP_GATHER_READY guid=77
GROUP_GATHER_START guid=76 deadline=1793282400
GROUP_GATHER_READY guid=76
GROUP_GATHER_READY guid=71
GROUP_GATHER_READY guid=42
[?2004hTC>Adventure GUID 26: enabled=1 schedule=1 phase=2 intent='groupformation' location='poi_guard_22' micro='' deadline=1793282400 detail='waiting at group meeting point before departure'
TC> The bot is gathering for departure; no new micro actions during the final five minutes.
GROUP_GATHER_READY guid=63
GROUP_GATHER_READY guid=64
GROUP_GATHER_START guid=82 deadline=1793282400
GROUP_GATHER_READY guid=82
GROUP_GATHER_START guid=83 deadline=1793282400
GROUP_GATHER_READY guid=83
GROUP_GATHER_READY guid=44
GROUP_GATHER_START guid=84 deadline=1793282400
GROUP_GATHER_READY guid=84
GROUP_GATHER_READY guid=38
GROUP_GATHER_START guid=85 deadline=1793282400
GROUP_GATHER_READY guid=85
GROUP_GATHER_START guid=80 deadline=1793282400
GROUP_GATHER_READY guid=80
GROUP_GATHER_READY guid=58
GROUP_GATHER_READY guid=40
GROUP_GATHER_READY guid=70
GROUP_GATHER_START guid=87 deadline=1793282400
GROUP_GATHER_READY guid=87
GROUP_GATHER_START guid=86 deadline=1793282400
GROUP_GATHER_READY guid=86
GROUP_GATHER_START guid=91 deadline=1793282400
GROUP_GATHER_READY guid=91
GROUP_GATHER_START guid=93 deadline=1793282400
GROUP_GATHER_READY guid=93
GROUP_GATHER_START guid=92 deadline=1793282400
GROUP_GATHER_READY guid=92
GROUP_GATHER_START guid=94 deadline=1793282400
GROUP_GATHER_READY guid=94
GROUP_GATHER_READY guid=77
GROUP_GATHER_START guid=97 deadline=1793282400
GROUP_GATHER_READY guid=97
GROUP_GATHER_START guid=99 deadline=1793282400
GROUP_GATHER_READY guid=99
GROUP_GATHER_START guid=96 deadline=1793282400
GROUP_GATHER_READY guid=96
GROUP_GATHER_READY guid=91
GROUP_GATHER_START guid=88 deadline=1793282400
GROUP_GATHER_READY guid=88
@@ -0,0 +1,9 @@
{
"test_adventure_group_gather": 0,
"test_adventure_leisure": 0,
"test_adventure_social": 0,
"test_adventure_groups": 0,
"test_adventure_group_travel": 0,
"test_adventure_intent_state": 0,
"test_adventure_profession_sightline": 0
}
@@ -0,0 +1 @@
Production five-minute gathering: exact cutoff, safe corridor return request, preparation cancellation, bounded retries, meeting hold and deadline release passed
@@ -0,0 +1 @@
Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed
@@ -0,0 +1 @@
Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.
@@ -0,0 +1 @@
Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.
@@ -0,0 +1 @@
Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.
@@ -0,0 +1 @@
Native workstation working-surface sightline and blocked geometry rejection passed.
@@ -0,0 +1 @@
Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed.
@@ -0,0 +1,18 @@
# Gathering validation,2026-10-29
Build2 completed; installed after verified graceful shutdown. Fresh ready marker,
worldserverPID290160 and listeners8085/8087 verified. Existing test plans and
loader targets50/5 per faction retained. Seven targeted regression programs pass.
During the real14:55 cutoff for the15:00 test deadline, native logs show
GROUP_GATHER_START/READY. At14:56,42 distinct bots had both events; remaining bots
were still joining through the staggered loader. Arthoran26 had an explicit
pre-cutoff search walk (no schedule override): safe return,4 completed legs,
GROUP_GATHER_READY. His native status then reported empty micro and
'waiting at group meeting point before departure'. A forced profession_tools
micro command was rejected during the window. These are runtime/log checks,
not visual acceptance of every bot. The subsequent15:00 departure has not yet
occurred at the time of this checkpoint; exact deadline release is tested with
the deterministic production handler and existing schedule handover tests.
No teleport or additional group-completeness wait added. No commit/push.
@@ -0,0 +1,8 @@
/home/wow_admin/wow_cata_server/server/debug/bin/Server.log:GameObject spell use failed: entry=195142 spawn=211298 spell=65729 user=GUID Full: 0x0000000000000053 Type: Player Low: 83 result=172 playerCast=0
/home/wow_admin/wow_cata_server/server/debug/bin/Server.log:GameObject spell use failed: entry=195142 spawn=211298 spell=65729 user=GUID Full: 0x0000000000000039 Type: Player Low: 57 result=172 playerCast=0
/home/wow_admin/wow_cata_server/server/debug/bin/Server.log:GameObject spell use failed: entry=195142 spawn=211298 spell=65729 user=GUID Full: 0x000000000000002e Type: Player Low: 46 result=172 playerCast=0
/home/wow_admin/wow_cata_server/server/debug/bin/adventurebot-navigation.log:2026-10-29_16:49:56 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=83 name='Mahuwa' edgeType='portal_use' edgeDbId=1760 node='portal_go_211298_target' spawn=211298 map=0 arrivalError=0.00 recovery=0
/home/wow_admin/wow_cata_server/server/debug/bin/adventurebot-navigation.log:2026-10-29_16:51:29 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=99 name='Shadra' edgeType='portal_use' edgeDbId=1760 node='portal_go_211298_target' spawn=211298 map=0 arrivalError=0.00 recovery=0
/home/wow_admin/wow_cata_server/server/debug/bin/adventurebot-navigation.log:2026-10-29_16:51:56 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=96 name='Darden' edgeType='portal_use' edgeDbId=1755 node='portal_go_170_target' spawn=170 map=0 arrivalError=0.00 recovery=2
/home/wow_admin/wow_cata_server/server/debug/bin/adventurebot-navigation.log:2026-10-29_16:52:47 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=109 name='Rixbin' edgeType='portal_use' edgeDbId=1760 node='portal_go_211298_target' spawn=211298 map=0 arrivalError=0.00 recovery=0
/home/wow_admin/wow_cata_server/server/debug/bin/adventurebot-navigation.log:2026-10-29_16:54:03 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=128 name='Volakmar' edgeType='portal_use' edgeDbId=1760 node='portal_go_211298_target' spawn=211298 map=0 arrivalError=0.00 recovery=0
@@ -0,0 +1,223 @@
[
{
"spawn": 83,
"bot": 102,
"success": false,
"actual": [
530.0,
-1894.19,
5387.94,
-11.9283
],
"target": [
0.0,
-8998.14,
861.254,
29.6206
]
},
{
"spawn": 170,
"bot": 102,
"success": true,
"actual": [
0.0,
-8998.14,
861.254,
29.6206
],
"target": [
0.0,
-8998.14,
861.254,
29.6206
]
},
{
"spawn": 164,
"bot": 83,
"success": true,
"actual": [
1.0,
1776.5,
-4338.8,
-7.48
],
"target": [
1.0,
1776.5,
-4338.8,
-7.48
]
},
{
"spawn": 219963,
"bot": 102,
"success": true,
"actual": [
0.0,
-11708.4,
-3168.0,
-5.07
],
"target": [
0.0,
-11708.4,
-3168.0,
-5.07
]
},
{
"spawn": 211298,
"bot": 83,
"success": true,
"actual": [
0.0,
-11708.4,
-3168.0,
-5.07
],
"target": [
0.0,
-11708.4,
-3168.0,
-5.07
]
},
{
"spawn": 61217,
"bot": 102,
"success": true,
"actual": [
0.0,
-8998.14,
861.254,
29.6206
],
"target": [
0.0,
-8998.14,
861.254,
29.6206
]
},
{
"spawn": 56568,
"bot": 83,
"success": true,
"actual": [
1.0,
1776.5,
-4338.8,
-7.48
],
"target": [
1.0,
1776.5,
-4338.8,
-7.48
]
},
{
"spawn": 57634,
"bot": 102,
"success": true,
"actual": [
1.0,
-8164.8,
-4768.5,
34.3
],
"target": [
1.0,
-8164.8,
-4768.5,
34.3
]
},
{
"spawn": 224457,
"bot": 102,
"success": false,
"actual": [
0.0,
-9547.84,
85.0556,
60.6021
],
"target": [
974.0,
-3618.67,
6315.67,
113.19
]
},
{
"spawn": 214421,
"bot": 83,
"success": false,
"actual": [
1.0,
-1471.78,
196.071,
-7.2928
],
"target": [
974.0,
-3618.67,
6315.67,
113.19
]
},
{
"spawn": 213395,
"bot": 102,
"success": true,
"actual": [
0.0,
-9517.5,
82.3,
59.51
],
"target": [
0.0,
-9517.5,
82.3,
59.51
]
},
{
"spawn": 212440,
"bot": 83,
"success": true,
"actual": [
1.0,
-1444.41,
215.34,
-7.75
],
"target": [
1.0,
-1444.41,
215.34,
-7.75
]
},
{
"spawn": 83,
"bot": 102,
"success": true,
"actual": [
0.0,
-8998.14,
861.254,
29.6206
],
"target": [
0.0,
-8998.14,
861.254,
29.6206
]
}
]
@@ -0,0 +1,2 @@
Portal condition migration: explicit user target, preserved level gate, scoped updates and idempotence passed.
Public portal caster migration: static allowlist, party/type guards and idempotence passed.
@@ -0,0 +1 @@
Portal near/far ACK and denied same-map transfer regressions passed.
@@ -0,0 +1,3 @@
Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.
Native portal destination discovery and unsupported/ambiguous spell rejection passed.
Production clientless ACK completion sequence increments only after completed near/far transfers.
@@ -0,0 +1,65 @@
entry name spell playerCast spawn map destinationPresent targetMap
183325 Shattrath Portal to Stormwind 17334 0 83 530 1 0
187056 Shattrath Portal to Isle of Quel'Danas 44876 0 47196 530 0 NULL
189985 Nexus Portal 49305 0 1046 578 1 578
190960 Dalaran Portal to Stormwind 17334 0 61217 571 1 0
191009 Dalaran Portal to Orgrimmar 17609 0 56568 571 1 1
193052 Portal to Orgrimmar 58418 0 66403 0 0 NULL
193053 Portal to Stormwind 58420 0 66405 0 0 NULL
193604 Dalaran Portal to Caverns of Time 59901 0 57634 571 1 1
193772 Portal to Wintergrasp 58622 0 61496 571 0 NULL
193952 Surface Portal 60905 0 87582 571 1 571
194011 Portal to The Purple Parlor 61419 0 60709 571 1 571
194012 Portal to The Violet Citadel 61420 0 67278 571 1 571
194012 Portal to The Violet Citadel 61420 0 151243 571 1 571
195071 Nightmare Portal 65409 0 204733 1 0 NULL
195139 Portal to Stormwind 17334 0 170 530 1 0
195140 Portal to Orgrimmar 17609 0 164 530 1 1
195141 Portal to Blasted Lands 65728 0 151163 530 1 0
195141 Portal to Blasted Lands 65728 0 217855 0 1 0
195141 Portal to Blasted Lands 65728 0 219119 1 1 0
195141 Portal to Blasted Lands 65728 0 219963 0 1 0
195142 Portal to Blasted Lands 65729 0 151162 530 1 0
195142 Portal to Blasted Lands 65729 0 203251 0 1 0
195142 Portal to Blasted Lands 65729 0 211298 1 1 0
195142 Portal to Blasted Lands 65729 0 211359 1 1 0
195142 Portal to Blasted Lands 65729 0 214456 1 1 0
195142 Portal to Blasted Lands 65729 0 218904 1 1 0
195371 Gunship Portal 66899 0 7088 641 1 628
195682 Portal to Dalaran 68328 0 21625 712 1 571
195682 Portal to Dalaran 68328 0 21630 713 1 571
196413 Gunship Portal 66899 0 7090 642 1 628
202079 Portal to Dalaran 71512 0 150335 631 1 571
205268 Portal to the Maelstrom 84464 0 223195 0 1 730
205268 Portal to the Maelstrom 84464 0 223199 1 1 730
205272 Portal to Stormwind 84505 0 223197 646 0 NULL
205273 Portal to Orgrimmar 84506 0 223235 646 0 NULL
205359 Portal to Therazane's Throne 84699 0 224294 646 1 646
206594 Portal to Tol Barad 88339 0 220365 0 1 732
206595 Portal to Tol Barad 88341 0 211264 1 1 732
206595 Portal to Tol Barad 88341 0 219035 1 1 732
207073 Ajamon's Portal to Tirth's Haunt 89685 0 201971 1 0 NULL
207078 Ajamon's Portal to the Southsea Holdfast 89691 0 202033 1 0 NULL
207565 Portal to Blasted Lands 93733 0 224514 1 1 0
207566 Portal to Blasted Lands 93726 0 224513 1 1 0
207686 Portal to Twilight Highlands 82621 0 211260 1 1 0
207687 Portal to Uldum 94564 0 211259 1 1 1
207688 Portal to Hyjal 94565 0 219028 1 1 1
207689 Portal to Deepholm 84513 0 211258 1 1 646
207690 Portal to Vashj'ir 90244 0 211257 1 0 NULL
207692 Portal to Hyjal 94565 0 200624 0 1 1
207693 Portal to Deepholm 84515 0 200625 0 1 646
207694 Portal to Twilight Highlands 85545 0 200627 0 1 0
207695 Portal to Uldum 94564 0 200626 0 1 1
207995 Portal to Exodar 32268 0 219120 1 1 530
208226 Portal to Orgrimmar 96177 0 213807 732 1 1
208227 Portal to Stormwind 96178 0 213979 732 1 0
208552 Portal to Mount Hyjal 99664 1 239512 861 0 NULL
208900 Portal to the Firelands 99556 1 238273 1 0 NULL
209080 Portal to Stormwind 84505 0 237748 1 0 NULL
209081 Portal to Orgrimmar 84506 0 237749 1 0 NULL
209538 Portal to the Darkmoon Faire 102864 0 214421 1 1 974
209538 Portal to the Darkmoon Faire 102864 0 224457 0 1 974
210107 Darkmoon Deathmatch Gate 108941 0 213507 974 0 NULL
210175 Portal to Elwynn Forest 101260 0 213395 974 1 0
210176 Portal to Mulgore 103582 0 212440 974 1 1
1 entry name spell playerCast spawn map destinationPresent targetMap
2 183325 Shattrath Portal to Stormwind 17334 0 83 530 1 0
3 187056 Shattrath Portal to Isle of Quel'Danas 44876 0 47196 530 0 NULL
4 189985 Nexus Portal 49305 0 1046 578 1 578
5 190960 Dalaran Portal to Stormwind 17334 0 61217 571 1 0
6 191009 Dalaran Portal to Orgrimmar 17609 0 56568 571 1 1
7 193052 Portal to Orgrimmar 58418 0 66403 0 0 NULL
8 193053 Portal to Stormwind 58420 0 66405 0 0 NULL
9 193604 Dalaran Portal to Caverns of Time 59901 0 57634 571 1 1
10 193772 Portal to Wintergrasp 58622 0 61496 571 0 NULL
11 193952 Surface Portal 60905 0 87582 571 1 571
12 194011 Portal to The Purple Parlor 61419 0 60709 571 1 571
13 194012 Portal to The Violet Citadel 61420 0 67278 571 1 571
14 194012 Portal to The Violet Citadel 61420 0 151243 571 1 571
15 195071 Nightmare Portal 65409 0 204733 1 0 NULL
16 195139 Portal to Stormwind 17334 0 170 530 1 0
17 195140 Portal to Orgrimmar 17609 0 164 530 1 1
18 195141 Portal to Blasted Lands 65728 0 151163 530 1 0
19 195141 Portal to Blasted Lands 65728 0 217855 0 1 0
20 195141 Portal to Blasted Lands 65728 0 219119 1 1 0
21 195141 Portal to Blasted Lands 65728 0 219963 0 1 0
22 195142 Portal to Blasted Lands 65729 0 151162 530 1 0
23 195142 Portal to Blasted Lands 65729 0 203251 0 1 0
24 195142 Portal to Blasted Lands 65729 0 211298 1 1 0
25 195142 Portal to Blasted Lands 65729 0 211359 1 1 0
26 195142 Portal to Blasted Lands 65729 0 214456 1 1 0
27 195142 Portal to Blasted Lands 65729 0 218904 1 1 0
28 195371 Gunship Portal 66899 0 7088 641 1 628
29 195682 Portal to Dalaran 68328 0 21625 712 1 571
30 195682 Portal to Dalaran 68328 0 21630 713 1 571
31 196413 Gunship Portal 66899 0 7090 642 1 628
32 202079 Portal to Dalaran 71512 0 150335 631 1 571
33 205268 Portal to the Maelstrom 84464 0 223195 0 1 730
34 205268 Portal to the Maelstrom 84464 0 223199 1 1 730
35 205272 Portal to Stormwind 84505 0 223197 646 0 NULL
36 205273 Portal to Orgrimmar 84506 0 223235 646 0 NULL
37 205359 Portal to Therazane's Throne 84699 0 224294 646 1 646
38 206594 Portal to Tol Barad 88339 0 220365 0 1 732
39 206595 Portal to Tol Barad 88341 0 211264 1 1 732
40 206595 Portal to Tol Barad 88341 0 219035 1 1 732
41 207073 Ajamon's Portal to Tirth's Haunt 89685 0 201971 1 0 NULL
42 207078 Ajamon's Portal to the Southsea Holdfast 89691 0 202033 1 0 NULL
43 207565 Portal to Blasted Lands 93733 0 224514 1 1 0
44 207566 Portal to Blasted Lands 93726 0 224513 1 1 0
45 207686 Portal to Twilight Highlands 82621 0 211260 1 1 0
46 207687 Portal to Uldum 94564 0 211259 1 1 1
47 207688 Portal to Hyjal 94565 0 219028 1 1 1
48 207689 Portal to Deepholm 84513 0 211258 1 1 646
49 207690 Portal to Vashj'ir 90244 0 211257 1 0 NULL
50 207692 Portal to Hyjal 94565 0 200624 0 1 1
51 207693 Portal to Deepholm 84515 0 200625 0 1 646
52 207694 Portal to Twilight Highlands 85545 0 200627 0 1 0
53 207695 Portal to Uldum 94564 0 200626 0 1 1
54 207995 Portal to Exodar 32268 0 219120 1 1 530
55 208226 Portal to Orgrimmar 96177 0 213807 732 1 1
56 208227 Portal to Stormwind 96178 0 213979 732 1 0
57 208552 Portal to Mount Hyjal 99664 1 239512 861 0 NULL
58 208900 Portal to the Firelands 99556 1 238273 1 0 NULL
59 209080 Portal to Stormwind 84505 0 237748 1 0 NULL
60 209081 Portal to Orgrimmar 84506 0 237749 1 0 NULL
61 209538 Portal to the Darkmoon Faire 102864 0 214421 1 1 974
62 209538 Portal to the Darkmoon Faire 102864 0 224457 0 1 974
63 210107 Darkmoon Deathmatch Gate 108941 0 213507 974 0 NULL
64 210175 Portal to Elwynn Forest 101260 0 213395 974 1 0
65 210176 Portal to Mulgore 103582 0 212440 974 1 1
+95
View File
@@ -0,0 +1,95 @@
# Flight departure repair (2026-10-29)
## Reproduced failures
Read-only native probes showed two distinct cases on map530:
* Brynli84: position3358.734/1369.172/514.129, upper surface504.148,
nearby-feet query no valid supporting surface, mounted1/canFly1/flying0.
This is an already suspended native flying mount, not a ground departure.
* Tholgar92: position-3650.900/316.897/35.852, upper surface48.203,
feet surface35.852, mounted0/canFly0. The sky-down query selects an overhead
layer rather than the actual supporting floor.
Neither case had liquid present; the raw-water possibility was excluded by
native GetLiquidStatus. The existing cruise/landing surface sampling stays
unchanged. A missing height/collision map is still a hard rejection.
## Changes
An existing learned profile flight mount can resume in a verified airborne
volume above the cruise surface and below the ceiling. The existing aura is
retained, no dismount/remount is performed. For an emergency breadcrumb return,
the ground below or within40yd of a resumed hover is used only after the complete descent column
and the flight corridor from the hover pass collision checks; a hover point itself is not claimed as a supported
landing. New splines still require the normal
volume collision, catalog and clearance checks. A ground mount, falling player,
missing surface, ceiling penetration or unknown mount cannot use this recovery.
A grounded visitor under an upper layer must have support within3yd using a
bounded query from feet+2. Search nearby open takeoff candidates at8/16/24/40yd,
with native vertical volume rays (center plus four2yd offsets), allowed area segment and complete local MMAP
route (<=80yd, maxsegment8yd, maxheightstep4yd, endpoint<=1.5yd, no suspicious path).
Use the existing local travel manager to walk there. Flight remains in mounting
status during this approach but does not cast the mount until real arrival.
Interrupt/death/map change/timeout cancels local travel; stale arrival away from
the chosen point is rejected. Shared flight trail identity survives the approach.
No teleport, forced elevation, disabled collision or invented ground surface.
Departure failures blacklist all flight targets from the same route source for
this action's recovery rather than trying five targets from an unchanged source.
Ground travel and departures at other nodes remain eligible.
A short final landing step may descend directly to the target when already
in flight, within25yd total distance and at most12yd above the landing. The
entire native collision/terrain volume must still pass Corridor; high approaches
and obstacles retain the existing avoidance logic. This prevents the observed
near-ground back-and-forth approach. Touchdown confirms the real support surface
from feet+2 (6yd VMAP search), not the sky-down upper floor; the body ray must be
clear before dismounting, including on breadcrumb return under an overhang.
`clientless flight probe guid x y` now includes actual player position, bounded
feet height, raw water and native liquid status, mount/flight/falling flags.
## Validation
Eight targeted tests passed, including production departure admission and open
MMAP search with deterministic layered terrain, mount retention, blocked paths,
cliffs, unsupported players and abort cancellation. Real planner test checks
source-wide flight exclusion while retaining a ground alternative. Update tests
wait for actual ground arrival and reject stale completion before mounting.
Existing flight geometry, handoff, group follow, terrain fallback, catalog and
native flight eligibility regressions also passed.
Final build /tmp/vcore-flight-safe-return-final-build.log and install succeeded.
Latest testserver PID317961, gdb/tmuxvcore-worldserver, console
/tmp/vcore-flight-safe-return-final-start.log. Fresh ready and8085/8087 listeners
verified. No persistent bot profile, plan or position changes were used for
flight validation; native logins requested for84/92 to observe normal behavior.
Live outcome is recorded separately in the checkpoint. The pre-existing
OutdoorPvP shutdown destructor crash remains separate and was preserved.
Landing candidates and emergency return anchors now use the same five-ray open
column check as takeoff candidates, up to the requested clearance within native
ceiling limits. This avoids selecting a narrow landing center whose surrounding
mount volume is obstructed. Ground return for resumed hovers is never assumed
without that column check.
Safe return-anchor search checks0/8/16/24/40yd radii, at most80yd spatial
distance, native open landing volume, area eligibility and full Corridor from
the resumed hover. A blocked column below the hover can therefore use a nearby
checked ground point; unknown/unreachable points do not authorize a landing.
Cancelling the takeoff ground approach does not interrupt unrelated combat casts.
## Final live result
At18:17:06 Brynli84 resumed the native flying mount and selected a verified
return landing near the former hover. At18:17:12 the normal requested flight
landed (54.32yd,3segments,zero avoidances); executor confirmed portal_at_4455_target
free_flight edge completion with recovery0, then continued its route.
Tholgar92 used a real8.18yd MMAP takeoff approach in the preceding run and
subsequently completed longer flight legs. The final build resumed his native
flight and completed396.61yd/19segments/zero avoidances at18:17:44.
See final-live.log. No final dungeon/raid destination or visual in-game acceptance
is claimed from these intermediate leg checks. Profiles/plans unchanged; server
remains online with normal staged population. No commit or push.
+40
View File
@@ -0,0 +1,40 @@
# Group preparation and gathering
During groupformation, bots may independently check equipment, talk and take
validated foot outings into surrounding capital streets. Bot group members do
not follow their leader continuously during preparation; the leader's shared
meeting location and next destination still govern the group. Normal group
following resumes outside formation. Only profession_tools is permitted as a
profession scene in this intent; gathering/crafting/trainer work remains limited
to the profession intent.
The group_search_walk catalog action now lasts120-240s (world migration
2026_10_29_02). Outings select70/110m radii, require a complete native MMAP
corridor of at most220m, an outdoor endpoint in the same capital, and retrace
the walked route to the meeting. These are surrounding-street searches, not
unrestricted tours through the entire city. No route/collision checks are waived.
The last300 seconds before the actual groupformation deadline are a gathering
window. At301s remaining preparation is allowed; at300s no new micro action may
start (including explicit admin micro commands). The same guard applies to
leisure availability and the main action picker. A foot outing finishes its
current short checkpoint and retraces visited checkpoints. Other micro scenes
end and the bot returns with local pathfinding to its arrival anchor at the
shared meeting location. Within3.5m it waits; it does not start another action.
A blocked return retries every10s, without a teleport or delaying the next
intent. Chat and group invitations remain available.
Normal travel to the meeting, combat/transport safety and an already pending
intent change take precedence. The gathering window ends at the deadline:
the ordinary schedule/manual transition may then start from the actual position.
No requirement for a complete group or every member's presence was added.
The cutoff derives from the existing intent deadline. It therefore also applies
after restart, to explicit preparation durations, and to merged multi-hour blocks
using their actual end rather than every intervening hour. Gathering bookkeeping
is transient; no new persistent schema or profile setting is required.
Native navigation events GROUP_GATHER_START and GROUP_GATHER_READY identify the
cutoff and arrival. Adventure status reports waiting/returning/path-retry detail.
Tests cover exact cutoff boundaries, preparation cancellation, safe return
requests, blocked-path retries, meeting hold and release at the deadline.
+72
View File
@@ -0,0 +1,72 @@
# Public portal investigation and repair (2026-10-29)
Two scoped world migrations address public static portals. No general spell,
LOS, faction, holiday or native interaction checks are disabled.
## Findings
* Blasted Lands spells 65728/65729 had level conditions on target 0 (caster).
Static portals cast as GameObjects, so the native Unit level check failed even
for eligible visitors. Live diagnostic returned 172. Migration 03 moves only
these level-58 conditions to explicit unit target 1. Minimum and comparison
remain unchanged. Before/after native traversal of Orgrimmar spawn 211298
confirmed the condition repair independently of the later caster change.
* Public portal templates used `spellcaster.playerCast=0`. Shattrath's Stormwind
portal accepted clicks and prepared a GameObject-origin cast but produced no
transfer. The spell target selection includes GO-origin LOS checks which can
remove the unit target after cast preparation. This is a plausible mechanism,
not an instrumented proof of the exact filtering branch. Migration 04 selects
the existing visitor-cast mode for twelve named public portal templates. The
interacting player now self-casts the native teleport, including normal spell
conditions and teleport handling. No destination or spell is replaced.
* Native destination audit found no missing required fixed DB destinations for
the examined spawned portal templates. Some quest/trigger/script portals do
not require a direct DB destination and are not classified as broken for that.
* Darkmoon entrance spawns 214421/224457 belong to event 3. Outside that event
they are unavailable; the event was not enabled or bypassed for this test.
## Live validation
Direct native clicks with isolated bots Corren (Alliance) and Mahuwa (Horde)
confirmed ten available portal spawns: Shattrath -> Stormwind (83), Hellfire ->
Stormwind/Orgrimmar (170/164), capitals -> Blasted Lands (219963/211298), Dalaran
-> Stormwind/Orgrimmar/Caverns of Time (61217/56568/57634), Darkmoon Island ->
Elwynn/Mulgore (213395/212440). Saved post-transfer positions match native spell
DB destinations. The first spawn-83 attempt was rejected while the bot was still
in transit; its isolated repeat succeeded. Both event-gated Darkmoon entrances
were unavailable as expected; actual traversal during the event remains open.
These tests cover native interaction and completed transfers, not visual client
acceptance or complete autonomous travel to the portal.
Bots were temporarily positioned using twelve task-specific game_tele fixtures.
Specific copies of their generated profiles disabled autonomous behavior during
probes without changing their persistent profiles. All fixtures and profile
copies were removed, game_tele reloaded, generated profiles restored via
luareload and normal schedules resumed. Arthoran's existing override is restored
byte-for-byte. No player character was changed.
## Diagnostics
`clientless portal object use <onlineBotGuid> <spawnId>` sends the native
CMSG_GAMEOBJ_USE handler without a graph edge or teleport shortcut. Requires a
clientless player and a spawned, visible, same-phase, unlocked spellcaster in
native interaction range; rejects dead/in-transit/transport passengers. A sent
click alone does not prove transfer; inspect final position/ACK. Failed native
spell casts now log entry, spawn, spell, user GUID and result at WARN under
entities.gameobject. Temporarily lower root log threshold to 4 if needed; the
test threshold has been restored to configured 5.
## Build and regressions
Diagnostic builds and installation succeeded; latest build
/tmp/vcore-portal-probe-build.log. Tests test_portal_conditions.py,
test_portal_use.py and test_portal_transfer.py pass. The existing ACK harness
was updated for the production delayed-teleport guard and now tests that guard.
SQL tests check scoped changes, preserved native level gate and repeatability.
See checkpoints/2026-10-29-public-portals for test outputs and live results.
Server is running under gdb in vcore-worldserver, latest startup console
/tmp/vcore-public-portals-fixed-start.log; fresh ready marker and listeners
8085/8087 checked. A separate reproducible shutdown-time OutdoorPvP destructor
SIGSEGV was captured in earlier debugger logs, outside the portal repair scope.
The original runtime vector assertion remains a separate unresolved crash.
@@ -0,0 +1,4 @@
-- A recruitment outing must have time to leave an inn, search nearby capital
-- streets and return; the previous 35-65 seconds only supported tiny indoor laps.
UPDATE vcore_adventure_action SET min_seconds=120,max_seconds=240
WHERE action_key='group_search_walk';
@@ -0,0 +1,6 @@
-- Static portals cast these spells as GameObjects. Apply the minimum level
-- to the explicit unit target (the user), not the non-unit portal caster.
UPDATE conditions SET ConditionTarget=1
WHERE SourceTypeOrReferenceId=17 AND SourceGroup=0 AND SourceEntry IN (65728,65729)
AND SourceId=0 AND ConditionTypeOrReference=27 AND ConditionTarget=0
AND ConditionValue1=58 AND ConditionValue2=3 AND NegativeCondition=0;
@@ -0,0 +1,6 @@
-- Public static portals cast their visitor teleport as the interacting player.
-- Keep normal interaction, spell conditions and teleport handling; do not
-- alter summoned mage portals, quest portals, destinations or event gating.
UPDATE gameobject_template SET data6=1
WHERE type=22 AND data2=0 AND data6=0 AND entry IN
(183325,183323,195139,195140,190960,191009,195141,195142,193604,209538,210175,210176);
@@ -2520,6 +2520,10 @@ void GameObject::Use(Unit* user)
else
castResult = CastSpell(user, spellId);
if (GetGoType() == GAMEOBJECT_TYPE_SPELLCASTER && castResult != SPELL_CAST_OK)
TC_LOG_WARN("entities.gameobject", "GameObject spell use failed: entry=%u spawn=%u spell=%u user=%s result=%u playerCast=%u",
GetEntry(), GetSpawnId(), spellId, user->GetGUID().ToString().c_str(), uint32(castResult), m_goInfo->spellcaster.playerCast);
if (addUse && castResult == SPELL_CAST_OK)
AddUse();
}
+50 -5
View File
@@ -619,6 +619,7 @@ bool AdventureBehaviorMgr::FishingDirection(Player* p,float& orientation) const
bool AdventureBehaviorMgr::LeisureAvailable(Player* p,Record const& r,AdventureMicroAction const& a) const
{
if(AdventureGroupPolicy::Gathering(r.Intent,r.Deadline,GameTime::GetGameTime()))return false;
if (Unsafe(p) || p->IsInWater() || p->IsFalling()) return false;
// Adventure exploration shares the proven native ground-outing executor.
if (r.Intent=="adventure") return (a.Requirement=="explore" || a.Requirement=="patrol") && p->IsOutdoors();
@@ -659,18 +660,18 @@ bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
// Choose one substantial destination, then follow its actual ground corridor.
// Randomness selects the outing, never the next individual footstep.
float const offset=std::uniform_real_distribution<float>(0,6.2831853f)(r.Random);
float const budget=r.Intent=="groupformation"?18.0f:std::min(220.0f,std::max(100.0f,float(r.MicroTimer)*0.001f*1.0f));
float const budget=r.Intent=="groupformation"?220.0f:std::min(220.0f,std::max(100.0f,float(r.MicroTimer)*0.001f*1.0f));
float best=0;
for (unsigned i=0;i<16;++i)
{
float const angle=offset+float(i%8)*6.2831853f/8;
float const radius=r.Intent=="groupformation"?(i<8?12.0f:8.0f):(i<8?std::min(150.0f,budget*0.7f):80.0f);
float const radius=r.Intent=="groupformation"?(i<8?110.0f:70.0f):(i<8?std::min(150.0f,budget*0.7f):80.0f);
Position goal;
goal.Relocate(s.Origin.GetPositionX()+std::cos(angle)*radius,
s.Origin.GetPositionY()+std::sin(angle)*radius,s.Origin.GetPositionZ());
float const z=p->GetMap()->GetHeight(p->GetPhaseShift(),goal.GetPositionX(),goal.GetPositionY(),
s.Origin.GetPositionZ()+2,true,15);
if (!std::isfinite(z) || z<=INVALID_HEIGHT+1 || std::fabs(z-s.Origin.GetPositionZ())>3) continue;
if (!std::isfinite(z) || z<=INVALID_HEIGHT+1 || std::fabs(z-s.Origin.GetPositionZ())>(r.Intent=="groupformation"?20.0f:3.0f)) continue;
goal.m_positionZ=z;
PathGenerator navigation(p);
navigation.SetPathLengthLimit(240);
@@ -680,7 +681,7 @@ bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
std::vector<Position> outbound;
Position last=s.Origin;
float length=0,section=0;
bool valid=true;
bool valid=true,endsOutdoors=false;
for (auto const& point:path)
{
Position target;target.Relocate(point.x,point.y,point.z);
@@ -692,11 +693,12 @@ bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
uint32 zone=0,area=0;
p->GetMap()->GetTerrain()->GetZoneAndAreaId(p->GetPhaseShift(),p->GetMapId(),zone,area,target);
if ((r.Intent!="groupformation" && !terrain.outdoors) || (boundary && area!=boundary && zone!=boundary)) {valid=false;break;}
endsOutdoors=terrain.outdoors;
length+=step;section+=step;last=target;
if (section>=20) {outbound.push_back(target);section=0;}
}
float const span=s.Origin.GetExactDist(last);
if (!valid || length>budget || span<(r.Intent=="groupformation"?4.0f:65.0f) || length>span*2.5f || last.GetExactDist(goal)>1.5f) continue;
if (!valid || length>budget || span<65.0f || (r.Intent=="groupformation" && !endsOutdoors) || length>span*2.5f || last.GetExactDist(goal)>1.5f) continue;
if (outbound.empty() || outbound.back().GetExactDist(last)>2) outbound.push_back(last);
else outbound.back()=last;
if (span>best)
@@ -1103,6 +1105,7 @@ bool AdventureBehaviorMgr::UpdateSocial(Player* player,Record& r,uint32 diff)
void AdventureBehaviorMgr::Micro(Player * player, Record & r)
{
if(AdventureGroupPolicy::Gathering(r.Intent,r.Deadline,GameTime::GetGameTime()))return;
auto spacingTravel=sClientlessTravelMgr->GetStatus(player->GetGUID().GetCounter());
if(r.NextSpacing>GameTime::GetGameTime() && spacingTravel && spacingTravel->State==ClientlessTravelState::Moving)return;
if(GameTime::GetGameTime()>=r.NextSpacing && player->GetStandState()==UNIT_STAND_STATE_STAND &&
@@ -1347,6 +1350,45 @@ bool AdventureBehaviorMgr::UpdateNavigationRescue(Player* player,Record& r)
return true;
}
bool AdventureBehaviorMgr::UpdateGathering(Player* player,Record& r,time_t now)
{
if(!AdventureGroupPolicy::Gathering(r.Intent,r.Deadline,now))
{r.Gathering=false;r.GatherRetryAt=0;return false;}
// A late arrival still uses its regular travel route. Combat/transport
// safety and an already requested intent change retain their precedence.
if(r.State!=Phase::OnSite || r.Dirty || Unsafe(player))return false;
uint32 guid=player->GetGUID().GetCounter();
if(!r.Gathering)
{
r.Gathering=true;r.GatherRetryAt=0;
TC_LOG_INFO("server.adventurebot","GROUP_GATHER_START guid=%u deadline=%llu",guid,static_cast<unsigned long long>(r.Deadline));
}
if(r.Leisure.Active)
{
// Finish the current short ground checkpoint, then retrace only the
// actually walked corridor. Never replace it with a straight shortcut.
r.Leisure.StopRequested=true;r.MicroTimer=0;return false;
}
if(!r.Micro.empty() || r.Social.Active || r.Recovery.Active)ResetMicro(player,r);
auto travel=sClientlessTravelMgr->GetStatus(guid);
if(player->GetMapId()==r.AnchorMap && player->GetExactDist(r.Anchor)<=3.5f)
{
if(travel && travel->State==ClientlessTravelState::Moving)
{sClientlessTravelMgr->CancelTravel(guid);player->StopMoving();}
if(r.Detail!="waiting at group meeting point before departure")
TC_LOG_INFO("server.adventurebot","GROUP_GATHER_READY guid=%u",guid);
r.Detail="waiting at group meeting point before departure";
return true;
}
if(travel && travel->State==ClientlessTravelState::Moving)return true;
if(now<r.GatherRetryAt)return true;
r.GatherRetryAt=now+10;
if(LocalMove(player,r,r.Anchor,500))
r.Detail="returning to group meeting point before departure";
else r.Detail="group meeting return path unavailable; retrying until intent change";
return true;
}
void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
{
uint32 guid = player->GetGUID().GetCounter();
@@ -1430,6 +1472,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
}
if(auto group=player->GetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID())
if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))AdoptGroup(player,leader);
if(UpdateGathering(player,r,now))return;
if (advanceLeisure()) return;
if (Unsafe(player))
{
@@ -1617,6 +1660,8 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
}
if(op=="micro")
{
if(AdventureGroupPolicy::Gathering(r.Intent,r.Deadline,GameTime::GetGameTime()))
{handler->SendSysMessage("The bot is gathering for departure; no new micro actions during the final five minutes.");return false;}
std::string key;in>>key;
if(!r.Enabled||r.State!=Phase::OnSite||Unsafe(player)||Busy(guid))
{handler->SendSysMessage("Use an idle on-site AdventureBot, without combat or transit.");return false;}
@@ -59,6 +59,8 @@ private:
std::string PersonalIntent,PersonalLocation;
time_t PersonalDeadline=0;
time_t Deadline=0;
bool Gathering=false;
time_t GatherRetryAt=0;
uint32 AreaRevision=0;
time_t StalledSince=0,LastRescue=0,RescueAt=0,NextSpacing=0,LastNavigationCheck=0;
uint32 RescueFailures=0,ProgressMap=0;
@@ -92,6 +94,7 @@ private:
};
bool ResolveInstance(Player* player, Record& record);
bool UpdateNavigationRescue(Player* player,Record& record);
bool UpdateGathering(Player* player,Record& record,time_t now);
bool SaveInstance(Player* player, Record& record);
bool RestoreInstance(Player* player, Record& record);
bool UpdateInstance(Player* player, Record& record, time_t now);
@@ -229,6 +229,8 @@ bool AdventureGroupMgr::FollowMember(Player* player)
{
auto group=player->GetGroup();
if(!group || !BotLed(player) || group->GetLeaderGUID()==player->GetGUID())return false;
// Preparations are individual; regroup at the shared meeting before departure.
if(Forming(player))return false;
return sAdventureGroupTravelMgr->Follow(player);
}
@@ -8,6 +8,8 @@ inline bool Capital(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 1637:case 1638:case 1497:case 3487:case 3557:case 3703:case 4395:return true;default:return false;}}
inline unsigned CapitalFaction(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 3557:return 1;case 1637:case 1638:case 1497:case 3487:return 2;default:return 0;}}
inline bool Gathering(std::string const& intent,long long deadline,long long now)
{return intent=="groupformation" && deadline>now && deadline-now<=300;}
inline bool Eligible(std::string const& intent)
{return intent=="instance" || intent=="raid" || intent=="adventure" || intent=="profession" || intent=="social";}
inline unsigned Capacity(std::string const& intent)
@@ -58,6 +58,8 @@ inline bool Recognized(std::string const& kind)
{
return Find(kind) || kind=="profession_tools" || kind=="profession_trainer" || kind=="profession_vendor";
}
inline bool IntentAllows(std::string const& kind,std::string const& intent)
{return intent=="profession" || (intent=="groupformation" && kind=="profession_tools");}
inline bool Untargeted(std::string const& kind){return kind=="profession_tools" || kind=="work_first_aid";}
inline std::string Legacy(std::string const& requirement)
{
@@ -423,7 +423,7 @@ void AdventureSocialActions::Prepare(Player * player)
bool AdventureSocialActions::Available(Player * p, AdventureMicroAction const & action, AdventureSocialContext & c)
{
auto const & kind = action.Requirement;
if (AdventureProfessionPolicy::Recognized(kind)) return c.Intent=="profession" &&
if (AdventureProfessionPolicy::Recognized(kind)) return AdventureProfessionPolicy::IntentAllows(kind,c.Intent) &&
AdventureProfessionPolicy::Allowed(kind,c.PrimaryProfessions) &&
(AdventureProfessionPolicy::Untargeted(kind)?!p->IsInWater() && !p->IsFalling():!AdventureProfessionTargets::Find(p,kind,c).empty());
if (AdventureExplorationPolicy::Inspection(kind)) return c.Intent=="adventure" && !InspectionObjects(p,c).empty();
@@ -558,7 +558,7 @@ bool AdventureSocialActions::Start(Player * p, AdventureSocialScene & s, Adventu
bool AdventureSocialActions::Update(Player * p, AdventureSocialScene & s, uint32 diff, AdventureSocialContext & c)
{
if (! s.Active) return false;
if(AdventureProfessionPolicy::Recognized(s.Kind) && (c.Intent!="profession" || !AdventureProfessionPolicy::Allowed(s.Kind,c.PrimaryProfessions)))return false;
if(AdventureProfessionPolicy::Recognized(s.Kind) && (!AdventureProfessionPolicy::IntentAllows(s.Kind,c.Intent) || !AdventureProfessionPolicy::Allowed(s.Kind,c.PrimaryProfessions)))return false;
s.Elapsed += diff;
if (s.Elapsed > (AdventureEconomyPolicy::Economic(s.Kind)?240000u:120000u)) return false;
if (s.Departing)
@@ -1539,6 +1539,8 @@ bool ClientlessActionExecutorMgr::RecoverFlightLeg(Player* player,uint32 charact
{
auto const& source=record.Route[record.RouteStep-1];
auto const& target=record.Route[record.RouteStep];
if(reason.rfind("departure-unavailable:",0)==0)
record.FailedBridgeKeys.insert("flight_origin:"+source.NodeKey);
if(source.NodeKey=="virtual_ground_exit")
record.FailedBridgeKeys.insert("flight_exit:exit:"+std::to_string(source.EdgeDatabaseId)+":"+std::to_string(source.GroundEndIndex)+(source.EdgeReverse?"R":"F"));
else if(target.NodeKey=="flight_location")record.FailedBridgeKeys.insert("flight_finish:"+source.NodeKey);
+117 -11
View File
@@ -53,6 +53,15 @@ namespace
float x, y, z;
return map->isInLineOfSight(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags(0)) && ! map->getObjectHitPos(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, x, y, z, 0);
}
bool OpenColumn(Player* player,float x,float y,Surface const& surface,float height=15)
{
if(!surface.Valid)return false;
float top=std::min(surface.Ceiling-5,surface.Ground+height);
if(top<surface.Ground+5.8f)return false;
for(auto offset:std::array<Point,5>{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}})
if(!ClearRay(player,{x+offset.X,y+offset.Y,surface.Ground+0.8f},{x+offset.X,y+offset.Y,top}))return false;
return true;
}
char const * StateName(ClientlessFlightState state)
{
switch (state)
@@ -158,6 +167,7 @@ ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const
void ClientlessFlightMgr::Clear(uint32 guid)
{
auto it=_records.find(guid);
if(it!=_records.end() && it->second.ApproachingDeparture)sClientlessTravelMgr->CancelTravel(guid);
if(it!=_records.end() && it->second.Status.State!=ClientlessFlightState::Arrived)it->second.Published->Aborted=true;
_records.erase(guid);
}
@@ -305,15 +315,26 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
roll -= candidate.second;
}
Record record;
auto previous=_records.find(player->GetGUID().GetCounter());
if(previous!=_records.end() && previous->second.ApproachingDeparture)
record.Published=previous->second.Published;
record.Status.MapId = player->GetMapId();
record.Status.Target = target;
record.Start.Relocate(player);
record.LastPosition.Relocate(player);
record.Settings.Clearance = std::clamp(sConfigMgr->GetFloatDefault("Clientless.Flight.Clearance", 60), 20.0f, 200.0f);
auto origin = Sample(player, record, player->GetPositionX(), player->GetPositionY());
if (! origin.Valid || std::fabs(origin.Ground - player->GetPositionZ()) > 3)
uint32 existingMount=0;
if(player->IsMounted() && player->CanFly())
for(auto const& candidate:candidates)if(player->HasAura(candidate.first)){existingMount=candidate.first;break;}
bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f &&
player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5});
float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
if (!origin.Valid || (!airborne && (std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3)))
{
error = "unknown flight bounds or no safe ground departure";
if(origin.Valid && std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true;
error = "departure-unavailable: no supported open takeoff point";
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u mounted=%u canFly=%u",player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,origin.Valid?1:0,player->IsMounted()?1:0,player->CanFly()?1:0);
return false;
}
bool foundLanding = false;
@@ -338,12 +359,7 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
float solid = player->GetMap()->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
if (landing.Ground > solid + 0.7f) continue;
// water is no solid touchdown
Point bottom
{
x, y, landing.Ground + 1
}, top = bottom;
top.Z += record.Settings.Clearance;
if (! ClearRay(player, bottom, top)) continue;
if(!OpenColumn(player,x,y,landing,record.Settings.Clearance))continue;
Position candidate;
candidate.Relocate(x, y, landing.Ground, target.GetOrientation());
if (radius > 0 || std::fabs(landing.Ground - target.GetPositionZ()) > 0.7f)
@@ -364,8 +380,11 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
}
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
player->StopMoving();
sClientlessLuaEngine->DismountGroundMount(player, "free-flight-takeoff");
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
if(!airborne)
{
sClientlessLuaEngine->DismountGroundMount(player, "free-flight-takeoff");
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
}
record.Status.State = ClientlessFlightState::Mounting;
record.Status.MountSpell = spell;
record.Status.Detail = "waiting for native flying mount";
@@ -382,6 +401,17 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter());
}
}
if(airborne)
{
// A resumed hover is not a supported return/landing point. Use the
// verified ground below it only when the full descent column is clear.
FindReturnLanding(player,record);
record.Status.MountSpell=existingMount;record.Status.State=ClientlessFlightState::Flying;
record.Memory.Departed=player->GetPositionZ()-origin.Ground>record.Settings.Safety;
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RESUME_AIRBORNE guid=%u ground=%.3f z=%.3f",player->GetGUID().GetCounter(),origin.Ground,player->GetPositionZ());
_records[player->GetGUID().GetCounter()]=std::move(record);return true;
}
_records[player->GetGUID().GetCounter()] = std::move(record);
if (player->CastSpell(player, spell, false) != SPELL_CAST_OK)
{
@@ -393,8 +423,61 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
return true;
}
bool ClientlessFlightMgr::FindReturnLanding(Player* player,Record& record)
{
auto sample=[&](float x,float y){return Sample(player,record,x,y);};
auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);};
Point current=PointOf(*player);
for(float radius:{0.0f,8.0f,16.0f,24.0f,40.0f})for(unsigned i=0;i<(radius==0?1u:8u);++i)
{
float angle=float(i)*0.785398163f;
float x=current.X+radius*std::cos(angle),y=current.Y+radius*std::sin(angle);
auto surface=sample(x,y);Point goal{x,y,surface.Ground};
if(!surface.Valid || Distance(current,goal)>80 || !OpenColumn(player,x,y,surface))continue;
Position candidate;candidate.Relocate(x,y,surface.Ground,player->GetOrientation());
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidate) ||
!Corridor(current,goal,goal,record.Settings,sample,ray,false))continue;
record.Start=candidate;
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RETURN_LANDING guid=%u target=(%.3f %.3f %.3f)",player->GetGUID().GetCounter(),x,y,surface.Ground);
return true;
}
return false;
}
bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool following)
{
// Only called after a real support surface was confirmed at the feet.
// Walk out from under the upper floor; never raise the player through it.
float heading=std::atan2(record.Status.Target.GetPositionY()-player->GetPositionY(),record.Status.Target.GetPositionX()-player->GetPositionX());
for(float radius:{8.0f,16.0f,24.0f,40.0f})for(float turn:{0.0f,0.785398f,-0.785398f,1.570796f,-1.570796f,2.356194f,-2.356194f,3.141593f})
{
float x=player->GetPositionX()+radius*std::cos(heading+turn),y=player->GetPositionY()+radius*std::sin(heading+turn);
auto surface=Sample(player,record,x,y);
if(!surface.Valid)continue;
Position point;point.Relocate(x,y,surface.Ground,player->GetOrientation());
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,point))continue;
if(!OpenColumn(player,x,y,surface))continue;
ClientlessPathDiagnostics path;
if(!sClientlessTravelMgr->AnalyzePath(player,point,path) || !path.Calculated || !path.Complete || path.Suspicious ||
path.FinalDistance>1.5f || path.PathLength>80 || path.MaxSegmentLength>8 || path.MaxZDelta>4)continue;
if(!sClientlessTravelMgr->StartTravel(player,point,false))continue;
record.ApproachingDeparture=true;record.Departure=point;
record.Status.State=ClientlessFlightState::Mounting;record.Status.FollowingPlayer=following;
record.Status.Detail="walking to an open takeoff point";
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_DEPARTURE_WALK guid=%u destination=(%.3f %.3f %.3f) path=%.2f",player->GetGUID().GetCounter(),x,y,surface.Ground,path.PathLength);
_records[player->GetGUID().GetCounter()]=std::move(record);return true;
}
return false;
}
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
{
if(record.ApproachingDeparture)
{
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());record.ApproachingDeparture=false;
// No mount cast has started while walking; do not interrupt a combat spell.
record.Status.State=ClientlessFlightState::Failed;
}
record.Published->Aborted=true;
record.Status.FollowingPlayer=false;
player->StopMoving();
@@ -429,7 +512,9 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
if (Horizontal(current, goal) < 1.5f && std::fabs(current.Z - goal.Z) < 0.7f)
{
auto floor = sample(current.X, current.Y);
if (! floor.Valid || std::fabs(current.Z - floor.Ground) > 0.7f)
float feet=player->GetMap()->GetHeight(player->GetPhaseShift(),current.X,current.Y,current.Z+2,true,6);
if (!floor.Valid || !std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(current.Z-feet)>0.7f ||
!ClearRay(player,{current.X,current.Y,current.Z+0.8f},{current.X,current.Y,current.Z+4.5f}))
{
Fail(player, record, "touchdown ground not confirmed");
return false;
@@ -518,6 +603,20 @@ void ClientlessFlightMgr::Update(uint32 diff)
Fail(player, record, "flight player/map state changed");
continue;
}
if(record.ApproachingDeparture)
{
auto travel=sClientlessTravelMgr->GetStatus(guid);
if(player->IsInCombat() || record.Elapsed>120000 || !travel ||
(travel->State!=ClientlessTravelState::Moving && travel->State!=ClientlessTravelState::Arrived))
{Fail(player,record,"departure-unavailable: ground approach interrupted");continue;}
if(travel->State==ClientlessTravelState::Moving)continue;
if(player->GetExactDist(record.Departure)>2)
{Fail(player,record,"departure-unavailable: takeoff point not reached");continue;}
auto target=record.Status.Target;bool following=record.Status.FollowingPlayer;std::string error;
// Begin replaces this record on success; do not access old fields afterwards.
if(!Begin(player,target,error,following))Fail(player,record,error);
continue;
}
// Count actual motion, not overlapping issued spline lengths.
record.Status.Travelled += Distance(PointOf(record.LastPosition), PointOf(*player));
record.LastPosition.Relocate(player);
@@ -618,6 +717,13 @@ bool ClientlessFlightMgr::Command(ChatHandler * handler, char const * args)
Record probe;
auto surface = Sample(player, probe, x, y);
handler->PSendSysMessage("Flight surface map=%u x=%.3f y=%.3f valid=%u ground=%.3f ceiling=%.3f", player->GetMapId(), x, y, surface.Valid ? 1u : 0u, surface.Ground, surface.Ceiling);
auto map=player->GetMap();
float feet=map->GetHeight(player->GetPhaseShift(),x,y,player->GetPositionZ()+2.0f,true,6.0f);
LiquidData liquid{};
auto liquidStatus=map->GetLiquidStatus(player->GetPhaseShift(),x,y,player->GetPositionZ(),map_liquidHeaderTypeFlags::AllLiquids,&liquid,player->GetCollisionHeight());
handler->PSendSysMessage("Flight departure guid=%u position=(%.3f %.3f %.3f) nearFeet=%.3f rawWater=%.3f liquidStatus=%u liquidLevel=%.3f mounted=%u canFly=%u flying=%u falling=%u",
guid,player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),feet,map->GetWaterLevel(player->GetPhaseShift(),x,y),uint32(liquidStatus),liquid.level,
player->IsMounted()?1:0,player->CanFly()?1:0,player->IsFlying()?1:0,player->IsFalling()?1:0);
return true;
}
if (op == "test_node")
@@ -49,6 +49,8 @@ class TC_GAME_API ClientlessFlightMgr
Position Start, Segment, LastPosition;
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
bool Moving = false, Returning = false;
bool ApproachingDeparture = false;
Position Departure;
uint32 TargetUpdated = 0, FormationDelay = 0;
std::string AbortReason;
std::vector < Position > Breadcrumbs;
@@ -57,6 +59,8 @@ class TC_GAME_API ClientlessFlightMgr
ClientlessFlightPath::Surface Sample(Player * player, Record & record, float x, float y) const;
bool Advance(Player * player, Record & record, bool handoff = false);
bool Begin(Player* player, Position const& target, std::string& error, bool following);
bool ApproachDeparture(Player* player, Record& record, bool following);
bool FindReturnLanding(Player* player, Record& record);
bool AdvanceFollowing(Player* player, Record& record);
void Fail(Player * player, Record & record, std::string const & reason);
std::pair < uint32, uint32 > AreaZone(Player const * player, uint32 map, Position const & point) const;
@@ -113,6 +113,11 @@ namespace ClientlessFlightPath
if (! here.Valid || ! end.Valid || std::fabs(end.Ground - goal.Z) > 2.5f) return
{
};
// A short, low final descent need not open another horizontal approach.
// Validate the entire landing volume, including obstacles and terrain.
if (memory.Departed && from.Z>=goal.Z && from.Z-goal.Z<=12 &&
Distance(from,goal)<=cfg.Step && Corridor(from,goal,goal,cfg,sample,ray,false))
return {goal,true,false,true};
if (distance < 2 && std::fabs(from.Z - goal.Z) < 1.5f) return
{
goal, Corridor(from, goal, goal, cfg, sample, ray, true), false, true
@@ -589,7 +589,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
{
if (!flightEnabled || from.MapId != map)
if (!flightEnabled || from.MapId != map ||
(blockedBridgeKeys && blockedBridgeKeys->count("flight_origin:"+from.Key)))
return std::numeric_limits<float>::infinity();
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
@@ -721,6 +722,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
entryNav.FinalDistance <= 1.5f && entryNav.MaxSegmentLength <= 8.0f &&
entryNav.MaxZDelta <= 5.0f && entryNav.PathLength <= 500.0f;
ClientlessTravelNode departure;
departure.Key="flight_departure";
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
@@ -49,6 +49,7 @@
#include "PhasingHandler.h"
#include "PhaseShift.h"
#include "Map.h"
#include "GameObject.h"
#include "Spell.h"
#include "SpellMgr.h"
#include "Log.h"
@@ -1623,6 +1624,33 @@ static bool HandleClientlessTaxiInfo(ChatHandler* handler, char const* args)
return true;
}
static bool HandleClientlessPortalObjectUse(ChatHandler* handler, char const* args)
{
std::istringstream input(args ? args : "");
uint32 guid=0,spawn=0; std::string extra;
if (!(input>>guid>>spawn) || (input>>extra))
{ handler->SendSysMessage("Usage: clientless portal object use <onlineBotGuid> <spawnId> (native click; must be in range)"); return false; }
auto session=sWorld->FindClientlessSession(guid);
auto player=session?session->GetPlayer():nullptr;
if(!player || !player->IsInWorld() || !player->IsAlive() || player->IsBeingTeleported() || player->IsInFlight() || player->GetTransport())
{handler->SendSysMessage("Bot must be online, alive and safely on the ground.");return false;}
auto const& objects=player->GetMap()->GetGameObjectBySpawnIdStore();
auto range=objects.equal_range(spawn);
for(auto itr=range.first;itr!=range.second;++itr)
{
auto object=itr->second;
if(!object || object->GetGoType()!=GAMEOBJECT_TYPE_SPELLCASTER || !object->isSpawned() ||
!player->IsInPhase(object) || !player->CanSeeOrDetect(object) ||
object->HasFlag(GAMEOBJECT_FLAGS,GO_FLAG_NOT_SELECTABLE|GO_FLAG_LOCKED) ||
player->GetGameObjectIfCanInteractWith(object->GetGUID())!=object)continue;
WorldPacket packet(CMSG_GAMEOBJ_USE,8);packet<<object->GetGUID();
session->HandleGameObjectUseOpcode(packet);
handler->PSendSysMessage("Native portal click sent: bot=%u spawn=%u entry=%u. Check teleport completion and cast diagnostics.",guid,spawn,object->GetEntry());
return true;
}
handler->SendSysMessage("No visible, unlocked spellcaster object in native interaction range.");return false;
}
static bool HandleClientlessPortalObjectImport(ChatHandler* handler, char const* args)
{
std::istringstream input(args ? args : "");
@@ -2578,6 +2606,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
{ "import", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalObjectImport, "" },
{ "use", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalObjectUse, "" },
} },
{ "import", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalImport, "" },
{ "info", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalInfo, "" },
@@ -0,0 +1,62 @@
"""Execute the real five-minute gathering handler with deterministic travel."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text();a=s.index('bool AdventureBehaviorMgr::UpdateGathering(');b=s.index('\nvoid AdventureBehaviorMgr::Update(',a)
code=r'''
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <ctime>
#include <string>
#include <cmath>
#include <iostream>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
struct Position{float x=0;};struct Guid{unsigned GetCounter(){return 26;}};
struct Player{float x=50;unsigned map=0;bool combat=false;unsigned stopped=0;Guid GetGUID(){return{};}unsigned GetMapId(){return map;}
float GetExactDist(Position const&p){return std::fabs(x-p.x);}void StopMoving(){++stopped;}};
bool Unsafe(Player*p){return p->combat;}
enum class ClientlessTravelState{Moving,Arrived,Rejected};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Rejected;}status;unsigned cancels=0;auto GetStatus(unsigned){return &status;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}}travel;
auto sClientlessTravelMgr=&travel;
struct AdventureBehaviorMgr{enum class Phase{Travel,OnSite};struct Record{
std::string Intent="groupformation",Micro="profession_tools",Detail;time_t Deadline=400,GatherRetryAt=0;
bool Gathering=false,Dirty=false;Phase State=Phase::OnSite;unsigned AnchorMap=0,MicroTimer=20000;Position Anchor;
struct{bool Active=false,StopRequested=false;}Leisure;
struct{bool Active=true;}Social;struct{bool Active=false;}Recovery;
};unsigned moves=0,resets=0;bool reachable=true;
bool UpdateGathering(Player*,Record&,time_t);
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();r.MicroTimer=0;r.Social.Active=r.Recovery.Active=false;}
bool LocalMove(Player*,Record&,Position const&target,float budget){assert(target.x==0&&budget==500);++moves;if(reachable)travel.status.State=ClientlessTravelState::Moving;return reachable;}
};
'''+s[a:b]+r'''
int main(){
using AdventureGroupPolicy::Gathering;
assert(!Gathering("groupformation",401,100));assert(Gathering("groupformation",400,100));
assert(Gathering("groupformation",400,399));assert(!Gathering("groupformation",400,400));
assert(!Gathering("groupformation",400,401));assert(!Gathering("groupformation",0,100));assert(!Gathering("leisure",400,100));
Player p;AdventureBehaviorMgr m;AdventureBehaviorMgr::Record r;
assert(!m.UpdateGathering(&p,r,99)&&m.moves==0);
r.Leisure.Active=true;assert(!m.UpdateGathering(&p,r,100));
assert(r.Gathering&&r.Leisure.StopRequested&&r.MicroTimer==0&&m.resets==0&&m.moves==0);
r.Leisure.Active=false;assert(m.UpdateGathering(&p,r,101));
assert(m.moves==1&&m.resets==1&&!r.Social.Active&&r.Micro.empty());
assert(m.UpdateGathering(&p,r,102)&&m.moves==1);
travel.status.State=ClientlessTravelState::Rejected;m.reachable=false;
assert(m.UpdateGathering(&p,r,105)&&m.moves==1);assert(m.UpdateGathering(&p,r,111)&&m.moves==2);
p.x=0;travel.status.State=ClientlessTravelState::Moving;
assert(m.UpdateGathering(&p,r,112)&&p.stopped==1&&travel.cancels==1);
assert(r.Detail=="waiting at group meeting point before departure");
auto n=m.moves;assert(m.UpdateGathering(&p,r,399)&&m.moves==n); // hold, no new action
assert(!m.UpdateGathering(&p,r,400)&&!r.Gathering&&r.GatherRetryAt==0); // deadline never waits for members
r={};r.State=AdventureBehaviorMgr::Phase::Travel;assert(!m.UpdateGathering(&p,r,100)&&!r.Gathering);
r.State=AdventureBehaviorMgr::Phase::OnSite;r.Dirty=true;assert(!m.UpdateGathering(&p,r,100));
r.Dirty=false;p.combat=true;assert(!m.UpdateGathering(&p,r,100));
p.combat=false;r.Intent="instance";assert(!m.UpdateGathering(&p,r,100));
std::cout<<"Production five-minute gathering: exact cutoff, safe corridor return request, preparation cancellation, bounded retries, meeting hold and deadline release passed\n";
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
+10 -1
View File
@@ -58,14 +58,23 @@ struct Location{std::string Name="Stormwind";struct{unsigned MapId=0;float X=0,Y
struct Locations{bool levelEligible=true;Location*Find(std::string const&){return &location;}bool LevelEligible(Player*,Location const&,std::string const&,bool){return levelEligible;}}locations;auto sAdventureLocationMgr=&locations;
struct Areas{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct GroupTravel {unsigned follows=0;bool Follow(Player*){++follows;return true;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
int messages=0;
void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;}
'''
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=r'''
int main(){
using namespace AdventureGroupPolicy;
{AdventureGroupMgr check;Player leader,member;leader.guid={901};member.guid={902};Group party;party.leader=leader.guid;leader.group=member.group=&party;
ObjectAccessor::players[leader.guid]=&leader;ObjectAccessor::players[member.guid]=&member;
behavior.plans[leader.guid.id]={true,true,"groupformation","capital","instance","target"};
assert(!check.FollowMember(&member)&&groupTravel.follows==0);
behavior.plans[leader.guid.id]={true,false,"instance","target"};
assert(check.FollowMember(&member)&&groupTravel.follows==1);
ObjectAccessor::players.clear();behavior.plans.clear();}
AdventureBehaviorPolicy::Day day;std::mt19937 random(42);
for(unsigned i=0;i<24;++i)day[i]={i%2?"instance":"social",std::to_string(i)};
Prepare(day,random,1,[]{return std::string("capital");});
@@ -70,6 +70,7 @@ public:
bool Allowed(Player*,AdventureLocation const&,std::string const&){return true;}
bool AdoptGroup(Player*,Player*){return false;}
bool UpdateNavigationRescue(Player*,Record&){return false;}
bool UpdateGathering(Player*,Record&,time_t){return false;}
void Update(Player*,uint32);
void EnterPlayerInstance(Player*);
void Generate(Player*,Record&,uint32){}
+11 -5
View File
@@ -10,6 +10,7 @@ def function(signature):
code=r'''
#include <algorithm>
#include "AdventureInstancePolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cmath>
#include <cstdint>
@@ -18,6 +19,7 @@ code=r'''
#include <string>
#include <vector>
#include <unordered_set>
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
using uint32=uint32_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;
@@ -31,8 +33,8 @@ struct Position{
};
struct PositionFullTerrainStatus{bool outdoors=true;};
struct Terrain{
bool outdoors=true;uint32 zone=1519;
void GetFullTerrainStatusForPosition(int,uint32,float,float,float,PositionFullTerrainStatus& s){s.outdoors=outdoors;}
bool outdoors=true;float outdoorAfter=1e9;uint32 zone=1519;
void GetFullTerrainStatusForPosition(int,uint32,float x,float y,float,PositionFullTerrainStatus& s){s.outdoors=outdoors || std::hypot(x,y)>outdoorAfter;}
void GetZoneAndAreaId(int,uint32,uint32& z,uint32& a,Position const&){z=zone;a=1;}
}terrain;
struct Map{float water=-1000,ground=0;Terrain* GetTerrain(){return &terrain;}float GetHeight(int,float,float,float,bool,float){return ground;}float GetWaterLevel(int,float,float){return water;}}map;
@@ -219,10 +221,14 @@ int main(){
combatMgr.available=true;assert(mgr.LeisureAvailable(&p,r,{"training"}));
assert(mgr.StartLeisure(&p,r,{"training"}) && combatMgr.starts==1);
r.Intent="adventure";assert(!mgr.LeisureAvailable(&p,r,{"training"}));
// Recruitment walks stay near the meeting point, also inside a tavern.
mgr.ResetLeisure(&p,r);r.Intent="groupformation";r.MicroTimer=45000;terrain.outdoors=false;p.outdoors=false;PathGenerator::bend=2;
// Recruitment walks leave the inn, stay in the capital and retrace the route.
mgr.ResetLeisure(&p,r);r.Intent="groupformation";r.MicroTimer=180000;terrain.outdoors=false;terrain.outdoorAfter=40;p.outdoors=false;PathGenerator::bend=2;
assert(mgr.StartLeisure(&p,r,{"walk"}));
for(auto const& point:r.Leisure.Route)assert(point.GetExactDist(r.Leisure.Origin)<=18);
assert(r.Leisure.Origin.GetExactDist(r.Leisure.Route[r.Leisure.OutboundSteps-1])>=65);
for(auto const& point:r.Leisure.Route)assert(point.GetExactDist(r.Leisure.Origin)<=250);
mgr.ResetLeisure(&p,r);terrain.outdoorAfter=1e9;assert(!mgr.StartLeisure(&p,r,{"walk"}));
terrain.outdoorAfter=40;assert(mgr.StartLeisure(&p,r,{"walk"}));
r.Deadline=GameTime::now+300;assert(!mgr.LeisureAvailable(&p,r,{"walk"}));r.Deadline=GameTime::now+301;assert(mgr.LeisureAvailable(&p,r,{"walk"}));r.Deadline=0;
assert(!mgr.LeisureAvailable(&p,r,{"ride"}));mgr.ResetLeisure(&p,r);terrain.outdoors=true;PathGenerator::bend=20;
r.RetryTimer=30000;r.Tried.insert("old");mgr.SetWanted(&p,r,"rest","new",1234);assert(r.RetryTimer==0&&r.Tried.empty()&&r.Deadline==1234);
+16 -3
View File
@@ -57,7 +57,7 @@ struct Player:Unit{
ObjectGuid selection;
bool visible=true;bool CanSeeOrDetect(GameObject*){return visible;}
NativeMap* GetMap(){return &nativeMap;}int GetPhaseShift(){return 0;}
bool IsBeingTeleported(){return false;}bool IsSitState(){return sit;}bool IsNonMeleeSpellCast(bool){return casting;}
bool IsInWater(){return false;}bool IsFalling(){return false;}bool IsBeingTeleported(){return false;}bool IsSitState(){return sit;}bool IsNonMeleeSpellCast(bool){return casting;}
float GetExactDist(Position const&){return distance;}float GetExactDist(WorldObject*){return distance;}void SetFacingToObject(WorldObject*){++faces;}
std::vector<float> headings;void SetFacingTo(float f){heading=f;headings.push_back(f);}int chairStand=5;uint32 GetStandState(){return sit?chairStand:0;}
std::vector<int> states;void SetSelection(ObjectGuid g){selection=g;}void SetUInt32Value(int,int state){states.push_back(state);}
@@ -90,9 +90,14 @@ void Say(Player*,std::string const&,AdventureSocialContext&){}
uint32 Reaction(AdventureSocialContext&){return 11;}
enum class ClientlessTravelState{Moving,Arrived,Rejected};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
struct AdventureProfessionTargets{static bool Valid(Player*,WorldObject*,std::string const&,AdventureSocialContext const&){return true;}};
class AdventureSocialActions{public:static bool Update(Player*,AdventureSocialScene&,uint32,AdventureSocialContext&);};
struct AdventureProfessionTargets{static std::vector<Unit*> Find(Player*,std::string const&,AdventureSocialContext const&){return {};}static bool Valid(Player*,WorldObject*,std::string const&,AdventureSocialContext const&){return true;}};
struct AdventureMicroAction{std::string Requirement;};
class AdventureSocialActions{public:static bool Available(Player*,AdventureMicroAction const&,AdventureSocialContext&);static bool Update(Player*,AdventureSocialScene&,uint32,AdventureSocialContext&);};
'''.replace('Sequence Sequence;', 'AdventureSocialFlow::Sequence Sequence;')
# Compile the actual profession eligibility branch; unrelated object selectors are outside this fixture.
a=source.index('bool AdventureSocialActions::Available(')
b=source.index(' if (AdventureExplorationPolicy::Inspection(kind))',a)
code+=source[a:b]+' return false;\n}\n'
code+=function('bool OnChair(')+'\n'+function('bool AtFront(')+'\n'+function('std::vector < Step > Steps(')+'\n'+function('bool AdventureSocialActions::Update(')
code+=r'''
int main(){
@@ -193,6 +198,14 @@ int main(){
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active&&p.headings.size()==2&&std::fabs(p.headings[0]-0.45f)<0.001&&std::fabs(p.headings[1]-1.95f)<0.001);
// Group preparation reuses equipment checks but cannot perform profession work.
c.Intent="groupformation";c.PrimaryProfessions={"mining"};
assert(AdventureSocialActions::Available(&p,{"profession_tools"},c));
assert(!AdventureSocialActions::Available(&p,{"work_mining"},c));
s={};s.Kind="profession_tools";s.Target={};s.Approaching=false;s.Sequence.Start(Steps(s.Kind,20000));p.emotes.clear();
assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(std::find(p.emotes.begin(),p.emotes.end(),EMOTE_ONESHOT_USE_STANDING)!=p.emotes.end());
// Profession gestures wait for arrival and never invoke use/cast handlers.
c.Intent="profession";c.PrimaryProfessions={"mining","blacksmithing"};
target=&chair;p.distance=2;p.visible=p.los=true;p.states.clear();
+129
View File
@@ -0,0 +1,129 @@
"""Exercise real departure admission/ground relocation against layered terrain."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
a=s.index('uint32 existingMount=0;');b=s.index(' bool foundLanding',a)
admission=s[a:b]
a=s.index('bool ClientlessFlightMgr::ApproachDeparture(');b=s.index('\nvoid ClientlessFlightMgr::Fail(',a)
approach=s[a:b]
a=s.index(' bool OpenColumn(');b=s.index(' char const * StateName',a)
column=s[a:b]
a=s.index(' if(!airborne)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a)
dismount=s[a:b]
a=s.index(' auto floor = sample(current.X, current.Y);');b=s.index(' player->StopMoving();',a)
touchdown=s[a:b]
a=s.index(' FindReturnLanding(player,record);');b=s.index(' record.Status.MountSpell=existingMount;',a)
return_anchor=s[a:b]
a=s.index('bool ClientlessFlightMgr::FindReturnLanding(');b=s.index('bool ClientlessFlightMgr::ApproachDeparture(',a)
find_return=s[a:b]
code=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <string>
#include <unordered_map>
#include <cstdint>
using uint32=unsigned;using namespace ClientlessFlightPath;
#define TC_LOG_WARN(...) ((void)0)
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;constexpr unsigned SPELL_AURA_MOUNTED=1;
struct Position{float x=0,y=0,z=0,o=0;
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
struct Guid{unsigned GetCounter()const{return 84;}};
struct Map{float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map;
struct Player:Position{bool mounted=false,fly=false,aura=false;int removals=0;
Guid GetGUID(){return {};}unsigned GetMapId(){return 530;}int GetPhaseShift(){return 0;}Map* GetMap(){return &map;}
bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;}
void RemoveAurasByType(unsigned){++removals;mounted=fly=false;}};
Point PointOf(Position const&p){return {p.x,p.y,p.z};}
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Record{struct{Position Target;ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;std::string Detail;}Status;
bool ApproachingDeparture=false;Position Departure,Start;ClientlessFlightPath::Settings Settings;};
struct ClientlessPathDiagnostics{bool Calculated=true,Complete=true,Suspicious=false;float FinalDistance=0,PathLength=15,MaxSegmentLength=4,MaxZDelta=1;};
struct Travel{ClientlessPathDiagnostics nav;bool start=true;int starts=0,probes=0;
bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics& d){++probes;d=nav;return true;}
bool StartTravel(Player*,Position const&,bool){++starts;return start;}}travel;
auto sClientlessTravelMgr=&travel;
struct Areas{bool allowed=true;bool AllowsSegment(Player*,unsigned,Position const&,Position const&){return allowed;}}areas;
auto sAdventureAreaMgr=&areas;
struct Lua{int calls=0;void DismountGroundMount(Player*,char const*){++calls;}}lua;
auto sClientlessLuaEngine=&lua;
bool clear=true,narrow=false,columnBlocked=false;
bool ClearRay(Player*,Point const&a,Point const&b){
if(!clear||(narrow&&std::fabs(a.Y)>=0.01f))return false;
if(columnBlocked)for(int i=0;i<=80;++i){auto p=Mix(a,b,i/80.f);if(std::fabs(p.X)<1&&std::fabs(p.Y)<1&&p.Z>3&&p.Z<5)return false;}
return true;
}
'''+column+r'''
struct ClientlessFlightMgr{
std::unordered_map<unsigned,Record>_records;Surface surface{0,300,true};int approached=0;bool seek=false;
Surface Sample(Player*,Record&,float,float){return surface;}
bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&);
bool Admit(Player* player,Record&record,std::string&error,bool following=false){
auto origin=surface;std::vector<std::pair<unsigned,unsigned>>candidates{{123,1}};
'''+admission+r'''
return true;
}
void ReturnAnchor(Player*player,Record&record){auto origin=surface;
'''+return_anchor+r'''
}
void Fail(Player*,Record&,std::string const&){}
bool Touchdown(Player*player){auto current=PointOf(*player);Record record;auto sample=[&](float,float){return surface;};
'''+touchdown+r'''
return true;
}
void KeepMount(Player*player,bool airborne){
'''+dismount+r'''
}
};
'''+find_return+approach+r'''
int main(){
ClientlessFlightMgr mgr;Player p;Record r;std::string error;r.Status.Target={100,0,0};
// A normal supported open-air departure remains valid.
assert(mgr.Admit(&p,r,error));
// Brynli: a native flying mount already suspended ten yards above terrain.
p.z=10;p.mounted=p.fly=p.aura=true;map.feet=INVALID_HEIGHT;
assert(mgr.Admit(&p,r,error));assert(mgr._records.empty());
r.Start=p;mgr.ReturnAnchor(&p,r);assert(r.Start.z==0);
narrow=true;r.Start=p;mgr.ReturnAnchor(&p,r);assert(r.Start.z==10);narrow=false;
columnBlocked=true;r.Start=p;mgr.ReturnAnchor(&p,r);assert(r.Start.x>1&&r.Start.z==0&&p.x==0&&p.z==10);
areas.allowed=false;r.Start=p;mgr.ReturnAnchor(&p,r);assert(r.Start.z==10);areas.allowed=true;columnBlocked=false;
mgr.KeepMount(&p,true);assert(p.mounted&&p.fly&&p.removals==0&&lua.calls==0);
// Airborne recovery never applies to an unmounted/ground-only bot.
p.fly=false;assert(!mgr.Admit(&p,r,error));assert(error.find("departure-unavailable:")==0);
p.fly=true;p.aura=false;assert(!mgr.Admit(&p,r,error));p.aura=true;
clear=false;assert(!mgr.Admit(&p,r,error));clear=true;
mgr.surface.Ceiling=12;assert(!mgr.Admit(&p,r,error));mgr.surface.Ceiling=300;
// Tholgar: grounded on a lower layer, while the sky sample sees an overhead floor.
p.z=0;p.mounted=p.fly=p.aura=false;map.feet=0;mgr.surface.Ground=12;
assert(mgr.Admit(&p,r,error));auto saved=mgr._records.at(84);
assert(saved.ApproachingDeparture&&saved.Status.State==ClientlessFlightState::Mounting);
assert(saved.Status.Target.x==100&&p.x==0&&p.z==0&&travel.starts==1);
// A roof, forbidden ground corridor, partial MMAP path, cliff or failed movement
// must never manufacture a departure or move the player through the obstruction.
mgr._records.clear();travel.starts=0;clear=false;
assert(!mgr.Admit(&p,r,error)&&mgr._records.empty()&&travel.starts==0);clear=true;
narrow=true;assert(!mgr.Admit(&p,r,error));narrow=false;
areas.allowed=false;assert(!mgr.Admit(&p,r,error));areas.allowed=true;
travel.nav.Complete=false;assert(!mgr.Admit(&p,r,error));travel.nav.Complete=true;
travel.nav.MaxZDelta=20;assert(!mgr.Admit(&p,r,error));travel.nav.MaxZDelta=1;
travel.nav.PathLength=200;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15;
travel.start=false;assert(!mgr.Admit(&p,r,error));travel.start=true;
map.feet=INVALID_HEIGHT;assert(!mgr.Admit(&p,r,error));
// A touchdown may use the actual lower floor, never a roof above it.
map.feet=0;p.z=0;mgr.surface.Ground=12;
assert(OpenColumn(&p,0,0,{0,300,true}));narrow=true;assert(!OpenColumn(&p,0,0,{0,300,true}));narrow=false;
assert(!OpenColumn(&p,0,0,{0,9,true}));assert(!OpenColumn(&p,0,0,{0,300,false}));
assert(mgr.Touchdown(&p));
p.z=1.2f;assert(!mgr.Touchdown(&p));p.z=0;
map.feet=INVALID_HEIGHT;assert(!mgr.Touchdown(&p));map.feet=0;
clear=false;assert(!mgr.Touchdown(&p));clear=true;
mgr.KeepMount(&p,false);assert(p.removals==1&&lua.calls==1);
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-departure-') as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production flight admission: airborne mount retained, lower-floor MMAP departure, blocked/cliff/missing support rejected.')
+15 -3
View File
@@ -21,6 +21,7 @@ struct Position {
float x=0,y=0,z=60;
float GetPositionX() const{return x;} float GetPositionY() const{return y;} float GetPositionZ() const{return z;}
void Relocate(Position const* p){*this=*p;}
float GetExactDist(Position const&p){return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);}
};
Point PointOf(Position const& p){return {p.x,p.y,p.z};}
struct Spline {int remaining=500; int timeRemaining()const{return remaining;}};
@@ -42,17 +43,21 @@ struct World {
auto sWorld=&world;
enum class ClientlessFlightState {Mounting,Flying,Arrived,Failed};
struct Record {
struct {bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0};float Travelled=0;} Status;
struct {bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0},Target{800,0,0};float Travelled=0;} Status;
Position LastPosition,Start,Segment{25,0,60};Settings Settings;
std::shared_ptr<int> Following;uint32 FormationDelay=0,TargetUpdated=0,Elapsed=0,SegmentElapsed=0,CheckTimer=0;
bool Moving=true,Returning=false;
bool Moving=true,Returning=false,ApproachingDeparture=false;Position Departure;
};
enum class ClientlessTravelState {Moving,Arrived,Stuck};
struct Travel {struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;
auto sClientlessTravelMgr=&travel;
bool clear=true;
bool ClearRay(Player*,Point const&,Point const&){return clear;}
class ClientlessFlightMgr {
public:
std::unordered_map<uint32,Record> _records;
int advances=0,handoffs=0,failures=0;
int advances=0,handoffs=0,failures=0,begins=0;
bool Begin(Player*,Position const&,std::string&,bool){++begins;return true;}
Surface Sample(Player*,Record&,float,float){return {0,300,true};}
void Fail(Player*,Record& r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;}
bool Advance(Player*,Record&,bool handoff=false){++advances;handoffs+=handoff;return true;}
@@ -83,6 +88,13 @@ int main(){
// A completed spline continues via the normal non-handoff path.
clear=true;r.CheckTimer=0;mgr.Update(50);assert(mgr.advances==3);
player.fly=false;mgr.Update(50);assert(mgr.failures==1);
// A ground departure approach must wait for actual arrival before mounting.
r.Status.State=ClientlessFlightState::Mounting;r.ApproachingDeparture=true;
r.Elapsed=0;r.Departure=player;travel.status.State=ClientlessTravelState::Moving;
mgr.Update(50);assert(mgr.begins==0&&mgr.failures==1);
travel.status.State=ClientlessTravelState::Arrived;mgr.Update(50);assert(mgr.begins==1);
// A stale Arrived flag cannot permit mounting away from the selected spot.
r.Departure.x=player.x+10;mgr.Update(50);assert(mgr.failures==2&&mgr.begins==1);
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-handoff-') as folder:
+3 -1
View File
@@ -61,8 +61,9 @@ struct Areas{bool known=true;auto LookupEntry(uint32){return known?&area:nullptr
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Record{std::shared_ptr<ClientlessFlightPath::SharedTrail>Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
struct{bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status;
bool Moving=true,Returning=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
bool Moving=true,Returning=false,ApproachingDeparture=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
};
struct Travel{int cancels=0;void CancelTravel(uint32){++cancels;}}travel;auto sClientlessTravelMgr=&travel;
class ClientlessFlightMgr{public:
Player const* _planningPlayer=nullptr;bool _planningEnabled=false;float _planningMinDistance=600;
std::unordered_set<uint32> _planningPreferredZones;
@@ -122,6 +123,7 @@ int main(){
profile.CanFreeFly=false;assert(!mgr.Enabled(&p));
mgr.BeginPlanning();assert(!std::isfinite(mgr.Estimate(&p,0,a,b)));
profile.CanFreeFly=true;
int beforeInterrupts=p.interrupts;Record approach;approach.ApproachingDeparture=true;approach.Status.State=ClientlessFlightState::Mounting;mgr.Fail(&p,approach,"interrupted");assert(travel.cancels==1&&!approach.ApproachingDeparture&&approach.Status.State==ClientlessFlightState::Failed&&p.interrupts==beforeInterrupts);
Record revoked;p.canFly=false;mgr.Fail(&p,revoked,"permission lost");assert(revoked.Status.State==ClientlessFlightState::Failed&&!revoked.Returning);
}
'''
+5
View File
@@ -23,6 +23,11 @@ Run simulate(Point start,Point target,Sample sample,Ray ray,int limit=500){
int main(){
auto flat=[](float,float){return Surface{0,300,true};};
auto clear=[](Point const&,Point const&){return true;};
Memory finalApproach;finalApproach.Departed=true;
auto touchdown=Next({6,0,10},{0,0,0},Settings{},finalApproach,flat,clear);
assert(touchdown.Valid&&touchdown.Landing&&Distance(touchdown.Next,{0,0,0})<0.01f);
auto solidWall=[](Point const&,Point const&){return false;};
assert(!Next({6,0,10},{0,0,0},Settings{},finalApproach,flat,solidWall).Valid);
auto normal=simulate({0,0,0},{800,0,0},flat,clear);
assert(normal.Arrived&&normal.Peak>30&&normal.Peak<=60.1f);
auto low=[](float,float){return Surface{0,42,true};};
@@ -151,6 +151,13 @@ assert(route.size()==3&&route.front().NodeKey=="flight_departure"&&route[1].Node
std::unordered_set<std::string> deniedFlightRoots={"flight:flight_departure->A"};
route.clear();
assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
// Reject all flight targets from a failed physical departure, not just one target.
deniedFlightRoots={"flight_origin:flight_departure"};route.clear();
assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
// The same origin restriction still allows a real local ground approach.
travel.complete=true;route.clear();
assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
assert(route.front().NodeKey!="flight_departure");
travel.complete=true;
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
}
@@ -0,0 +1,38 @@
"""Check the scoped portal condition migration and preserve the native level gate."""
from pathlib import Path
import sqlite3
root = Path(__file__).resolve().parents[2]
sql = (root / 'sql/custom/world/2026_10_29_03_blasted_lands_portal_conditions.sql').read_text()
db = sqlite3.connect(':memory:')
columns = ['SourceTypeOrReferenceId','SourceGroup','SourceEntry','SourceId',
'ConditionTypeOrReference','ConditionTarget','ConditionValue1',
'ConditionValue2','NegativeCondition']
db.execute('CREATE TABLE conditions (' + ','.join(c+' INTEGER' for c in columns) + ')')
rows = [(17,0,65728,0,27,0,58,3,0), (17,0,65729,0,27,0,58,3,0),
(17,0,17334,0,27,0,58,3,0), (17,0,65728,0,27,0,60,3,0),
(17,0,65729,0,27,0,58,3,1), (13,0,65728,0,27,0,58,3,0)]
db.executemany('INSERT INTO conditions VALUES (?,?,?,?,?,?,?,?,?)',rows)
db.executescript(sql)
result = db.execute('SELECT * FROM conditions').fetchall()
assert result[0][5] == result[1][5] == 1
assert result[2:] == rows[2:]
for row in result[:2]:
assert row[6:8] == (58,3), 'Native minimum level and comparison must survive'
db.executescript(sql)
assert db.execute('SELECT * FROM conditions').fetchall() == result
print('Portal condition migration: explicit user target, preserved level gate, scoped updates and idempotence passed.')
# Only named public static portals change caster; preserve summoned/quest data.
db.execute('CREATE TABLE gameobject_template(entry INTEGER,type INTEGER,data2 INTEGER,data6 INTEGER)')
rows2=[(183325,22,0,0),(195142,22,0,0),(209538,22,0,0),(210176,22,0,0),
(176296,22,1,0),(205268,22,0,0),(183325,22,1,0),(183325,10,0,0)]
db.executemany('INSERT INTO gameobject_template VALUES(?,?,?,?)',rows2)
public_sql=(root/'sql/custom/world/2026_10_29_04_public_portal_player_cast.sql').read_text()
db.executescript(public_sql)
after=db.execute('SELECT * FROM gameobject_template').fetchall()
assert all(r[3]==1 for r in after[:4])
assert after[4:]==rows2[4:]
db.executescript(public_sql)
assert db.execute('SELECT * FROM gameobject_template').fetchall()==after
print('Public portal caster migration: static allowlist, party/type guards and idempotence passed.')
+2 -1
View File
@@ -200,7 +200,7 @@ b = movement_source.index('void WorldSession::CompleteNearTeleport(', a)
ack_harness = r'''
#include <cassert>
#include <cstdint>
struct Player {bool pending=false,far=false,world=true;bool IsBeingTeleported(){return pending;}bool IsBeingTeleportedFar(){return pending&&far;}bool IsBeingTeleportedNear(){return pending&&!far;}bool IsInWorld(){return world;}};
struct Player {bool pending=false,far=false,world=true,delayed=false;bool IsHasDelayedTeleport(){return delayed;}bool IsBeingTeleported(){return pending;}bool IsBeingTeleportedFar(){return pending&&far;}bool IsBeingTeleportedNear(){return pending&&!far;}bool IsInWorld(){return world;}};
struct WorldSession {
Player* player;bool clientless=true,works=true;int near=0,far=0;uint64_t _clientlessTeleportSequence=0;
bool IsClientless(){return clientless;}Player* GetPlayer(){return player;}
@@ -213,6 +213,7 @@ ack_harness += movement_source[a:b]
ack_harness += r'''
int main(){
Player p;WorldSession s{&p};s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==0);
p.pending=p.delayed=true;s.CompleteClientlessTeleport();assert(s.near==0&&s._clientlessTeleportSequence==0);p.delayed=false;
p.pending=true;s.CompleteClientlessTeleport();assert(s.near==1&&s._clientlessTeleportSequence==1);
s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==1);
p.pending=p.far=true;p.world=false;s.CompleteClientlessTeleport();assert(s.far==1&&s._clientlessTeleportSequence==2);