diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index e4939ed0fe..b43bd257b7 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -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 assertion unresolved. Earlier checkpoints below historic. + +# Latest: crash investigation 16:19 + +Server aborted SIGABRT16:19:22; libstdc++ const vector::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 diff --git a/doc/clientless/blasted-lands-portals.md b/doc/clientless/blasted-lands-portals.md new file mode 100644 index 0000000000..343b6334bd --- /dev/null +++ b/doc/clientless/blasted-lands-portals.md @@ -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`. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/before-probes.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/before-probes.log new file mode 100644 index 0000000000..7a482d89b7 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/before-probes.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 diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/final-live.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/final-live.log new file mode 100644 index 0000000000..0d846d9f61 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/final-live.log @@ -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' diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test-results.json b/doc/clientless/checkpoints/2026-10-29-flight-departure/test-results.json new file mode 100644 index 0000000000..b7a4aab2d3 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test-results.json @@ -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 +} \ No newline at end of file diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_area_query.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_area_query.log new file mode 100644 index 0000000000..c45af5ec3a --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_area_query.log @@ -0,0 +1 @@ +Native terrain query map/phase, area/zone ordering and cache passed. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_departure.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_departure.log new file mode 100644 index 0000000000..d341e74219 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_departure.log @@ -0,0 +1 @@ +Production flight admission: airborne mount retained, lower-floor MMAP departure, blocked/cliff/missing support rejected. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_handoff.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_handoff.log new file mode 100644 index 0000000000..12538ff1e3 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_handoff.log @@ -0,0 +1 @@ +Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_policy.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_policy.log new file mode 100644 index 0000000000..90f68a0a2b --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_policy.log @@ -0,0 +1 @@ +Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_terrain_fallback.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_terrain_fallback.log new file mode 100644 index 0000000000..818c3722db --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_flight_terrain_fallback.log @@ -0,0 +1 @@ +Missing phase tile uses native base terrain; an existing phase tile with missing collision stays forbidden diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight.log new file mode 100644 index 0000000000..fed22fb4eb --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight.log @@ -0,0 +1 @@ +Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors. diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight_planner.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight_planner.log new file mode 100644 index 0000000000..f1c6539cbe --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_free_flight_planner.log @@ -0,0 +1 @@ +ground planner regressions and dynamic flight/node/location/virtual-exit integration passed diff --git a/doc/clientless/checkpoints/2026-10-29-flight-departure/test_group_flight_follow.log b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_group_flight_follow.log new file mode 100644 index 0000000000..f69a883e14 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-flight-departure/test_group_flight_follow.log @@ -0,0 +1 @@ +Moving-target flight: bounded phased corridors, obstacle hold, ceiling rejection, aerial formation hold, safe landing and return fallback passed diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/live-gather.log b/doc/clientless/checkpoints/2026-10-29-group-gather/live-gather.log new file mode 100644 index 0000000000..d94eed9acd --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/live-gather.log @@ -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 diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test-results.json b/doc/clientless/checkpoints/2026-10-29-group-gather/test-results.json new file mode 100644 index 0000000000..9e5aedff48 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test-results.json @@ -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 +} diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_gather.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_gather.log new file mode 100644 index 0000000000..9d6c2094f9 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_gather.log @@ -0,0 +1 @@ +Production five-minute gathering: exact cutoff, safe corridor return request, preparation cancellation, bounded retries, meeting hold and deadline release passed diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_travel.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_travel.log new file mode 100644 index 0000000000..3eb62f5522 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_group_travel.log @@ -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 diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_groups.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_groups.log new file mode 100644 index 0000000000..83db5db447 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_groups.log @@ -0,0 +1 @@ +Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed. diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_intent_state.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_intent_state.log new file mode 100644 index 0000000000..ff47137737 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_intent_state.log @@ -0,0 +1 @@ +Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed. diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_leisure.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_leisure.log new file mode 100644 index 0000000000..01ee3be2fc --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_leisure.log @@ -0,0 +1 @@ +Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed. diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_profession_sightline.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_profession_sightline.log new file mode 100644 index 0000000000..0cc6cdefa1 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_profession_sightline.log @@ -0,0 +1 @@ +Native workstation working-surface sightline and blocked geometry rejection passed. diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_social.log b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_social.log new file mode 100644 index 0000000000..cbcffc4869 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/test_adventure_social.log @@ -0,0 +1 @@ +Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed. diff --git a/doc/clientless/checkpoints/2026-10-29-group-gather/validation.md b/doc/clientless/checkpoints/2026-10-29-group-gather/validation.md new file mode 100644 index 0000000000..204db5773e --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-group-gather/validation.md @@ -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. diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/level-fix-evidence.log b/doc/clientless/checkpoints/2026-10-29-public-portals/level-fix-evidence.log new file mode 100644 index 0000000000..5d9ecc2a43 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/level-fix-evidence.log @@ -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 diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/live-probes.json b/doc/clientless/checkpoints/2026-10-29-public-portals/live-probes.json new file mode 100644 index 0000000000..2d115433c6 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/live-probes.json @@ -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 + ] + } +] \ No newline at end of file diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/portal-conditions-test.log b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-conditions-test.log new file mode 100644 index 0000000000..36c3c99a99 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-conditions-test.log @@ -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. diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/portal-transfer-test.log b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-transfer-test.log new file mode 100644 index 0000000000..c8948ed790 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-transfer-test.log @@ -0,0 +1 @@ +Portal near/far ACK and denied same-map transfer regressions passed. diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/portal-use-test.log b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-use-test.log new file mode 100644 index 0000000000..63ce324f78 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/portal-use-test.log @@ -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. diff --git a/doc/clientless/checkpoints/2026-10-29-public-portals/static-portal-audit.tsv b/doc/clientless/checkpoints/2026-10-29-public-portals/static-portal-audit.tsv new file mode 100644 index 0000000000..f198598878 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-public-portals/static-portal-audit.tsv @@ -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 diff --git a/doc/clientless/flight-departure-repair.md b/doc/clientless/flight-departure-repair.md new file mode 100644 index 0000000000..07cc7eb1c8 --- /dev/null +++ b/doc/clientless/flight-departure-repair.md @@ -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. diff --git a/doc/clientless/group-preparation.md b/doc/clientless/group-preparation.md new file mode 100644 index 0000000000..773b88131b --- /dev/null +++ b/doc/clientless/group-preparation.md @@ -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. diff --git a/doc/clientless/public-portal-repair.md b/doc/clientless/public-portal-repair.md new file mode 100644 index 0000000000..21b9df74c4 --- /dev/null +++ b/doc/clientless/public-portal-repair.md @@ -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 ` 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. diff --git a/sql/custom/world/2026_10_29_02_adventure_group_preparation_walk.sql b/sql/custom/world/2026_10_29_02_adventure_group_preparation_walk.sql new file mode 100644 index 0000000000..7a38d50458 --- /dev/null +++ b/sql/custom/world/2026_10_29_02_adventure_group_preparation_walk.sql @@ -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'; diff --git a/sql/custom/world/2026_10_29_03_blasted_lands_portal_conditions.sql b/sql/custom/world/2026_10_29_03_blasted_lands_portal_conditions.sql new file mode 100644 index 0000000000..a700b0fa6d --- /dev/null +++ b/sql/custom/world/2026_10_29_03_blasted_lands_portal_conditions.sql @@ -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; diff --git a/sql/custom/world/2026_10_29_04_public_portal_player_cast.sql b/sql/custom/world/2026_10_29_04_public_portal_player_cast.sql new file mode 100644 index 0000000000..1496d021c5 --- /dev/null +++ b/sql/custom/world/2026_10_29_04_public_portal_player_cast.sql @@ -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); diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index 3574d1f3b3..1465dee051 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -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(); } diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index 380640bb98..c4d319356f 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -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(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 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(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(nowGetGUID().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;} diff --git a/src/server/game/World/AdventureBehaviorMgr.h b/src/server/game/World/AdventureBehaviorMgr.h index d7dd7d7c98..74572cee00 100644 --- a/src/server/game/World/AdventureBehaviorMgr.h +++ b/src/server/game/World/AdventureBehaviorMgr.h @@ -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); diff --git a/src/server/game/World/AdventureGroupMgr.cpp b/src/server/game/World/AdventureGroupMgr.cpp index 420cf8fee0..9970fa86f3 100644 --- a/src/server/game/World/AdventureGroupMgr.cpp +++ b/src/server/game/World/AdventureGroupMgr.cpp @@ -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); } diff --git a/src/server/game/World/AdventureGroupPolicy.h b/src/server/game/World/AdventureGroupPolicy.h index 71c5101b04..d2e98f77fa 100644 --- a/src/server/game/World/AdventureGroupPolicy.h +++ b/src/server/game/World/AdventureGroupPolicy.h @@ -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) diff --git a/src/server/game/World/AdventureProfessionPolicy.h b/src/server/game/World/AdventureProfessionPolicy.h index 1b38203d92..a6a2537da4 100644 --- a/src/server/game/World/AdventureProfessionPolicy.h +++ b/src/server/game/World/AdventureProfessionPolicy.h @@ -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) { diff --git a/src/server/game/World/AdventureSocialActions.cpp b/src/server/game/World/AdventureSocialActions.cpp index 66c336cea6..b27647d9a9 100644 --- a/src/server/game/World/AdventureSocialActions.cpp +++ b/src/server/game/World/AdventureSocialActions.cpp @@ -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) diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 30ac845948..914a73fd29 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -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); diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index dc23030ac7..b41e5915c0 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -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{{{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") diff --git a/src/server/game/World/ClientlessFlightMgr.h b/src/server/game/World/ClientlessFlightMgr.h index 5e1592a34c..d3c16ad591 100644 --- a/src/server/game/World/ClientlessFlightMgr.h +++ b/src/server/game/World/ClientlessFlightMgr.h @@ -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; diff --git a/src/server/game/World/ClientlessFlightPath.h b/src/server/game/World/ClientlessFlightPath.h index ef01e1fe31..6aba3d8443 100644 --- a/src/server/game/World/ClientlessFlightPath.h +++ b/src/server/game/World/ClientlessFlightPath.h @@ -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 diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index 6459bfc117..575bf84afd 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -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::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::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable); diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 830c252ed8..0e6519746b 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -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 (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<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, "" }, diff --git a/tests/clientless/test_adventure_group_gather.py b/tests/clientless/test_adventure_group_gather.py new file mode 100644 index 0000000000..829db42235 --- /dev/null +++ b/tests/clientless/test_adventure_group_gather.py @@ -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 +#include +#include +#include +#include +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) diff --git a/tests/clientless/test_adventure_groups.py b/tests/clientless/test_adventure_groups.py index 587b5a1a81..73d11972b4 100644 --- a/tests/clientless/test_adventure_groups.py +++ b/tests/clientless/test_adventure_groups.py @@ -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 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");}); diff --git a/tests/clientless/test_adventure_intent_state.py b/tests/clientless/test_adventure_intent_state.py index 5c6644a20c..aede760cc4 100644 --- a/tests/clientless/test_adventure_intent_state.py +++ b/tests/clientless/test_adventure_intent_state.py @@ -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){} diff --git a/tests/clientless/test_adventure_leisure.py b/tests/clientless/test_adventure_leisure.py index 74cf64de8a..25d76a859c 100644 --- a/tests/clientless/test_adventure_leisure.py +++ b/tests/clientless/test_adventure_leisure.py @@ -10,6 +10,7 @@ def function(signature): code=r''' #include #include "AdventureInstancePolicy.h" +#include "AdventureGroupPolicy.h" #include #include #include @@ -18,6 +19,7 @@ code=r''' #include #include #include +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); diff --git a/tests/clientless/test_adventure_social.py b/tests/clientless/test_adventure_social.py index 4f9f07b8d4..7738292bb3 100644 --- a/tests/clientless/test_adventure_social.py +++ b/tests/clientless/test_adventure_social.py @@ -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 headings;void SetFacingTo(float f){heading=f;headings.push_back(f);}int chairStand=5;uint32 GetStandState(){return sit?chairStand:0;} std::vector 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 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(); diff --git a/tests/clientless/test_flight_departure.py b/tests/clientless/test_flight_departure.py new file mode 100644 index 0000000000..801c678309 --- /dev/null +++ b/tests/clientless/test_flight_departure.py @@ -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 +#include +#include +#include +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 ↦} + 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_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>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.') diff --git a/tests/clientless/test_flight_handoff.py b/tests/clientless/test_flight_handoff.py index 494fa1d52e..a19047c722 100644 --- a/tests/clientless/test_flight_handoff.py +++ b/tests/clientless/test_flight_handoff.py @@ -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 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 _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: diff --git a/tests/clientless/test_flight_policy.py b/tests/clientless/test_flight_policy.py index 3196bd06b4..7ba509d424 100644 --- a/tests/clientless/test_flight_policy.py +++ b/tests/clientless/test_flight_policy.py @@ -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_ptrPublished=std::make_shared(); struct{bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status; - bool Moving=true,Returning=false;std::vector Breadcrumbs{{}};std::string AbortReason; + bool Moving=true,Returning=false,ApproachingDeparture=false;std::vector 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 _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); } ''' diff --git a/tests/clientless/test_free_flight.py b/tests/clientless/test_free_flight.py index 68ff718d69..f79e2efcc1 100644 --- a/tests/clientless/test_free_flight.py +++ b/tests/clientless/test_free_flight.py @@ -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};}; diff --git a/tests/clientless/test_free_flight_planner.py b/tests/clientless/test_free_flight_planner.py index 441546ce73..f27867a7cb 100644 --- a/tests/clientless/test_free_flight_planner.py +++ b/tests/clientless/test_free_flight_planner.py @@ -151,6 +151,13 @@ assert(route.size()==3&&route.front().NodeKey=="flight_departure"&&route[1].Node std::unordered_set 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"; } diff --git a/tests/clientless/test_portal_conditions.py b/tests/clientless/test_portal_conditions.py new file mode 100644 index 0000000000..bedf18fdd8 --- /dev/null +++ b/tests/clientless/test_portal_conditions.py @@ -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.') diff --git a/tests/clientless/test_portal_use.py b/tests/clientless/test_portal_use.py index b0bea9d0b1..153a7f9f67 100644 --- a/tests/clientless/test_portal_use.py +++ b/tests/clientless/test_portal_use.py @@ -200,7 +200,7 @@ b = movement_source.index('void WorldSession::CompleteNearTeleport(', a) ack_harness = r''' #include #include -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);