diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index b43bd257b7..65e35d7dbf 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,160 @@ +# Commit checkpoint: group follow, grounded portal and explicit tram rescue + +User requested a local commit of the current completed changes. Includes group +meeting spacing/road formation and player-led flight mount/hover fixes, grounded +Outland Dark Portal source/capital-return ACL migration05, and explicit rescue +node category/ordinary graph exclusion/recorded escape execution + migration06. +Tests, documentation and native evidence are included. Final rescue build3 and +ten focused tests passed; installed server341932 reached ready with8085/8087. +Native exits verified forDurndan58/Tinkbin33/Hilddis56. Full visual acceptance, +all walking rescues and core static-transport passenger persistence are not +claimed. Earlier UNCOMMITTED/pending install notes below are historical task +checkpoints; consult git log for final commit. No push or restart in commit task. + +# Latest: explicit tram rescue LIVE, successful native exits verified + +Implemented rescue node category, ordinary graph exclusion, scoped recorded +escape search, walk-first then map369-only native transfer fallback after normal +replans/flight exhausted. Applied SQL06; bothnodesrescue/Alliance1. Verified +recorded edges1781/1782/1783 and all points byte-for-byte unchanged against saved +DBsnapshot. Final complete build3 and10 targeted tests PASS. Installed +/tmp/vcore-rescue-route-install.log. Fresh gdb/tmux vcore-worldserver PID341932, +console/tmp/vcore-tram-rescue-start-20261029.log, newready12s verified, process +persists,8085/8087 listening. Old328503 fully exited before install. + +LIVE:58Durndan transferIF20:38:59, authored rescuecomplete20:39:09, +nativeexit2166 map0 at20:39:11, resumed groupformation/tavern preparation inIF. +33Tinkbin transferIF20:39:11, rescuecomplete20:39:34, then normal tram route and +nativeSWexit2171 at20:42:17; resumed citytravel/groupchat.56Hilddis transferSW +20:41:38, rescuecomplete20:41:48, resumed original route including real tram +SW->IF Arrived20:43:18, nativeIFexit2166 map0 at20:43:32.76Renalin started +WALKING rescue20:42:42 with transferred=0, recorded corridor underway (completion +not yet verified). Other automatic rescue transfers/completions78,80,82,88,90,94 +observed. No manual player/bot relocation or forced intent used for these tests. +Evidence checkpoints/2026-10-29-tram-rescue/live-rescue.log. Core static transport +passenger persistence remains unchanged; requested emergency exit covers stranded +bots. End-to-end exits verified for58,33,56, not all bots/visual acceptance. + +All previous groupfollow/portal WIP preserved and UNCOMMITTED; SQL05/06 are +intent-to-add under ignored sql/custom. No push. User's latest interruption +message handled by resuming live verification. Next: report completion; await +user feedback. Do not claim every walking rescue or future restart is tested. + +# Latest rescue install in progress: final build3 SUCCESS + +Latest complete source build passed/tmp/vcore-rescue-route-final-build3.log; +all10 targeted tests pass. Added missing-anchor and partial-route-end hooks on +rescue-network maps; source newer than build2 is compiled in finalbuild3. +Issued shutdown10 to328503. Wait for complete exit/gdb OutdoorPvP trace, apply +migration06, verify recorded edges/points unchanged, install then start fresh +vcore-worldserver undergdb to/tmp/vcore-tram-rescue-start-20261029.log. Verify +fresh ready/listeners and observe native RESCUE_ROUTE_START/TRANSFER/COMPLETE, +portal exits for stranded56,58. No rescue migration/installation performed yet. + +# Latest WIP: explicit rescue network implemented, final build/testing + +User recorded tram_rescue_sw,tram_rescue_if and edges1781,1782,1783. Implemented +rescue node category + command validation. Graph normal roots/anchors/static/ +adaptive/virtual bridge/flight edges exclude rescue; scoped PlanRescueRoute +admits allowed same-map recorded ground escape paths to ordinary nodes. Walk +first; if no approach, map369 only may nativeTeleportTo+ACK closest allowed +rescue node with a real recorded escape. Managed/alive/not combat/transit only, +at most2 rescue starts per action (second reserved for failed walking rescue). +Executor tries rescue after normal recovery radii+fallbackflight fail; initial +no-progress/missing anchor/partial completion on rescue-network maps now reach +ordinary recovery instead of silently cycling. Other maps preserve prior initial +failure semantics. Keep original destination, replan on ordinary escape endpoint, +no group formation/opportunistic MMAP exits during authored escape. Core tram +passenger persistence itself unchanged; this implements user-requested escape. +Migration06 prepared, NOT YET applied: classify two existing nodes rescue and +AllianceACL1; recorded geometry untouched/snapshot in +checkpoints/2026-10-29-tram-rescue/recordings-before.tsv. Category SQL ignored by +.gitignore; add with -N -f like migration05 when ready. Existing source WIP +from group flight+portals must be preserved. +Ten targeted tests passed (/tmp/vcore-rescue-tests.json), new production rescue +graph/executor tests. Later no-shortcut assertion rechecked. Final incremental +build running/tmp/vcore-rescue-route-final-build2.log; one late partial-recovery +hook changed Executor after compilation may require final incremental rebuild. +DO NOT install stale binary. Current testserver328503 still running; no lifecycle +or DB changes in this rescue task yet. Next: finish latest build+verify tests, +applySQL06 while stopped, install, restart undergdb and verify freshready/ports, +observe native rescue behavior for stranded58Durndan/56Hilddis and other369bots. +User says recording completed; normal authorized test restart allowed. + +# Latest: diagnosed stranded tram bots; no fix/relocation/restart yet + +User observes bots stuck in Deeprun Tram. Read-only diagnostics confirm58Durndan +(5.774,858.385,-84.745) and56Hilddis(3.825,1299.646,-125.218),map369, +native live probes match saved positions, no supported feet floor, not flying, +falling or mounted. Both have NO transport ride state after restart.16 bots +have saved online positions on369; do not infer all16 independently verified. +Action58 Completed with `no useful travel progress available`, repeated intent +fallback every30s. Core Player::SaveToDB stores transportGUID only for dynamic +Transport*(MO_TRANSPORT); static GO transport tram176081 is excluded. Load +restores only HighGuid::Mo_Transport. Thus static passenger binding is lost on +restart while world position in tunnel persists. TransportMgr ride/recovery is +in-memory and absent after restart. Generic navigation rescue also recognizes +only Failed; Completed/no-useful-progress doesn't increment failure counter; +Fallback switches StateIdle, resetting stall evidence. Separate disembark issue: +124Marnra boarded19:16:50, Leaving19:18:02, then Riding19:18:18 after departure +before transition finished. Not established as cause for all bots. No new code, +DB relocation or lifecycle actions; previous WIP preserved. Evidence +checkpoints/2026-10-29-tram-stranding/live.log. Server328503 remains running. + +# Latest: group follow build installed, portal graph corrected, server READY + +User released recording restart hold. Installed successful final build using +existing CMake install (/tmp/vcore-group-follow-fixes-install.log). Old317961 +fully exited with previously known OutdoorPvP shutdown destructor SIGSEGV; +backtrace preserved in oldconsole. Fresh gdb/tmux vcore-worldserver PID328503, +console/tmp/vcore-group-follow-fixes-start-20261029.log. NewServer.log reports +worldinitialized12s + daemonready; process persists and8085/8087 listeners +verified. Normal loader enabled/staged. No critical startup/portal rejection +errors observed; existing unrelated DB content warnings remain. +Portal164 graphimport creates enablededge1776/Hordeonlysource; source4352 +nativeground84.380 replaces DBCboxcenter144.362, unchangedXY within trigger, +target4354unchanged. Applied guarded SQLmigration05, retained originalrow and +nativeprobe evidence. Nativeobjectimport164 afterstartup CONFIRMED current graph accepts binding +(idempotent preserves nodepositions); freshconsole line787. +Source WIP and migration05 (intent-to-add because sql/custom ignored) uncommitted. +Eleven targeted tests/build passed; gameplay/visual acceptance still pending. +See dark-portal-return-graph.md and group-follow-spacing-flight.md. + +# Latest: restart hold released; portal graph corrections applied + +User explicitly authorized installation/restart. Native GO import164 succeeded, +edge1776 Hellfire->Orgrimmar enabled; previously tested click had no graphbinding. +Applied worldmigration2026_10_29_05: source164 Horde2, existing170 Alliance1, +portal_at_4352_source grounded Z84.380 (native feetprobe49, DBCcenter144.362, +actualtriggervertical77.462..211.262). Arrivalnode4354target unchanged. +See dark-portal-return-graph.md and before.tsv checkpoint. No DBC/native teleport +edits. Groupfollow final build passed; eleven tests passed. Existing WIP preserved. +Issued `.server shutdown 10` to PID317961 before install. Next verify full stop +(including known gdb shutdown-crash capture), install existing CMake build, +restart under gdb with fresh console, verify new ready+listeners8085/8087. + +# Latest: gathering/formation and player-led flight fixes prepared, DO NOT RESTART + +User is actively recording routes and explicitly requested code/build first, +notification before install/restart. Preserve current running worldserver +PID317961. No console commands, DB/profile/plan changes or lifecycle actions +performed in this task. New source WIP after54d77d1e4d is intentional. +Changes: gathering uses existing safe nearby positioning+reservations within +meeting neighborhood and finishes spacing walks; ground-follow destination +reuse0.75yd; recorded road row wait also across brief executing-leader stops, +long mounted row window, native taxi/transport boarding released; prepare native +profile flying mount when leader equips one; retain it on actual takeoff; +owned stale ground route cannot block flight; follow does not depend on ordinary +visual detection range (same map/phase and all safety checks retained); +stationary aerial flags+Fly animation native publication without idle flood, +Ground tier on regular touchdown. New test_group_flight_mount.py plus updated +production harnesses. Eleven tests passed, summary/tmp/vcore-group-fixes-tests.json. +Initial and final complete builds passed; final log +/tmp/vcore-group-follow-fixes-final-build.log. +See group-follow-spacing-flight.md. Not installed, not live tested, not committed. +Next: final build is successful; notify user ready for install/restart, +WAIT for route-recording interruption to be released before stopping server. + # Commit checkpoint: group preparation, public portals and flight recovery User requested committing all current changes. This checkpoint combines the diff --git a/doc/clientless/checkpoints/2026-10-29-dark-portal-return/after.tsv b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/after.tsv new file mode 100644 index 0000000000..99bc8d80d7 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/after.tsv @@ -0,0 +1,9 @@ +node_key enabled map position_x position_y position_z allowed_factions +portal_at_4352_source 1 530 -247.677 895.675 84.38 NULL +portal_at_4354_target 1 530 -248.113 922.9 84.3497 NULL +portal_go_164_source 1 530 -161.318 965.41 54.2904 2 +portal_go_164_target 1 1 1776.5 -4338.8 -7.48 NULL +id from_node_key to_node_key edge_type cost bidirectional enabled comment +1776 portal_go_164_source portal_go_164_target portal_use 1 0 1 Native GameObject portal spawn 164 +edge_id gameobject_spawn_id gameobject_entry spell_id +1776 164 195140 17609 diff --git a/doc/clientless/checkpoints/2026-10-29-dark-portal-return/before.tsv b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/before.tsv new file mode 100644 index 0000000000..2890bd83e8 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/before.tsv @@ -0,0 +1,4 @@ +id point_key category map position_x position_y position_z orientation enabled comment +710 portal_at_4352_source travel_portal 530 -247.677 895.675 144.362 0 1 Auto-imported AreaTrigger 4352 source +node_key allowed_factions flightmaster_spawn_id notes +portal_go_170_source NULL NULL diff --git a/doc/clientless/checkpoints/2026-10-29-dark-portal-return/native-probe.log b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/native-probe.log new file mode 100644 index 0000000000..fde2517683 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-dark-portal-return/native-probe.log @@ -0,0 +1,4 @@ +Portal object import complete: spawn=164 entry=195140 spell=17609 portal_go_164_source -> portal_go_164_target (map 530 -> 1). One-way native click interaction. Existing coordinates and enabled flags preserved; inspect reload logs for rejected edges. +[?2004hTC>Flight surface map=530 x=-247.677 y=895.675 valid=1 ground=290.508 ceiling=600.000 +Flight surface map=530 x=-247.677 y=895.675 valid=1 ground=290.508 ceiling=600.000 +Flight departure guid=49 position=(190.876 2610.930 87.284) nearFeet=84.380 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-tram-rescue/live-rescue.log b/doc/clientless/checkpoints/2026-10-29-tram-rescue/live-rescue.log new file mode 100644 index 0000000000..dd74733331 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-tram-rescue/live-rescue.log @@ -0,0 +1,44 @@ +2026-10-29_20:38:59 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=58 node='tram_rescue_if' +2026-10-29_20:38:59 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=58 steps=2 transferred=1 +2026-10-29_20:39:09 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=58 map=369 +2026-10-29_20:39:11 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=33 node='tram_rescue_if' +2026-10-29_20:39:11 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=33 steps=2 transferred=1 +2026-10-29_20:39:11 INFO [server.adventurebot.navigation] EDGE_START guid=58 name='Durndan' action='travel_to_location' step=3/4 node='portal_at_2166_target' nodeType='portal' edgeType='portal' edgeDbId=1516 reverse=0 recovery=1 +2026-10-29_20:39:11 INFO [server.adventurebot.navigation] PORTAL_TRIGGER guid=58 name='Durndan' edgeDbId=1516 trigger=2166 sourceMap=369 targetMap=0 source=(76.027 10.504 -4.297) target=(-4839.510 -1317.740 501.868) +2026-10-29_20:39:11 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=58 name='Durndan' edgeType='portal' edgeDbId=1516 node='portal_at_2166_target' trigger=2166 map=0 arrivalError=0.00 recovery=1 +2026-10-29_20:39:34 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=33 map=369 +2026-10-29_20:40:48 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=33 phase='Boarding' from='tram_if' to='tram_sw' passenger=0 detail='boarding recorded land-to-deck transition' +2026-10-29_20:40:51 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=33 phase='Riding' from='tram_if' to='tram_sw' passenger=1 detail='aboard; waiting for destination dock' +2026-10-29_20:41:38 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=56 node='tram_rescue_sw' +2026-10-29_20:41:38 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=56 steps=2 transferred=1 +2026-10-29_20:41:48 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=56 map=369 +2026-10-29_20:41:56 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=56 phase='Boarding' from='tram_sw' to='tram_if' passenger=0 detail='boarding recorded land-to-deck transition' +2026-10-29_20:41:56 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=33 phase='Leaving' from='tram_if' to='tram_sw' passenger=1 detail='leaving via reversed recorded transition' +2026-10-29_20:41:57 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=56 phase='Riding' from='tram_sw' to='tram_if' passenger=1 detail='aboard; waiting for destination dock' +2026-10-29_20:41:57 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=33 phase='Arrived' from='tram_if' to='tram_sw' passenger=0 detail='arrived at shared destination node' +2026-10-29_20:42:17 INFO [server.adventurebot.navigation] EDGE_START guid=33 name='Tinkbin' action='travel_to_location' step=6/8 node='portal_at_2171_target' nodeType='portal' edgeType='portal' edgeDbId=1517 reverse=0 recovery=1 +2026-10-29_20:42:17 INFO [server.adventurebot.navigation] PORTAL_TRIGGER guid=33 name='Tinkbin' edgeDbId=1517 trigger=2171 sourceMap=369 targetMap=0 source=(80.163 2490.880 -1.857) target=(-8359.270 517.585 84.409) +2026-10-29_20:42:17 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=33 name='Tinkbin' edgeType='portal' edgeDbId=1517 node='portal_at_2171_target' trigger=2171 map=0 arrivalError=0.00 recovery=1 +2026-10-29_20:42:42 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=76 steps=3 transferred=0 +2026-10-29_20:42:48 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=78 node='tram_rescue_if' +2026-10-29_20:42:48 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=78 steps=2 transferred=1 +2026-10-29_20:42:57 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=80 node='tram_rescue_if' +2026-10-29_20:42:57 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=80 steps=2 transferred=1 +2026-10-29_20:42:58 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=78 map=369 +2026-10-29_20:43:00 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=82 node='tram_rescue_if' +2026-10-29_20:43:00 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=82 steps=2 transferred=1 +2026-10-29_20:43:13 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=80 map=369 +2026-10-29_20:43:13 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=56 phase='Leaving' from='tram_sw' to='tram_if' passenger=1 detail='leaving via reversed recorded transition' +2026-10-29_20:43:13 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=82 map=369 +2026-10-29_20:43:18 INFO [server.adventurebot.navigation] TRANSPORT_STATE guid=56 phase='Arrived' from='tram_sw' to='tram_if' passenger=0 detail='arrived at shared destination node' +2026-10-29_20:43:25 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=88 node='tram_rescue_sw' +2026-10-29_20:43:25 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=88 steps=2 transferred=1 +2026-10-29_20:43:32 INFO [server.adventurebot.navigation] EDGE_START guid=56 name='Hilddis' action='travel_to_location' step=5/7 node='portal_at_2166_target' nodeType='portal' edgeType='portal' edgeDbId=1516 reverse=0 recovery=1 +2026-10-29_20:43:32 INFO [server.adventurebot.navigation] PORTAL_TRIGGER guid=56 name='Hilddis' edgeDbId=1516 trigger=2166 sourceMap=369 targetMap=0 source=(76.027 10.504 -4.297) target=(-4839.510 -1317.740 501.868) +2026-10-29_20:43:32 INFO [server.adventurebot.navigation] EDGE_COMPLETE guid=56 name='Hilddis' edgeType='portal' edgeDbId=1516 node='portal_at_2166_target' trigger=2166 map=0 arrivalError=0.00 recovery=1 +2026-10-29_20:43:34 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=90 node='tram_rescue_sw' +2026-10-29_20:43:34 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=90 steps=2 transferred=1 +2026-10-29_20:43:34 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=88 map=369 +2026-10-29_20:43:45 INFO [server.adventurebot.navigation] RESCUE_ROUTE_COMPLETE guid=90 map=369 +2026-10-29_20:43:47 WARN [server.adventurebot.navigation] RESCUE_NODE_TRANSFER guid=94 node='tram_rescue_if' +2026-10-29_20:43:47 WARN [server.adventurebot.navigation] RESCUE_ROUTE_START guid=94 steps=2 transferred=1 diff --git a/doc/clientless/checkpoints/2026-10-29-tram-rescue/nodes-after.tsv b/doc/clientless/checkpoints/2026-10-29-tram-rescue/nodes-after.tsv new file mode 100644 index 0000000000..7e0f35904e --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-tram-rescue/nodes-after.tsv @@ -0,0 +1,3 @@ +node_key node_type map position_x position_y position_z allowed_factions +tram_rescue_if rescue 369 33.5987 40.562 -4.29735 1 +tram_rescue_sw rescue 369 33.2794 2460.21 -4.29702 1 diff --git a/doc/clientless/checkpoints/2026-10-29-tram-rescue/recordings-before.tsv b/doc/clientless/checkpoints/2026-10-29-tram-rescue/recordings-before.tsv new file mode 100644 index 0000000000..b57131d82a --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-tram-rescue/recordings-before.tsv @@ -0,0 +1,262 @@ +id from_node_key to_node_key edge_type cost bidirectional enabled comment +1781 tram_rescue_sw portal_at_2173_target ground 64.545 1 1 Recorded in game +1782 tram_rescue_if portal_at_2175_target ground 66.158 1 1 Recorded in game +1783 tram_rescue_if tram_rescue_sw ground 2522.03 1 1 Recorded in game +edge_id point_index map position_x position_y position_z orientation +1781 0 369 33.2794 2460.21 -4.29702 3.14159 +1781 1 369 34.4697 2468.04 -4.29644 1.58578 +1781 2 369 34.3648 2475.04 -4.29644 1.58578 +1781 3 369 34.2599 2482.04 -4.29644 1.58578 +1781 4 369 34.1551 2489.03 -4.29644 1.58578 +1781 5 369 34.1254 2491.02 -4.29644 1.58578 +1781 6 369 42.2844 2490.82 -4.29644 6.27068 +1781 7 369 50.9148 2490.71 -4.29644 0.00712752 +1781 8 369 57.9147 2490.74 -4.29644 0.00320053 +1781 9 369 65.0337 2490.76 -4.29644 0.00320053 +1781 10 369 67.7607 2490.98 -4.29649 3.14159 +1782 0 369 33.5987 40.562 -4.29735 1.65099 +1782 1 369 33.4101 10.2316 -4.29735 4.70617 +1782 2 369 50.7893 9.99874 -4.29735 6.27696 +1782 3 369 59.7685 10.0018 -4.29735 0.0134046 +1782 4 369 69.2277 10.3932 -4.29665 3.14159 +1783 0 369 33.5987 40.562 -4.29735 1.65099 +1783 1 369 5.77265 39.95 -13.8634 3.18642 +1783 2 369 5.77265 39.95 -13.8634 3.18642 +1783 3 369 5.72619 51.9688 -13.8634 1.57321 +1783 4 369 5.7093 58.9688 -13.8634 1.57321 +1783 5 369 5.6924 65.9687 -13.8634 1.57321 +1783 6 369 5.67551 72.9687 -13.8634 1.57321 +1783 7 369 5.65861 79.9687 -13.8634 1.57321 +1783 8 369 5.64172 86.9687 -13.8634 1.57321 +1783 9 369 5.62482 93.9687 -13.8634 1.57321 +1783 10 369 5.60793 100.969 -13.8634 1.57321 +1783 11 369 5.58259 111.469 -13.8634 1.57321 +1783 12 369 5.56569 118.469 -13.8634 1.57321 +1783 13 369 5.5488 125.469 -13.8634 1.57321 +1783 14 369 5.5319 132.469 -13.8634 1.57321 +1783 15 369 5.51501 139.469 -13.8634 1.57321 +1783 16 369 5.49811 146.469 -13.8634 1.57321 +1783 17 369 5.48122 153.468 -13.8634 1.57321 +1783 18 369 5.46432 160.468 -13.8634 1.57321 +1783 19 369 5.41364 181.468 -13.8634 1.57321 +1783 20 369 5.39674 188.468 -13.8634 1.57321 +1783 21 369 5.37985 195.468 -13.8634 1.57321 +1783 22 369 5.34606 209.468 -15.6739 1.57321 +1783 23 369 5.32916 216.468 -18.6992 1.57321 +1783 24 369 5.30382 226.968 -20.6303 1.57321 +1783 25 369 5.28692 233.968 -23.5947 1.57321 +1783 26 369 5.26158 244.468 -27.3972 1.57321 +1783 27 369 5.24469 251.468 -28.4898 1.57321 +1783 28 369 5.22779 258.468 -31.5143 1.57321 +1783 29 369 5.2109 265.468 -34.1641 1.57321 +1783 30 369 5.194 272.468 -34.1641 1.57321 +1783 31 369 5.08419 317.968 -47.698 1.57321 +1783 32 369 5.06729 324.968 -49.6455 1.57321 +1783 33 369 4.83921 419.468 -54.4648 1.57321 +1783 34 369 4.80542 433.468 -54.4648 1.57321 +1783 35 369 4.78008 443.968 -54.4648 1.57321 +1783 36 369 4.75474 454.468 -54.4648 1.57321 +1783 37 369 4.73784 461.468 -54.4648 1.57321 +1783 38 369 4.72095 468.468 -54.4648 1.57321 +1783 39 369 4.70405 475.468 -54.4648 1.57321 +1783 40 369 4.68716 482.468 -54.4648 1.57321 +1783 41 369 4.67026 489.467 -54.4648 1.57321 +1783 42 369 4.65337 496.467 -54.4648 1.57321 +1783 43 369 4.63647 503.467 -54.4648 1.57321 +1783 44 369 4.61958 510.467 -54.4648 1.57321 +1783 45 369 4.60268 517.467 -54.4648 1.57321 +1783 46 369 4.58579 524.467 -54.4648 1.57321 +1783 47 369 4.56889 531.467 -54.4648 1.57321 +1783 48 369 4.552 538.467 -54.4648 1.57321 +1783 49 369 4.5351 545.467 -54.4648 1.57321 +1783 50 369 4.51821 552.467 -54.4648 1.57321 +1783 51 369 4.50131 559.467 -54.4648 1.57321 +1783 52 369 4.48442 566.467 -54.4648 1.57321 +1783 53 369 4.46752 573.467 -54.4648 1.57321 +1783 54 369 4.45063 580.467 -54.637 1.57321 +1783 55 369 4.43373 587.467 -57.6621 1.57321 +1783 56 369 4.41684 594.467 -60.6876 1.57321 +1783 57 369 4.39994 601.467 -61.2317 1.57321 +1783 58 369 4.38305 608.467 -64.0704 1.57321 +1783 59 369 4.36615 615.467 -67.0933 1.57321 +1783 60 369 4.34926 622.467 -67.9986 1.57321 +1783 61 369 4.33237 629.467 -70.4778 1.57321 +1783 62 369 4.31547 636.467 -73.5021 1.57321 +1783 63 369 4.29858 643.467 -74.7656 1.57321 +1783 64 369 4.24789 664.467 -80.3308 1.57321 +1783 65 369 4.231 671.467 -81.5325 1.57321 +1783 66 369 4.2141 678.467 -83.7141 1.57321 +1783 67 369 4.19721 685.467 -86.737 1.57321 +1783 68 369 4.18031 692.467 -88.2994 1.57321 +1783 69 369 4.13807 709.967 -94.6582 1.57321 +1783 70 369 4.12118 716.967 -95.0663 1.57321 +1783 71 369 4.70651 788.601 -95.0663 1.56143 +1783 72 369 4.93322 833.981 -95.0663 1.56928 +1783 73 369 4.94805 843.781 -95.0663 1.56928 +1783 74 369 4.95864 850.781 -95.0663 1.56928 +1783 75 369 4.97983 864.781 -95.0663 1.56928 +1783 76 369 5.0275 896.281 -95.0663 1.56928 +1783 77 369 5.0381 903.281 -95.0663 1.56928 +1783 78 369 5.04869 910.281 -95.0663 1.56928 +1783 79 369 5.05929 917.281 -95.0663 1.56928 +1783 80 369 5.06988 924.281 -95.0663 1.56928 +1783 81 369 5.08047 931.281 -95.0663 1.56928 +1783 82 369 5.09107 938.281 -95.0663 1.56928 +1783 83 369 5.10166 945.281 -95.0663 1.56928 +1783 84 369 5.11226 952.281 -95.0663 1.56928 +1783 85 369 5.12285 959.281 -96.9771 1.56928 +1783 86 369 5.13345 966.281 -100.002 1.56928 +1783 87 369 5.14404 973.281 -101.833 1.56928 +1783 88 369 5.15463 980.281 -103.386 1.56928 +1783 89 369 5.16523 987.281 -106.409 1.56928 +1783 90 369 5.17582 994.281 -108.6 1.56928 +1783 91 369 5.18642 1001.28 -109.793 1.56928 +1783 92 369 5.19701 1008.28 -112.818 1.56928 +1783 93 369 5.2076 1015.28 -115.367 1.56928 +1783 94 369 5.2182 1022.28 -115.367 1.56928 +1783 95 369 5.22879 1029.28 -116.62 1.56928 +1783 96 369 5.23939 1036.28 -119.646 1.56928 +1783 97 369 5.24998 1043.28 -122.134 1.56928 +1783 98 369 5.26057 1050.28 -123.03 1.56928 +1783 99 369 5.27117 1057.28 -126.053 1.56928 +1783 100 369 5.28176 1064.28 -128.901 1.56928 +1783 101 369 5.29236 1071.28 -129.437 1.56928 +1783 102 369 5.30295 1078.28 -132.461 1.56928 +1783 103 369 5.31354 1085.28 -135.486 1.56928 +1783 104 369 5.32414 1092.28 -135.668 1.56928 +1783 105 369 5.33473 1099.28 -135.668 1.56928 +1783 106 369 5.34533 1106.28 -135.668 1.56928 +1783 107 369 5.35592 1113.28 -135.668 1.56928 +1783 108 369 5.46716 1186.78 -135.668 1.56928 +1783 109 369 5.47775 1193.78 -135.668 1.56928 +1783 110 369 5.48835 1200.78 -135.668 1.56928 +1783 111 369 5.49894 1207.78 -135.668 1.56928 +1783 112 369 5.50953 1214.78 -135.668 1.56928 +1783 113 369 5.52013 1221.78 -135.668 1.56928 +1783 114 369 5.53072 1228.78 -135.668 1.56928 +1783 115 369 5.54132 1235.78 -135.668 1.56928 +1783 116 369 5.55191 1242.78 -135.668 1.56928 +1783 117 369 5.5625 1249.78 -135.668 1.56928 +1783 118 369 5.65785 1312.78 -135.668 1.56928 +1783 119 369 5.67904 1326.78 -135.668 1.56928 +1783 120 369 5.69493 1337.28 -135.668 1.56928 +1783 121 369 5.70552 1344.28 -135.668 1.56928 +1783 122 369 5.72142 1354.78 -135.668 1.56928 +1783 123 369 5.73201 1361.78 -135.668 1.56928 +1783 124 369 5.7426 1368.78 -135.668 1.56928 +1783 125 369 5.7532 1375.78 -135.668 1.56928 +1783 126 369 5.76379 1382.78 -135.668 1.56928 +1783 127 369 5.77439 1389.78 -135.668 1.56928 +1783 128 369 5.78498 1396.78 -135.668 1.56928 +1783 129 369 5.79557 1403.78 -135.668 1.56928 +1783 130 369 5.80617 1410.78 -135.668 1.56928 +1783 131 369 5.81676 1417.78 -134.48 1.56928 +1783 132 369 5.82736 1424.78 -131.456 1.56928 +1783 133 369 5.83795 1431.78 -128.901 1.56928 +1783 134 369 5.84854 1438.78 -128.071 1.56928 +1783 135 369 5.85914 1445.78 -125.048 1.56928 +1783 136 369 5.86973 1452.78 -122.133 1.56928 +1783 137 369 5.88033 1459.78 -121.665 1.56928 +1783 138 369 5.89092 1466.78 -118.639 1.56928 +1783 139 369 5.90151 1473.78 -115.614 1.56928 +1783 140 369 5.91211 1480.78 -115.367 1.56928 +1783 141 369 5.9227 1487.78 -114.836 1.56928 +1783 142 369 5.9333 1494.78 -111.812 1.56928 +1783 143 369 5.94389 1501.78 -108.787 1.56928 +1783 144 369 5.95448 1508.78 -108.427 1.56928 +1783 145 369 5.96508 1515.78 -105.404 1.56928 +1783 146 369 5.97567 1522.78 -102.381 1.56928 +1783 147 369 5.98627 1529.78 -101.833 1.56928 +1783 148 369 6.01275 1547.28 -95.0659 1.56928 +1783 149 369 6.02335 1554.28 -95.0659 1.56928 +1783 150 369 6.07102 1585.78 -95.0659 1.56928 +1783 151 369 6.1081 1610.28 -95.0659 1.56928 +1783 152 369 6.16637 1648.78 -95.0659 1.56928 +1783 153 369 6.18755 1662.78 -95.0659 1.56928 +1783 154 369 6.22993 1690.78 -95.0659 1.56928 +1783 155 369 6.24052 1697.78 -95.0659 1.56928 +1783 156 369 6.25112 1704.78 -95.0659 1.56928 +1783 157 369 6.26171 1711.78 -95.0659 1.56928 +1783 158 369 6.27231 1718.78 -95.0659 1.56928 +1783 159 369 6.2829 1725.78 -95.0659 1.56928 +1783 160 369 6.29349 1732.78 -95.0659 1.56928 +1783 161 369 6.30409 1739.78 -95.0659 1.56928 +1783 162 369 6.31468 1746.78 -95.0659 1.56928 +1783 163 369 6.323 1753.78 -95.0659 1.57321 +1783 164 369 6.17717 1762.19 -95.0659 1.57714 +1783 165 369 6.15937 1769.19 -95.0659 1.57321 +1783 166 369 6.09758 1788.32 -95.0659 1.58892 +1783 167 369 5.7779 1797.41 -91.787 1.60855 +1783 168 369 5.48354 1806.04 -88.2992 1.58892 +1783 169 369 5.3567 1813.04 -87.6971 1.58892 +1783 170 369 5.25385 1819.22 -85.0282 1.58106 +1783 171 369 5.18198 1826.22 -82.0052 1.58106 +1783 172 369 5.10969 1833.26 -81.5324 1.58106 +1783 173 369 5.03535 1840.26 -78.6035 1.58499 +1783 174 369 5.01315 1846.28 -76.002 1.56928 +1783 175 369 5.06044 1853.28 -74.7651 1.56143 +1783 176 369 5.14916 1859.57 -74.7651 1.54965 +1783 177 369 5.29719 1866.57 -72.5058 1.54965 +1783 178 369 5.67775 1886.41 -66.6006 1.56143 +1783 179 369 5.73378 1893.17 -63.6804 1.56928 +1783 180 369 5.72013 1900.17 -61.2315 1.57714 +1783 181 369 5.67575 1907.17 -60.2976 1.57714 +1783 182 369 5.63136 1914.17 -57.2721 1.57714 +1783 183 369 5.58698 1921.17 -54.4646 1.57714 +1783 184 369 5.5426 1928.17 -54.4646 1.57714 +1783 185 369 5.49821 1935.17 -54.4646 1.57714 +1783 186 369 5.4662 1942.17 -54.4646 1.57321 +1783 187 369 5.4493 1949.17 -54.4646 1.57321 +1783 188 369 5.39017 1973.67 -54.4646 1.57321 +1783 189 369 5.25501 2029.67 -54.4646 1.57321 +1783 190 369 5.23812 2036.67 -54.4646 1.57321 +1783 191 369 5.21277 2047.17 -54.4646 1.57321 +1783 192 369 5.19588 2054.17 -54.4646 1.57321 +1783 193 369 5.17899 2061.17 -54.4646 1.57321 +1783 194 369 5.09451 2096.17 -54.4646 1.57321 +1783 195 369 5.06072 2110.17 -54.4646 1.57321 +1783 196 369 5.00159 2134.67 -54.4646 1.57321 +1783 197 369 4.95935 2152.17 -54.4646 1.57321 +1783 198 369 4.94246 2159.17 -54.4646 1.57321 +1783 199 369 4.92556 2166.17 -53.793 1.57321 +1783 200 369 4.90867 2173.17 -50.7683 1.57321 +1783 201 369 4.99947 2179.34 -48.0994 1.47111 +1783 202 369 5.86159 2185.71 -47.6966 1.43969 +1783 203 369 6.63432 2192.78 -44.9595 1.4986 +1783 204 369 7.22189 2201.75 -41.0863 1.50645 +1783 205 369 7.67199 2208.73 -40.7331 1.50645 +1783 206 369 8.1221 2215.72 -37.7138 1.50645 +1783 207 369 8.66178 2224.09 -34.1639 1.7028 +1783 208 369 7.90861 2232.35 -34.1639 1.63211 +1783 209 369 7.64265 2239.34 -32.7776 1.60462 +1783 210 369 7.39944 2246.53 -29.6724 1.60462 +1783 211 369 7.16269 2253.52 -27.3969 1.60462 +1783 212 369 6.92595 2260.52 -26.2913 1.60462 +1783 213 369 6.6892 2267.52 -23.2703 1.60462 +1783 214 369 6.45246 2274.51 -20.6299 1.60462 +1783 215 369 6.21572 2281.51 -19.8905 1.60462 +1783 216 369 5.97897 2288.5 -16.8666 1.60462 +1783 217 369 5.96024 2296.94 -13.8626 1.56142 +1783 218 369 6.02585 2303.94 -13.8626 1.56142 +1783 219 369 6.07254 2310.82 -13.8626 1.58106 +1783 220 369 6.00071 2317.82 -13.8626 1.58106 +1783 221 369 5.92888 2324.82 -13.8626 1.58106 +1783 222 369 5.85942 2331.59 -13.8626 1.58106 +1783 223 369 5.76426 2340.86 -13.863 1.58106 +1783 224 369 5.69244 2347.86 -13.863 1.58106 +1783 225 369 5.51286 2365.36 -13.863 1.58106 +1783 226 369 5.44103 2372.36 -13.863 1.58106 +1783 227 369 5.26146 2389.86 -13.863 1.58106 +1783 228 369 5.18963 2396.86 -13.863 1.58106 +1783 229 369 5.1178 2403.86 -13.863 1.58106 +1783 230 369 5.04597 2410.85 -13.863 1.58106 +1783 231 369 4.97415 2417.85 -13.863 1.58106 +1783 232 369 4.90232 2424.85 -13.863 1.58106 +1783 233 369 4.83049 2431.85 -13.863 1.58106 +1783 234 369 4.77863 2437.98 -13.863 1.5732 +1783 235 369 4.78883 2444.98 -13.863 1.56928 +1783 236 369 4.79947 2451.98 -13.863 1.56928 +1783 237 369 4.81117 2459.68 -13.863 1.56928 +1783 238 369 4.81223 2460.38 -13.863 1.56928 +1783 239 369 32.6858 2459.78 -4.29708 6.26596 +1783 240 369 33.2794 2460.21 -4.29702 3.14159 diff --git a/doc/clientless/checkpoints/2026-10-29-tram-rescue/tests.json b/doc/clientless/checkpoints/2026-10-29-tram-rescue/tests.json new file mode 100644 index 0000000000..b67193e2d4 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-tram-rescue/tests.json @@ -0,0 +1,12 @@ +{ + "test_rescue_graph": 0, + "test_rescue_executor": 0, + "test_free_flight_planner": 0, + "test_shared_group_route": 0, + "test_adventure_area_node": 0, + "test_adventure_intent_state": 0, + "test_adventure_group_travel": 0, + "test_adventure_group_gather": 0, + "test_group_flight_follow": 0, + "test_group_flight_mount": 0 +} \ No newline at end of file diff --git a/doc/clientless/checkpoints/2026-10-29-tram-stranding/live.log b/doc/clientless/checkpoints/2026-10-29-tram-stranding/live.log new file mode 100644 index 0000000000..db97a3ca48 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-29-tram-stranding/live.log @@ -0,0 +1,5 @@ +[?2004hTC>No transport ride state for this character. +TC> No transport ride state for this character. +TC> Action GUID 58: state=Completed action='travel_to_location' target='location:poi_tele_253' targetGuid=0 entry=1274 detail='no useful travel progress available' +Flight departure guid=58 position=(5.774 858.385 -84.745) nearFeet=-200000.000 rawWater=0.000 liquidStatus=0 liquidLevel=0.000 mounted=0 canFly=0 flying=0 falling=0 +Flight departure guid=56 position=(3.825 1299.646 -125.218) nearFeet=-200000.000 rawWater=0.000 liquidStatus=0 liquidLevel=0.000 mounted=0 canFly=0 flying=0 falling=0 diff --git a/doc/clientless/dark-portal-return-graph.md b/doc/clientless/dark-portal-return-graph.md new file mode 100644 index 0000000000..95bd312968 --- /dev/null +++ b/doc/clientless/dark-portal-return-graph.md @@ -0,0 +1,33 @@ +# Hellfire capital return and grounded Dark Portal approach + +2026-10-29: user found no nearby Orgrimmar return portal graph node, and the +Outland Dark Portal source node suspended in air. Both observations confirmed. +The previously successful native click test of spawn164 had not imported its +graph binding (the Stormwind return170 was already imported). + +Executed native `clientless portal object import 164`: edge1776, enabled, +one-way portal_go_164_source -> portal_go_164_target. Real spawn164/entry195140, +spell17609. Source map530(-161.318,965.410,54.2904), target map1 +(1776.5,-4338.8,-7.48). Native portal interaction and spell destination retained. +World migration2026_10_29_05_dark_portal_graph_positions.sql marks164 source +Horde2 and existing170 source Alliance1; parent area rules retain precedence. + +AreaTrigger4352 DBC box center is(-247.677,895.675,144.362), box dimensions +102.8 x6.611 x133.8, yaw0. Native bounded ground probe at unchanged XY using +online map530 bot49 (Z87.284) returned nearFeet84.380. Sky-height query instead +sees the portal structure roof at290.508; using that would be wrong. Migration +moves only portal_at_4352_source Z144.362 ->84.380, inside the actual trigger +vertical span77.462..211.262. portal_at_4354_target remains unchanged at84.3497. +Guarded SQL preserves any unrelated explicitly relocated source. Original row +saved in checkpoints/2026-10-29-dark-portal-return/before.tsv. DBC files and +native trigger/teleport destinations unchanged. An explicit AreaTrigger import +can regenerate its DBC center; do not blindly reimport this manually corrected +source. Native GO importer preserves existing graph positions. + +User released recording-related restart hold. Install/restart of the prepared +group follow build authorized together with these data fixes. Live graph +traversal from new ground node and visual acceptance remain to be verified. + +Fresh startup under installed build reached ready in12s, PID328503, bothports +8085/8087 listening. Repeated native import164 confirmed active binding in the +fresh graph (startupconsole line787). Original corrected source remains84.380. diff --git a/doc/clientless/group-follow-spacing-flight.md b/doc/clientless/group-follow-spacing-flight.md new file mode 100644 index 0000000000..a29964d1da --- /dev/null +++ b/doc/clientless/group-follow-spacing-flight.md @@ -0,0 +1,64 @@ +# Group gathering, road spacing and human-led flight follow + +Prepared 2026-10-29 after commit 54d77d1e4d. The user is recording native +transport routes and explicitly requested no installation or server restart. +These changes are source/build only and have not been accepted in-game. + +## Changes + +- Five-minute gathering retains a six-yard meeting neighborhood. The existing + positioning helper selects available, supported MMAP-reachable places within + five yards of the anchor, considering nearby units and reservations. A spacing + walk is allowed to finish; waiting positions refresh their reservation. + If no safe free place exists, the bot may share the available space. +- Ground-follow spline reuse now requires its destination to be within 0.75yd + of the newly assigned formation position, rather than 3yd (the entire walking + row spacing). Recorded road rows keep their separation during short stops + while the leader is still executing a route, and the distance window scales + with long mounted formations. Native taxi/transport boarding releases that + road wait. Exact portal and boarding endpoints remain unchanged. +- When a human leader equips a native flight-capable mount, eligible followers + prepare their learned profile flight mount even before the leader takes off + and while holding formation. Native capability, outdoor, combat, water, + transport, casting and battlefield restrictions apply. Grounded preparation + retries are limited to once per five seconds. A valid equipped flight mount + is retained during takeoff instead of being removed and cast again. +- An owned ground catch-up route cannot indefinitely prevent switching to the + leader's flight. Queued recorded portal/taxi/transport transitions still have + precedence. Same-map/phase group following does not depend on the leader + remaining in ordinary visual detection range; path, area and separation + checks still apply. +- Stationary aerial formation explicitly publishes MOVEMENTFLAG_FLYING and + AnimTier::Fly through the native movement update packet. Idle updates are + emitted only when stopping or restoring changed flags/animation, avoiding + repeated idle broadcasts. Verified regular touchdown restores Ground tier. + +## Verification and pending installation + +Existing Debug CMake configuration is unchanged. Initial complete build passed; +final complete build also passed, log: /tmp/vcore-group-follow-fixes-final-build.log. +Eleven focused executable harnesses passed; summary: +/tmp/vcore-group-fixes-tests.json (per-test logs share that prefix). +These include real production gathering/follow/road/mount/hover adapters, +flight admission and handoff, flight policy/geometry, generic shared routes, +group policy and group planning. Doubles do not establish actual client visual +behavior or real-world route completion. + +No SQL/profile/plan edits, console commands, installation, restart, or commit +were performed for this task. The existing worldserver PID317961 remained +running. Install only after the user finishes recording and permits the planned +interruption, then run normal startup verification. In-game acceptance should +check Gilded Rose gathering, a larger mounted group at roads/taxi transitions, +Lunna's mount switch and following a human leader, stationary griffon animation, +and normal landing/dismount behavior. Native taxi routes may naturally overlap; +these changes do not create separate flight-master taxi trajectories. + +## Installation completed + +The user subsequently authorized interruption. Installed successfully with +existing CMake configuration, after full process exit. Fresh worldserver328503 +under gdb/tmux reached ready (12s initialization); process and8085/8087 checked. +Install log /tmp/vcore-group-follow-fixes-install.log, startup console +/tmp/vcore-group-follow-fixes-start-20261029.log. The old shutdown again hit the +known OutdoorPvP destructor fault; its gdb trace is preserved in the old console. +No live visual/group acceptance is claimed by startup verification. diff --git a/doc/clientless/tram-rescue-routes.md b/doc/clientless/tram-rescue-routes.md new file mode 100644 index 0000000000..b809875d23 --- /dev/null +++ b/doc/clientless/tram-rescue-routes.md @@ -0,0 +1,67 @@ +# Explicit rescue graph (Deeprun Tram) + +The user recorded tram_rescue_sw and tram_rescue_if and three bidirectional +normal ground recordings:1781 SW->portal_at_2173_target,1782 IF->portal_at_2175_target, +1783 IF<->SW. Their geometry is preserved. Migration2026_10_29_06 marks the two +nodes rescue and limits them to Alliance. Future nodes can be created with +`.clientless node add rescue` and connected using ordinary route recording. + +Normal graph start selection, nearest location anchors, static and adaptive +routing, virtual entries/exits, node bridges and free-flight candidates exclude +rescue nodes and edges touching them. They remain administratively visible. +Only the synchronous RAII scope in PlanRescueRoute enables them for emergency +planning, while retaining area/faction/level and blocked-edge checks. + +After normal replanning search radii500/1000/2000/4000 and fallback flight fail, +the executor may seek an explicit rescue route. Already exhausted execution +retries also reach this fallback. Initial no-progress actions on maps with a +rescue network now exhaust those normal searches instead of silently completing +and indefinitely cycling intents. Maps without rescue nodes keep prior initial +failure behavior. No early rescue while ordinary planning succeeds. + +For each allowed same-map rescue node, breadth-first search finds an escape +through recorded ground edges to an ordinary node. A complete native adaptive +approach to the rescue node is preferred. If no reachable approach exists, only +on map369, the bot may transfer to the nearest allowed rescue root that has an +actual recorded escape. Native TeleportTo + clientless ACK are used. No arbitrary +nearest-node transfer, faction bypass or teleport on other maps. Actual transport +passengers, taxi/free flight, combat, death or pending transfer exclude rescue. + +The escape executes the authored ground splines. Group road formation and +opportunistic ground-exit shortcuts do not alter this emergency path. Intent +handover waits until the escape reaches an ordinary node. The executor retains +the original destination, then replans it from the escape endpoint. At most two +rescue starts per action: the second is reserved for a failed walking escape and +may use the tram-only transfer. Blocked recordings remain excluded. Rescue does +not repair core static-transport passenger persistence; it provides the requested +safe exit for affected bots after a restart or navigation failure. + +Tests test_rescue_graph and test_rescue_executor execute production methods with +native geometry/access/transfer doubles. They cover ordinary-route exclusion, +recorded-only escapes, walking before transfer, map369 restriction, scope reset, +blocked edges, access denial, native ACK/state cleanup, bounded retry and original +goal retention. Further existing flight-planner/area/group regressions pass. +Build/runtime evidence will be appended after installation. + +Final complete build passed: /tmp/vcore-rescue-route-final-build3.log. +Ten targeted test results: /tmp/vcore-rescue-tests.json, per-test logs +/tmp/vcore-rescue-test_*.log. Shutdown requested for installation. + +## Installation and native verification + +SQL06 applied; exact edge/point snapshot comparison passed. Installed finalbuild3 +with /tmp/vcore-rescue-route-install.log. Fresh server341932 under gdb/tmux, +console/tmp/vcore-tram-rescue-start-20261029.log, ready12s, ports8085/8087 verified. + +Durndan58: fallback transferIF20:38:59, recorded rescue completed20:39:09, +native Ironforge exit2166 at20:39:11, resumed tavern preparation. Tinkbin33: +transferIF20:39:11, escapecomplete20:39:34, original route took native tram and +Stormwind exit2171 at20:42:17, resumed city/group activity. Hilddis56: +transferSW20:41:38, escapecomplete20:41:48, original route took actual tram +SW->IF (Arrived20:43:18), native IFexit2166 at20:43:32. Renalin76 chose a +walkable rescue approach (transferred=0)20:42:42; its completion remains pending. +Additional automatic rescues observed for78/80/82/88/90/94. No manual relocation, +forced action or altered dayplan was used. See checkpoint live-rescue.log. +This verifies three actual escapes and subsequent normal travel, rather than +asserting every bot or visual behavior was accepted. Static tram passenger +persistence is still a separate core limitation. diff --git a/sql/custom/world/2026_10_29_05_dark_portal_graph_positions.sql b/sql/custom/world/2026_10_29_05_dark_portal_graph_positions.sql new file mode 100644 index 0000000000..84b5c231c9 --- /dev/null +++ b/sql/custom/world/2026_10_29_05_dark_portal_graph_positions.sql @@ -0,0 +1,15 @@ +-- The Outland Dark Portal's DBC box center is Z144.362, not a walking surface. +-- Native bounded GetHeight at its unchanged XY confirms the platform Z84.380. +-- DBC4352 spans Z77.462..211.262: the corrected point remains inside the trigger. +-- Preserve the correctly located arrival node and the native teleport destination. +UPDATE vcore_clientless_world_point +SET position_z=84.380, comment='Dark Portal Outland ground approach inside AreaTrigger 4352' +WHERE point_key='portal_at_4352_source' AND map=530 + AND ABS(position_x+247.677)<0.01 AND ABS(position_y-895.675)<0.01 + AND ABS(position_z-144.362)<0.01; + +-- Capital return portals are faction-specific even in shared Hellfire Peninsula. +INSERT INTO vcore_clientless_node_access (node_key,allowed_factions,notes) +VALUES ('portal_go_164_source',2,'Hellfire Peninsula native return portal to Orgrimmar'), + ('portal_go_170_source',1,'Hellfire Peninsula native return portal to Stormwind') +ON DUPLICATE KEY UPDATE allowed_factions=VALUES(allowed_factions),notes=VALUES(notes); diff --git a/sql/custom/world/2026_10_29_06_tram_rescue_nodes.sql b/sql/custom/world/2026_10_29_06_tram_rescue_nodes.sql new file mode 100644 index 0000000000..f822ac1a06 --- /dev/null +++ b/sql/custom/world/2026_10_29_06_tram_rescue_nodes.sql @@ -0,0 +1,11 @@ +-- User-recorded escape routes remain byte-for-byte intact. Only the explicit +-- rescue endpoints are classified; every edge touching them is excluded from +-- ordinary planning by the core, and admitted in a scoped rescue search. +UPDATE vcore_clientless_travel_node n +JOIN vcore_clientless_world_point p ON p.point_key=n.world_point_key +SET n.node_type='rescue' +WHERE n.node_key IN ('tram_rescue_sw','tram_rescue_if') AND p.map=369; +INSERT INTO vcore_clientless_node_access (node_key,allowed_factions,notes) +VALUES ('tram_rescue_sw',1,'Recorded Deeprun Tram escape; recovery only'), + ('tram_rescue_if',1,'Recorded Deeprun Tram escape; recovery only') +ON DUPLICATE KEY UPDATE allowed_factions=VALUES(allowed_factions),notes=VALUES(notes); diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index c4d319356f..91bcc49cab 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -1371,10 +1371,22 @@ bool AdventureBehaviorMgr::UpdateGathering(Player* player,Record& r,time_t now) } 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(player->GetMapId()==r.AnchorMap && player->GetExactDist(r.Anchor)<=6.0f) { + // Allow an already assigned spacing walk to finish; do not pull it + // back to the shared anchor on the next gathering update. + if(nowState==ClientlessTravelState::Moving)return true; if(travel && travel->State==ClientlessTravelState::Moving) {sClientlessTravelMgr->CancelTravel(guid);player->StopMoving();} + if(now>=r.NextSpacing && ClientlessPositioning::Clearance(player,*player)<1.1f) + { + r.NextSpacing=now+30; + Position spot;std::vector path; + if(ClientlessPositioning::Nearby(player,r.Anchor,5.0f,spot,path) && + sClientlessTravelMgr->StartTravel(player,spot)) + {r.Detail="taking a free place at group meeting point";return true;} + } + ClientlessPositioning::Reserve(player,*player); 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"; diff --git a/src/server/game/World/AdventureGroupTravelMgr.cpp b/src/server/game/World/AdventureGroupTravelMgr.cpp index 349c232291..0c96ce1d92 100644 --- a/src/server/game/World/AdventureGroupTravelMgr.cpp +++ b/src/server/game/World/AdventureGroupTravelMgr.cpp @@ -347,9 +347,18 @@ bool AdventureGroupTravelMgr::Follow(Player* player) JourneyFollow(player,record,record.Pending.front()); record.Detail="following/waiting for known travel transition";return true; } + // A route to a previous ground position must not block a human leader's + // takeoff. Recorded transport/portal journeys above retain precedence. + bool flightLeader=player->IsInMap(leader) && leader->IsInPhase(player) && + leader->IsMounted() && leader->CanFly() && !leader->IsInFlight() && !leader->GetTransport(); + if(flightLeader && leader->IsFlying() && record.RouteOwned && Executing(guid) && !player->IsFlying()) + { + sClientlessPlayerMgr->ClearQueuedActions(guid); + sClientlessActionExecutorMgr->Clear(guid);record.RouteOwned=false; + } if(Executing(guid))return true; if(!player->IsInMap(leader))return true; - if(!player->IsInPhase(leader) || !player->CanSeeOrDetect(leader))return true; + if(!player->IsInPhase(leader))return true; if(leader->IsInFlight() || leader->GetTransport())return true; if(leader->IsFlying() && sClientlessFlightMgr->Enabled(player)) { @@ -359,6 +368,15 @@ bool AdventureGroupTravelMgr::Follow(Player* player) record.Detail=error;if(player->IsFlying())return true; } if(player->IsFlying() || player->IsFalling())return true; + // Mount before takeoff as well, including while holding formation. + if(flightLeader && !player->CanFly() && sClientlessFlightMgr->Enabled(player)) + { + auto now=GameTime::GetGameTime(); + if(nowPrepareFollowingMount(player)) + {record.Detail="preparing flying mount to match group leader";return true;} + } auto target=Formation(player,leader,false); if(!GroundPoint(player,target) || (leader->IsFlying() && std::fabs(leader->GetPositionZ()-target.GetPositionZ())>AdventureGroupTravelPolicy::GroundLeaderHeight)) {sClientlessTravelMgr->CancelTravel(guid);record.Detail="leader above reachable ground; waiting";return true;} @@ -372,7 +390,7 @@ bool AdventureGroupTravelMgr::Follow(Player* player) if(player->GetExactDist(target)<1.2f) {sClientlessTravelMgr->CancelTravel(guid);record.Detail="holding assigned formation position";return true;} auto nav=sClientlessTravelMgr->GetStatus(guid); - if(nav && nav->State==ClientlessTravelState::Moving && nav->Target.GetExactDist(target)<3) + if(nav && nav->State==ClientlessTravelState::Moving && nav->Target.GetExactDist(target)<0.75f) { if(auto active=sClientlessTravelMgr->GetActivePath(guid))if(!ValidateGroundPath(player,*active)) {sClientlessTravelMgr->CancelTravel(guid);record.Detail="active follow corridor became unsafe";} @@ -380,12 +398,12 @@ bool AdventureGroupTravelMgr::Follow(Player* player) } if(!SafePath(player,target)) {sClientlessTravelMgr->CancelTravel(guid);record.Detail="no safe complete follow path; waiting";return true;} - if(leader->IsMounted()) + if(leader->IsMounted() && !(flightLeader && player->CanFly())) { auto profile=sClientlessLuaEngine->GetAdventureProfile(guid); if(profile)sClientlessLuaEngine->PrepareGroundMount(player,std::max(profile->GroundMountMinDistance,player->GetExactDist(target)),"group-follow"); } - else if(player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot"); + else if(!leader->IsMounted() && player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot"); bool started=sClientlessTravelMgr->StartTravel(player,target,true); record.Detail=started?"safe ground formation":"follow spline rejected; waiting";return true; } @@ -402,7 +420,9 @@ bool AdventureGroupTravelMgr::AdaptRoad(Player* player,std::vector& pa bool isLeader=player==leader;unsigned slot=unsigned(member-members.begin()); unsigned columns=isLeader?1:Columns(player,leader); auto offset=AdventureGroupTravelPolicy::Formation(slot,columns,true,leader->IsMounted(),false); - if(!isLeader && leader->isMoving() && player->GetExactDist(leader)<65) + if(!isLeader && !leader->IsInFlight() && !leader->GetTransport() && + (leader->isMoving() || Executing(leader->GetGUID().GetCounter())) && + player->GetExactDist(leader) Pending; diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 914a73fd29..f75ed539df 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -242,6 +242,7 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c record.FinalLocationId.clear(); record.FailedEdgeDatabaseIds.clear(); record.RecoveryCount = 0; + record.RescueActive=false;record.RescueWalkFailed=false;record.RescueAttempts=0; record.RecoveryWanderAttempts = 0; record.RecoveryWanderActive = false; record.PlannedStartRadius = 500.0f; @@ -636,18 +637,11 @@ bool ClientlessActionExecutorMgr::StartTravelToNode( if (TryFallbackFlight(player, characterGuid, record)) return true; - if (record.HardDestination) - { - record.Status.State = ClientlessActionExecutionState::Failed; - record.Status.Detail = error; - record.Active = false; - return false; - } - - record.Status.State = ClientlessActionExecutionState::Completed; - record.Status.Detail = "no useful travel progress available"; - record.Active = false; - return true; + if(sClientlessTravelGraphMgr->HasRescueNodes(player->GetMapId())) + return RecoverTravel(player,characterGuid,record,0,"initial-plan-failed: "+error); + record.Status.State=record.HardDestination?ClientlessActionExecutionState::Failed:ClientlessActionExecutionState::Completed; + record.Status.Detail=record.HardDestination?error:"no useful travel progress available"; + record.Active=false;return !record.HardDestination; } TC_LOG_INFO("server.adventurebot.navigation", @@ -807,6 +801,8 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( { if (TryFallbackFlight(player, characterGuid, record)) return true; + if(sClientlessTravelGraphMgr->HasRescueNodes(player->GetMapId())) + return RecoverTravel(player,characterGuid,record,0,"initial-plan-failed: no destination anchor"); record.Status.State = record.HardDestination ? ClientlessActionExecutionState::Failed : ClientlessActionExecutionState::Completed; @@ -831,12 +827,11 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( locationId.c_str(), error.c_str()); if (TryFallbackFlight(player, characterGuid, record)) return true; - record.Status.State = record.HardDestination - ? ClientlessActionExecutionState::Failed - : ClientlessActionExecutionState::Completed; - record.Status.Detail = record.HardDestination ? error : "no useful travel progress available"; - record.Active = false; - return !record.HardDestination; + if(sClientlessTravelGraphMgr->HasRescueNodes(player->GetMapId())) + return RecoverTravel(player,characterGuid,record,0,"initial-plan-failed: "+error); + record.Status.State=record.HardDestination?ClientlessActionExecutionState::Failed:ClientlessActionExecutionState::Completed; + record.Status.Detail=record.HardDestination?error:"no useful travel progress available"; + record.Active=false;return !record.HardDestination; } record.FinalLocalLegPending = record.RouteComplete; TC_LOG_INFO("server.adventurebot.navigation", @@ -910,7 +905,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep( uint32 characterGuid, Record& record) { - if (record.StopAtNode && record.RouteStep > 0) + if (record.StopAtNode && !record.RescueActive && record.RouteStep > 0) { auto const& reached = record.Route[record.RouteStep - 1]; if (reached.NodeKey != "flight_location" && !player->GetTransport() && !player->IsInFlight() && @@ -924,6 +919,13 @@ bool ClientlessActionExecutorMgr::StartRouteStep( } if (record.RouteStep >= record.Route.size()) { + if(record.RescueActive) + { + record.RescueActive=false;record.RecoveryCount=0; + if(!record.Route.empty())record.FailedBridgeKeys.erase("start:"+record.Route.back().NodeKey); + TC_LOG_INFO("server.adventurebot.navigation","RESCUE_ROUTE_COMPLETE guid=%u map=%u",characterGuid,player->GetMapId()); + return RecoverTravel(player,characterGuid,record,0,"rescue-route-complete"); + } if (record.FinalLocalLegPending) return StartFinalLocalLeg(player, characterGuid, record); @@ -931,6 +933,8 @@ bool ClientlessActionExecutorMgr::StartRouteStep( { if (TryFallbackFlight(player, characterGuid, record)) return true; + if(sClientlessTravelGraphMgr->HasRescueNodes(player->GetMapId())) + return RecoverTravel(player,characterGuid,record,0,"partial-route-ended-before-destination"); TC_LOG_WARN("server.adventurebot.navigation", "TRAVEL_PARTIAL guid=%u name='%s' action='%s' requested='%s' reached='%s' " "map=%u pos=(%.3f %.3f %.3f)", @@ -1806,7 +1810,7 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath( for(size_t i=1;isecond(player,path)) @@ -2146,6 +2150,40 @@ bool ClientlessActionExecutorMgr::StartRecoveryWander( return false; } +bool ClientlessActionExecutorMgr::TryRescueTravel(Player* player,uint32 characterGuid,Record& record) +{ + if(!player || !sClientlessPlayerMgr->IsManaged(characterGuid) || !player->IsAlive() || + player->IsInCombat() || player->IsBeingTeleported() || player->GetTransport() || + player->IsInFlight() || player->IsFlying() || record.RescueAttempts>=2 || + (record.RescueAttempts && !record.RescueWalkFailed))return false; + std::vector route;std::string transfer; + if(!sClientlessTravelGraphMgr->PlanRescueRoute(player,route,transfer,record.RescueWalkFailed, + &record.FailedEdgeDatabaseIds))return false; + ++record.RescueAttempts; + if(!transfer.empty()) + { + auto node=sClientlessTravelGraphMgr->FindNode(transfer); + if(player->GetMapId()!=369 || !node || node->MapId!=369 || node->Type!="rescue" || + !sAdventureAreaMgr->AllowsNode(player,*node))return false; + sClientlessTravelMgr->CancelTravel(characterGuid);player->StopMoving(); + if(!player->TeleportTo(node->MapId,node->X,node->Y,node->Z,node->O))return false; + player->GetSession()->CompleteClientlessTeleport(); + TC_LOG_WARN("server.adventurebot.navigation","RESCUE_NODE_TRANSFER guid=%u node='%s'",characterGuid,transfer.c_str()); + } + sClientlessTravelMgr->CancelTravel(characterGuid); + record.Route=std::move(route);record.RouteStep=0;record.EdgePath.clear(); + record.DeferredStartActive=false;record.GroundExitActive=false;record.GroundResumeActive=false; + record.TaxiActive=false;record.RecoveryWanderActive=false; + record.TransportActive=false;record.FlightActive=false;record.FlightApproachActive=false;record.PortalUseActive=false; + record.FormationWaiting=false;record.GroundChunk=false;record.ReplanAtGroundEnd=false;record.GroundFlightExit=false; + record.FinalLocalLegActive=false;record.FinalLocalLegPending=false; + record.RouteComplete=true;record.RescueActive=true;record.StopAtNode=false; + record.Active=true;record.Status.State=ClientlessActionExecutionState::Moving; + record.Status.Detail="following explicit rescue route after exhausted normal planning"; + TC_LOG_WARN("server.adventurebot.navigation","RESCUE_ROUTE_START guid=%u steps=%u transferred=%u",characterGuid,uint32(record.Route.size()),transfer.empty()?0:1); + return StartRouteStep(player,characterGuid,record); +} + bool ClientlessActionExecutorMgr::RecoverTravel( Player* player, uint32 characterGuid, @@ -2158,6 +2196,8 @@ bool ClientlessActionExecutorMgr::RecoverTravel( if (!player || !player->IsInWorld()) return false; + if(record.RescueActive) + {record.RescueActive=false;record.RescueWalkFailed=true;} if (failedEdgeDatabaseId) record.FailedEdgeDatabaseIds.insert(failedEdgeDatabaseId); // A failed initial local approach must not be tried five times at the same spot. @@ -2180,6 +2220,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel( { if (TryFallbackFlight(player, characterGuid, record)) return true; + if (TryRescueTravel(player,characterGuid,record))return true; record.Status.State = ClientlessActionExecutionState::Failed; record.Status.Detail = "travel recovery exhausted: " + reason; record.Active = false; @@ -2310,6 +2351,8 @@ bool ClientlessActionExecutorMgr::RecoverTravel( if (TryFallbackFlight(player, characterGuid, record)) return true; + if (TryRescueTravel(player,characterGuid,record))return true; + if (StartRecoveryWander(player, characterGuid, record, lastError)) return true; @@ -2582,7 +2625,7 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode( if (index >= 0 && std::size_t(index) < record.EdgeSplinePoints.size()) record.EdgeNextPoint = std::max(record.EdgeNextPoint, record.EdgeSplinePoints[std::size_t(index)]); - if(!_groundAdapters.count(characterGuid))CheckGroundExit(player, characterGuid, record); + if(!record.RescueActive && !_groundAdapters.count(characterGuid))CheckGroundExit(player, characterGuid, record); if (record.GroundExitActive) return; } diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index 88cbfcf6cf..61aac66e94 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -106,6 +106,8 @@ private: // current action; nothing is disabled persistently in the database. std::unordered_set FailedEdgeDatabaseIds; uint32 RecoveryCount = 0; + bool RescueActive = false, RescueWalkFailed = false; + uint32 RescueAttempts = 0; uint32 RecoveryWanderAttempts = 0; bool RecoveryWanderActive = false; float PlannedStartRadius = 500.0f; @@ -137,6 +139,7 @@ private: bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record); bool GetTravelGoal(Record const& record, ClientlessTravelNode& goal) const; bool TryFallbackFlight(Player* player, uint32 characterGuid, Record& record); + bool TryRescueTravel(Player* player, uint32 characterGuid, Record& record); void CheckGroundExit(Player* player, uint32 characterGuid, Record& record); void UpdateGroundExit(Player* player, uint32 characterGuid, Record& record); bool ResumeRecordedEdge(Player* player, uint32 characterGuid, Record& record); diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index b41e5915c0..ebe98659a6 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -29,6 +29,9 @@ #include "VMapManager2.h" #include "World.h" #include "WorldSession.h" +#include "WorldPacket.h" +#include "Opcodes.h" +#include "GameTime.h" #include #include #include @@ -200,6 +203,51 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error) {return Begin(player,target,error,false);} +bool ClientlessFlightMgr::PrepareFollowingMount(Player* player) +{ + if(!Enabled(player) || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || + player->GetTransport() || player->IsInFlight() || player->IsFalling() || player->IsInWater() || + !player->IsOutdoors() || player->GetMap()->GetEntry()->MapType!=MAP_COMMON)return false; + if(player->IsMounted() && player->CanFly())return false; + if(player->HasUnitState(UNIT_STATE_CASTING))return true; + if(auto battlefield=sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(),player->GetZoneId())) + if(!battlefield->CanFlyIn())return false; + auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); + uint32 total=0,spell=0; + for(auto const& mount:profile->FlightMounts) + { + if(!player->HasSpell(mount.DisplayId))continue; + uint32 type=FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId)); + auto capability=type?player->GetMountCapability(type):nullptr; + if(!capability || !(capability->Flags&MOUNT_CAPABILITY_FLAG_FLYING))continue; + uint32 weight=std::min(mount.Weight,uint32(10000));if(!weight)continue; + total+=weight;if(urand(1,total)<=weight)spell=mount.DisplayId; + } + if(!spell)return false; + sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());player->StopMoving(); + sClientlessLuaEngine->DismountGroundMount(player,"group flying mount"); + player->RemoveAurasByType(SPELL_AURA_MOUNTED); + return player->CastSpell(player,spell,false)==SPELL_CAST_OK; +} + +void ClientlessFlightMgr::HoldFollowing(Player* player,Record& record) +{ + bool changed=record.Moving || !player->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); + if(record.Moving)player->StopMoving(); + record.Moving=false; + player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); + if(player->GetByteValue(UNIT_FIELD_BYTES_1,UNIT_BYTES_1_OFFSET_ANIM_TIER)!=uint8(AnimTier::Fly)) + {player->SetAnimTier(AnimTier::Fly);changed=true;} + if(!changed)return; + // Clientless players have no client sending movement heartbeats after a + // spline stop. Publish the stationary flight flags to nearby clients. + player->m_movementInfo.guid=player->GetGUID(); + player->m_movementInfo.pos.Relocate(player->GetPosition()); + player->m_movementInfo.time=GameTime::GetGameTimeMS(); + WorldPacket update(SMSG_MOVE_UPDATE);player->WriteMovementInfo(update); + player->SendMessageToSet(&update,true); +} + bool ClientlessFlightMgr::Follow(Player* player,Position const& target,std::string& error) { if (!player || !sAdventureAreaMgr->AllowsPosition(player,player->GetMapId(),target)) @@ -236,7 +284,7 @@ bool ClientlessFlightMgr::LandFollowing(Player* player) bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record) { Point current=PointOf(*player),goal=PointOf(record.Status.Target); - if(Distance(current,goal)<2){if(record.Moving)player->StopMoving();record.Moving=false;record.Status.Detail="holding flight formation";return true;} + if(Distance(current,goal)<2){HoldFollowing(player,record);record.Status.Detail="holding flight formation";return true;} 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);}; auto settings=record.Settings;settings.Safety=1; @@ -254,7 +302,7 @@ bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record) float candidate=Distance(point,goal)+std::fabs(turn)*4+climb; if(candidateStopMoving();record.Moving=false;record.Status.Detail="group flight corridor blocked; holding mounted";return false;} + if(!std::isfinite(score)){HoldFollowing(player,record);record.Status.Detail="group flight corridor blocked; holding mounted";return false;} if(record.Moving && player->IsSplineEnabled() && Distance(best,PointOf(record.Segment))<3)return true; if(record.Breadcrumbs.size()<4096)record.Breadcrumbs.push_back(*player); record.Segment.Relocate(best.X,best.Y,best.Z,player->GetOrientation()); @@ -380,13 +428,13 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s } sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter()); player->StopMoving(); - if(!airborne) + if(!existingMount) { sClientlessLuaEngine->DismountGroundMount(player, "free-flight-takeoff"); player->RemoveAurasByType(SPELL_AURA_MOUNTED); } record.Status.State = ClientlessFlightState::Mounting; - record.Status.MountSpell = spell; + record.Status.MountSpell = existingMount ? existingMount : spell; record.Status.Detail = "waiting for native flying mount"; if(!following)if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID()) { @@ -413,7 +461,7 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s _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) + if (!existingMount && player->CastSpell(player, spell, false) != SPELL_CAST_OK) { Fail(player, _records.at(player->GetGUID().GetCounter()), "native mount spell rejected"); error = "native mount spell rejected"; @@ -521,6 +569,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff } player->StopMoving(); player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_ASCENDING | MOVEMENTFLAG_DESCENDING); + player->SetAnimTier(AnimTier::Ground); player->RemoveAurasByType(SPELL_AURA_MOUNTED); record.Status.State = record.Returning ? ClientlessFlightState::Failed : ClientlessFlightState::Arrived; record.Status.Detail = record.Returning ? "safely returned after abort: " + record.AbortReason : "landed on validated destination ground"; diff --git a/src/server/game/World/ClientlessFlightMgr.h b/src/server/game/World/ClientlessFlightMgr.h index d3c16ad591..48c894d7ac 100644 --- a/src/server/game/World/ClientlessFlightMgr.h +++ b/src/server/game/World/ClientlessFlightMgr.h @@ -34,6 +34,7 @@ class TC_GAME_API ClientlessFlightMgr bool Start(Player * player, Position const & target, std::string & error); bool Follow(Player* player, Position const& target, std::string& error); bool LandFollowing(Player* player); + bool PrepareFollowingMount(Player* player); void Update(uint32 diff); void Clear(uint32 guid); ClientlessFlightStatus const * GetStatus(uint32 guid) const; @@ -62,6 +63,7 @@ class TC_GAME_API ClientlessFlightMgr bool ApproachDeparture(Player* player, Record& record, bool following); bool FindReturnLanding(Player* player, Record& record); bool AdvanceFollowing(Player* player, Record& record); + void HoldFollowing(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; // Immutable only for one explicit world-thread graph-planning scope. diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index 575bf84afd..849c8860c0 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -289,7 +289,7 @@ std::string ClientlessTravelGraphMgr::FindNearestStartNode( ClientlessTravelNode const& node = pair.second; if (node.MapId != player->GetMapId()) continue; - if (!sAdventureAreaMgr->AllowsNode(player,node)) continue; + if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; float const dx = node.X - player->GetPositionX(); float const dy = node.Y - player->GetPositionY(); @@ -331,7 +331,7 @@ bool ClientlessTravelGraphMgr::PlanRoute( return false; } - if (!sAdventureAreaMgr->AllowsNode(player,_nodes.at(destinationNode))) + if ((!RescueNodeAllowed(_nodes.at(destinationNode)) || !sAdventureAreaMgr->AllowsNode(player,_nodes.at(destinationNode)))) { error="destination node denied by area/node catalog"; return false; } if (startRadius <= 0.0f) @@ -390,7 +390,7 @@ bool ClientlessTravelGraphMgr::PlanRoute( for (ClientlessTravelEdge const& edge : edgeItr->second) { - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end()) @@ -489,7 +489,7 @@ bool ClientlessTravelGraphMgr::FindNearestNodeToPosition( for (auto const& pair : _nodes) { ClientlessTravelNode const& node = pair.second; - if (node.MapId != mapId) + if (node.MapId != mapId || !RescueNodeAllowed(node)) continue; float const dx = node.X - x; @@ -578,7 +578,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( return false; } - if (!locationGoal && !sAdventureAreaMgr->AllowsNode(player,destinationItr->second)) + if (!locationGoal && (!RescueNodeAllowed(destinationItr->second) || !sAdventureAreaMgr->AllowsNode(player,destinationItr->second))) { error="destination node denied by area/node catalog"; return false; } if (locationGoal) { @@ -699,7 +699,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( ClientlessTravelNode const& node = pair.second; if (node.MapId != player->GetMapId()) continue; - if (!sAdventureAreaMgr->AllowsNode(player,node)) continue; + if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; float const dx = node.X - player->GetPositionX(); float const dy = node.Y - player->GetPositionY(); @@ -777,7 +777,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( ClientlessTravelNode const& node = pair.second; if (node.MapId != player->GetMapId()) continue; - if (!sAdventureAreaMgr->AllowsNode(player,node)) continue; + if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; float dx = node.X - player->GetPositionX(); float dy = node.Y - player->GetPositionY(); float dz = node.Z - player->GetPositionZ(); @@ -808,7 +808,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( if (edge.Type != "ground" || !edge.DatabaseId || (edge.FromNode != near.second && edge.ToNode != near.second)) continue; - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; if (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)) continue; std::string key = std::to_string(edge.DatabaseId) + (edge.Reverse ? "R" : "F"); @@ -988,7 +988,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId))) continue; if (_nodes.at(edge.FromNode).MapId!=goalMap || _nodes.at(edge.ToNode).MapId!=goalMap) continue; - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; std::string const identity = std::to_string(edge.DatabaseId) + (edge.Reverse ? "R" : "F"); if (!scannedDirected.insert(identity).second) @@ -1229,7 +1229,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( { for (ClientlessTravelEdge const& edge : edgeItr->second) { - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; if (portalArrival && edge.Type != "portal") continue; if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId)) @@ -1272,7 +1272,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // route already found; this preserves the full graph search. float lowerBound=40.0f+std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f; if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue; - if (!sAdventureAreaMgr->AllowsNode(player,target)) continue; + if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue; if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue; float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z); if (!std::isfinite(cost)) continue; @@ -1325,7 +1325,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId))) continue; if(_nodes.at(edge.FromNode).MapId!=from.MapId || _nodes.at(edge.ToNode).MapId!=from.MapId)continue; - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; auto pts = _edgePoints.find(edge.DatabaseId); if (pts == _edgePoints.end() || pts->second.size() < 3) continue; @@ -1443,7 +1443,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( for (auto const& key : neighborhood->second) { ClientlessTravelNode const& to = _nodes.at(key); - if (!sAdventureAreaMgr->AllowsNode(player,to)) continue; + if ((!RescueNodeAllowed(to) || !sAdventureAreaMgr->AllowsNode(player,to))) continue; if (to.Key == from.Key || to.MapId != from.MapId) continue; @@ -1935,7 +1935,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition( ClientlessTravelNode const& node = pair.second; if (node.MapId != player->GetMapId()) continue; - if (!sAdventureAreaMgr->AllowsNode(player,node)) continue; + if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; float const dx = node.X - player->GetPositionX(); float const dy = node.Y - player->GetPositionY(); @@ -1970,7 +1970,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition( for (ClientlessTravelEdge const& edge : edgeItr->second) { - if (!sAdventureAreaMgr->AllowsEdge(player,edge)) continue; + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end()) @@ -2001,7 +2001,7 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition( for (auto const& pair : _nodes) { ClientlessTravelNode const& node = pair.second; - if (!sAdventureAreaMgr->AllowsNode(player,node)) continue; + if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; if (node.MapId != destinationMapId) continue; @@ -2096,3 +2096,69 @@ ClientlessTravelGraphMgr::GetEdgePoints(uint32 edgeDatabaseId) const auto itr = _edgePoints.find(edgeDatabaseId); return itr != _edgePoints.end() ? &itr->second : nullptr; } + +// Rescue nodes stay visible to administration but are never normal graph roots, +// anchors, bridge endpoints or shortcuts. Only this bounded planning scope can +// enter the recorded escape network. Native access policy remains mandatory. +bool ClientlessTravelGraphMgr::PlanRescueRoute(Player* player, + std::vector& route, std::string& teleportNode, + bool forceTeleport, std::unordered_set const* blockedEdgeIds) +{ + route.clear();teleportNode.clear();EnsureLoaded(); + if(!player || !player->IsInWorld())return false; + struct Scope {bool& Flag;bool Previous;Scope(bool& f):Flag(f),Previous(f){Flag=true;}~Scope(){Flag=Previous;}} scope(_rescuePlanning); + std::vector roots; + for(auto const& entry:_nodes) + if(entry.second.Type=="rescue" && entry.second.MapId==player->GetMapId() && + sAdventureAreaMgr->AllowsNode(player,entry.second))roots.push_back(&entry.second); + auto distance=[&](ClientlessTravelNode const* n){return player->GetExactDist(n->X,n->Y,n->Z);}; + std::sort(roots.begin(),roots.end(),[&](auto a,auto b){float da=distance(a),db=distance(b);return da==db?a->KeyKey:da fallback; + std::string fallbackNode; + auto stepOf=[](ClientlessTravelNode const& node,ClientlessTravelEdge const* edge) + { + ClientlessTravelRouteStep step;step.NodeKey=node.Key;step.NodeType=node.Type;step.WorldPointKey=node.WorldPointKey; + step.MapId=node.MapId;step.X=node.X;step.Y=node.Y;step.Z=node.Z;step.O=node.O; + step.EdgeType=edge?edge->Type:"start";step.EdgeDatabaseId=edge?edge->DatabaseId:0; + step.EdgeReverse=edge && edge->Reverse;return step; + }; + for(auto root:roots) + { + // Follow only explicitly recorded same-map ground edges until the first + // ordinary node. No virtual bridge or flight may invent an escape edge. + std::vector queue{root->Key}; + std::unordered_map previous; + previous[root->Key]=nullptr;std::string exit; + for(size_t i=0;isecond) + { + auto const& target=_nodes.at(edge.ToNode); + auto points=GetEdgePoints(edge.DatabaseId); + if(edge.Type!="ground" || !points || points->size()<2 || target.MapId!=root->MapId || + previous.count(target.Key) || (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)) || + !sAdventureAreaMgr->AllowsNode(player,target) || !sAdventureAreaMgr->AllowsEdge(player,edge))continue; + previous[target.Key]=&edge; + if(target.Type!="rescue"){exit=target.Key;break;} + queue.push_back(target.Key); + } + } + if(exit.empty())continue; + std::vector escape; + for(std::string key=exit;key!=root->Key;) + {auto edge=previous.at(key);escape.push_back(stepOf(_nodes.at(key),edge));key=edge->FromNode;} + std::reverse(escape.begin(),escape.end()); + std::vector approach;bool complete=false;std::string reached,error; + if(!forceTeleport && PlanAdaptiveRoute(player,root->Key,4000,200,approach,complete,reached,error,blockedEdgeIds) && complete) + { + route=std::move(approach);route.insert(route.end(),escape.begin(),escape.end());return true; + } + if(fallback.empty()) + {fallback.push_back(stepOf(*root,nullptr));fallback.insert(fallback.end(),escape.begin(),escape.end());fallbackNode=root->Key;} + } + // The special transfer permission applies to stranded Deeprun Tram bots + // only. Other maps may use a reachable escape network, never teleport to it. + if(player->GetMapId()!=369 || fallback.empty())return false; + route=std::move(fallback);teleportNode=fallbackNode;return true; +} diff --git a/src/server/game/World/ClientlessTravelGraphMgr.h b/src/server/game/World/ClientlessTravelGraphMgr.h index 204d80f918..71ccf8b00b 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.h +++ b/src/server/game/World/ClientlessTravelGraphMgr.h @@ -160,6 +160,14 @@ public: std::string& nodeKey, float& distance) const; + // Explicit recorded escape network, accessible only after ordinary recovery. + bool PlanRescueRoute(Player* player, std::vector& route, + std::string& teleportNode, bool forceTeleport = false, + std::unordered_set const* blockedEdgeIds = nullptr); + + bool HasRescueNodes(uint32 map) const + { for(auto const& entry:_nodes)if(entry.second.MapId==map && entry.second.Type=="rescue")return true;return false; } + uint32 GetNodeCount() const { return uint32(_nodes.size()); } ClientlessTravelNode const* FindNode(std::string const& key) const; uint32 GetEdgeCount() const { return _edgeCount; } @@ -174,6 +182,11 @@ private: ClientlessGraphProbeCache _positionProbes; std::unordered_map> _bridgeNeighborhoods; + bool RescueNodeAllowed(ClientlessTravelNode const& node) const + { return _rescuePlanning || node.Type != "rescue"; } + bool RescueEdgeAllowed(ClientlessTravelEdge const& edge) const + { return RescueNodeAllowed(_nodes.at(edge.FromNode)) && RescueNodeAllowed(_nodes.at(edge.ToNode)); } + bool _rescuePlanning = false; // scoped synchronous world-thread planning only bool _loaded = false; uint32 _edgeCount = 0; diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 0e6519746b..8291a06d12 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -1035,7 +1035,8 @@ namespace VianoBotCommands static bool IsClientlessTravelNodeType(std::string const& type) { - return type == "ground" + return type == "rescue" + || type == "ground" || type == "taxi" || type == "ship" || type == "zeppelin" @@ -1061,14 +1062,14 @@ static bool HandleClientlessNodeAdd(ChatHandler* handler, char const* args) if (key.empty()) { handler->SendSysMessage( - "Usage: .clientless node add [ground|taxi|ship|zeppelin|tram|portal|elevator]"); + "Usage: .clientless node add [ground|taxi|ship|zeppelin|tram|portal|elevator|rescue]"); return true; } if (!IsClientlessTravelNodeType(type)) { handler->PSendSysMessage( - "Unknown travel node type '%s'. Allowed: ground, taxi, ship, zeppelin, tram, portal, elevator.", + "Unknown travel node type '%s'. Allowed: ground, taxi, ship, zeppelin, tram, portal, elevator, rescue.", type.c_str()); return true; } diff --git a/tests/clientless/test_adventure_group_gather.py b/tests/clientless/test_adventure_group_gather.py index 829db42235..d0aecb1df0 100644 --- a/tests/clientless/test_adventure_group_gather.py +++ b/tests/clientless/test_adventure_group_gather.py @@ -10,6 +10,7 @@ code=r''' #include #include #include +#include using uint32=unsigned; #define TC_LOG_INFO(...) ((void)0) struct Position{float x=0;};struct Guid{unsigned GetCounter(){return 26;}}; @@ -17,10 +18,14 @@ struct Player{float x=50;unsigned map=0;bool combat=false;unsigned stopped=0;Gui 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; +struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Rejected;}status;unsigned cancels=0;bool StartTravel(Player*,Position const&){status.State=ClientlessTravelState::Moving;return true;}auto GetStatus(unsigned){return &status;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}}travel; auto sClientlessTravelMgr=&travel; +namespace ClientlessPositioning{float clearance=10;bool available=false;unsigned reserves=0; +float Clearance(Player*,Player const&){return clearance;}void Reserve(Player*,Player const&){++reserves;} +bool Nearby(Player*,Position const&,float,Position&spot,std::vector&){spot.x=3;return available;}} + struct AdventureBehaviorMgr{enum class Phase{Travel,OnSite};struct Record{ - std::string Intent="groupformation",Micro="profession_tools",Detail;time_t Deadline=400,GatherRetryAt=0; + std::string Intent="groupformation",Micro="profession_tools",Detail;time_t Deadline=400,GatherRetryAt=0,NextSpacing=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; @@ -48,6 +53,12 @@ int main(){ 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 + ClientlessPositioning::clearance=0;ClientlessPositioning::available=true; + assert(m.UpdateGathering(&p,r,399)&&r.NextSpacing==429&&travel.status.State==ClientlessTravelState::Moving); + auto cancels=travel.cancels; + assert(m.UpdateGathering(&p,r,399)&&travel.cancels==cancels); // spacing walk must finish + p.x=3;travel.status.State=ClientlessTravelState::Arrived; + assert(m.UpdateGathering(&p,r,399)&&travel.cancels==cancels); 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)); diff --git a/tests/clientless/test_adventure_group_travel.py b/tests/clientless/test_adventure_group_travel.py index f3be19d552..29675778c7 100644 --- a/tests/clientless/test_adventure_group_travel.py +++ b/tests/clientless/test_adventure_group_travel.py @@ -46,7 +46,7 @@ struct Player:Position{ ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0;Group*g=&group;Session session; Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}unsigned GetPhaseShift(){return 169;} Session*GetSession(){return &session;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool IsCharmed(){return charmed;} - bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;} + bool CanFly(){return mounted && flightCapable;}bool flightCapable=false;bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;} bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return visible && p->visible;}bool IsInAreaTriggerRadius(void*){return false;} }; struct WorldPacket{WorldPacket(int,int){}templateWorldPacket&operator<<(T){return *this;}}; @@ -71,7 +71,7 @@ enum class ClientlessTransportPhase{Waiting,Riding,Arrived,Failed}; struct TransportStatus{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;};struct Stop{unsigned Map=0;struct Point{Position Pos;};std::vectorPoints{{{0,0,0}}};}; struct Transport{TransportStatus status;auto GetStatus(unsigned){return &status;}Stop*GetStop(std::string const&){return nullptr;}bool Start(Player*,std::string const&,std::string const&,std::string&){return false;}}transport;auto sClientlessTransportMgr=&transport; enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed}; -struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;} +struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*){return false;} bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight; struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua; struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access; @@ -102,6 +102,20 @@ int main(){ leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0; // A native-capable follower uses flight, and safely lands after a kick. flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a); + // A stale owned ground catch-up route cannot suppress a visible-phase + // human leader taking off, even beyond normal visual detection distance. + leader.mounted=leader.flightCapable=leader.flying=true;leader.visible=false; + flight.enabled=true;flight.status.State=ClientlessFlightState::Failed; + mgr._records.at(26).RouteOwned=true;executor.status.State=ClientlessActionExecutionState::Moving; + auto cleared=executor.clears;auto flown=flight.follows; + mgr.Follow(&a);assert(executor.clears==cleared+1&&flight.follows==flown+1); + // Matching a grounded flying mount must never be immediately undone by + // the ground-follow dismount branch. + leader.flying=false;leader.visible=true;a.mounted=a.flightCapable=true; + flight.status.State=ClientlessFlightState::Arrived;leader.m_positionX=8; + mgr.Follow(&a);assert(a.mounted); + leader.mounted=leader.flightCapable=false;a.mounted=a.flightCapable=false; + flight.enabled=false;leader.m_positionX=20; // Taxi journey and its arrival grace pause distance counting. Completion must // not protect unrelated later ground separation. auto& state=observed;state.Kind=AdventureGroupTravelMgr::Journey::Taxi;state.Generation=1;state.Began=GameTime::now;state.To="taxi_42";state.GoalMap=1;state.Goal={1000,0,0};leader.taxi=true;leader.m_positionX=900; @@ -120,6 +134,20 @@ int main(){ // A blocked/unrecognized trip cannot grant unlimited separation protection. mgr._records.at(26).Pending.clear();state.Kind=AdventureGroupTravelMgr::Journey::None;state.Generation=4;leader.mapid=0;leader.m_positionX=1000;a.mapid=0;a.m_positionX=0;executor.status.State=ClientlessActionExecutionState::Completed; for(unsigned i=0;i<1200;++i){if(i%2==0)++GameTime::now;mgr.Observe(&a,500);}assert(mgr.Separated(&a));leader.m_positionX=10;mgr.Observe(&a,500);assert(!mgr.Separated(&a)); + // Nearby targets belonging to adjacent rows are distinct: a 2yd change + // cannot reuse an old spline that ends at a different formation position. + leader.m_positionX=20;leader.m_positionZ=0;leader.moving=false; + travel.status.State=ClientlessTravelState::Arrived;mgr.Follow(&a); + auto oldTarget=travel.status.Target;leader.m_positionX+=2;auto startsBefore=travel.starts; + mgr.Follow(&a);assert(travel.starts==startsBefore+1&&travel.status.Target.GetExactDist(oldTarget)>1.9f); + // A short stop during an executing route still preserves row distance. + leader.m_positionX=6;leader.moving=false;executor.status.State=ClientlessActionExecutionState::Moving; + std::vectorshortRoad{{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}}; + assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()<6); + // Boarding is released once the leader actually starts the native taxi. + leader.taxi=true;shortRoad={{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}}; + assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()==8); + leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed; // Recorded roads are bounded and validated, never a spline through a cliff. std::vectorroad;for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);leader.moving=false;map.cliff=true;assert(!mgr.AdaptRoad(&a,road));map.cliff=false;road.clear();for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);assert(mgr.AdaptRoad(&a,road) && road.back().GetPositionX()<=60); } diff --git a/tests/clientless/test_flight_departure.py b/tests/clientless/test_flight_departure.py index 801c678309..8255e3fdce 100644 --- a/tests/clientless/test_flight_departure.py +++ b/tests/clientless/test_flight_departure.py @@ -9,7 +9,7 @@ a=s.index('bool ClientlessFlightMgr::ApproachDeparture(');b=s.index('\nvoid Clie approach=s[a:b] a=s.index(' bool OpenColumn(');b=s.index(' char const * StateName',a) column=s[a:b] -a=s.index(' if(!airborne)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a) +a=s.index(' if(!existingMount)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a) dismount=s[a:b] a=s.index(' auto floor = sample(current.X, current.Y);');b=s.index(' player->StopMoving();',a) touchdown=s[a:b] @@ -73,7 +73,7 @@ struct ClientlessFlightMgr{ '''+touchdown+r''' return true; } - void KeepMount(Player*player,bool airborne){ + void KeepMount(Player*player,bool airborne){uint32 existingMount=airborne?123:0; '''+dismount+r''' } }; diff --git a/tests/clientless/test_free_flight_planner.py b/tests/clientless/test_free_flight_planner.py index f27867a7cb..de77c33302 100644 --- a/tests/clientless/test_free_flight_planner.py +++ b/tests/clientless/test_free_flight_planner.py @@ -56,6 +56,9 @@ struct PathGenerator {static inline unsigned probes=0;static inline bool availab std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;} ''' cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+''' +bool _rescuePlanning=false; +bool RescueNodeAllowed(ClientlessTravelNode const& n)const{return _rescuePlanning || n.Type!="rescue";} +bool RescueEdgeAllowed(ClientlessTravelEdge const& e)const{return RescueNodeAllowed(_nodes.at(e.FromNode))&&RescueNodeAllowed(_nodes.at(e.ToNode));} ClientlessGraphProbeCache _positionProbes;std::unordered_map> _bridgeNeighborhoods; std::unordered_map _nodes; std::unordered_map> _edges; diff --git a/tests/clientless/test_group_flight_follow.py b/tests/clientless/test_group_flight_follow.py index c56d790ea4..a74159d040 100644 --- a/tests/clientless/test_group_flight_follow.py +++ b/tests/clientless/test_group_flight_follow.py @@ -37,7 +37,7 @@ struct ClientlessFlightMgr{ Surface Sample(Player*,Record&,float,float){return {map.floor,ceiling,valid};} bool Begin(Player*,Position const&,std::string&,bool following){assert(following);++beginnings;return true;} void Fail(Player*,Record&r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;} - bool Follow(Player*,Position const&,std::string&);bool LandFollowing(Player*);bool AdvanceFollowing(Player*,Record&); + void HoldFollowing(Player*p,Record&r){if(r.Moving)p->StopMoving();r.Moving=false;}bool Follow(Player*,Position const&,std::string&);bool LandFollowing(Player*);bool AdvanceFollowing(Player*,Record&); }; ''' for sig in ['bool ClientlessFlightMgr::Follow(', 'bool ClientlessFlightMgr::LandFollowing(', 'bool ClientlessFlightMgr::AdvanceFollowing(']:code+=method(sig)+'\n' @@ -49,7 +49,7 @@ int main(){ assert(mgr.AdvanceFollowing(&p,r));assert(p.launches==1 && r.Segment.x>0 && r.Segment.x<=25 && r.Segment.z==20); // Moving the leader updates the destination without inventing a ground landing. target.Relocate(110,20,30);r.Elapsed=2000;assert(mgr.Follow(&p,target,error));assert(r.Status.Target.z==30 && r.TargetUpdated==2000); - clear=false;assert(!mgr.AdvanceFollowing(&p,r));assert(p.stops==1 && r.Status.State==ClientlessFlightState::Flying); + clear=false;r.Moving=true;assert(!mgr.AdvanceFollowing(&p,r));assert(p.stops==1 && r.Status.State==ClientlessFlightState::Flying); clear=true;ceiling=25;assert(!mgr.Follow(&p,target,error));ceiling=300; p.Relocate(110,20,30);r.Moving=true;assert(mgr.AdvanceFollowing(&p,r));assert(!r.Moving && p.stops==2); // holds in air, never dismounts at moving goal assert(mgr.LandFollowing(&p));assert(!r.Status.FollowingPlayer && r.Status.Landing.z==0 && r.Status.Travelled==0); diff --git a/tests/clientless/test_group_flight_mount.py b/tests/clientless/test_group_flight_mount.py new file mode 100644 index 0000000000..c4956e5cbc --- /dev/null +++ b/tests/clientless/test_group_flight_mount.py @@ -0,0 +1,79 @@ +"""Run native mount admission and stationary-flight publication adapters.""" +from pathlib import Path +import subprocess +import tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text() +def method(sig): + a=s.index(sig);b=s.index('{',a);e=b+1;n=1 + while n: + n+=(s[e]=='{')-(s[e]=='}');e+=1 + return s[a:e] +code=r''' +#include +#include +#include +using uint32=unsigned;using uint8=unsigned char; +constexpr int MAP_COMMON=0,UNIT_STATE_CASTING=1,MOUNT_CAPABILITY_FLAG_FLYING=1,SPELL_AURA_MOUNTED=1,SPELL_CAST_OK=0; +constexpr int MOVEMENTFLAG_FLYING=1,UNIT_FIELD_BYTES_1=1,UNIT_BYTES_1_OFFSET_ANIM_TIER=1,SMSG_MOVE_UPDATE=1; +enum class AnimTier:uint8{Ground=0,Fly=3}; +struct Position{void Relocate(Position const&){}}; +struct Map{struct Entry{int MapType=MAP_COMMON;}entry;Entry*GetEntry(){return &entry;}}map; +struct Guid{unsigned GetCounter(){return 49;}}; +struct WorldPacket{explicit WorldPacket(int){}}; +namespace GameTime{unsigned GetGameTimeMS(){return 100;}} +struct Player{ + bool alive=true,combat=false,teleport=false,transport=false,taxi=false,falling=false,water=false,outdoors=true,mounted=true,fly=false,casting=false,learned=true; + unsigned flags=0,tier=0,stops=0,updates=0,animations=0,removals=0,casts=0;int castResult=SPELL_CAST_OK; + struct Capability{unsigned Flags=MOUNT_CAPABILITY_FLAG_FLYING;}capability; + struct{Guid guid;Position pos;unsigned time=0;}m_movementInfo; + bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return taxi;} + bool IsFalling(){return falling;}bool IsInWater(){return water;}bool IsOutdoors(){return outdoors;}Map*GetMap(){return ↦}bool IsMounted(){return mounted;}bool CanFly(){return fly;} + bool HasUnitState(int){return casting;}unsigned GetZoneId(){return 1519;}Guid GetGUID(){return {};} + bool HasSpell(unsigned){return learned;}Capability*GetMountCapability(unsigned){return &capability;} + void StopMoving(){++stops;}void RemoveAurasByType(int){++removals;mounted=false;} + int CastSpell(Player*,unsigned spell,bool triggered){assert(spell==123&&!triggered);++casts;return castResult;} + bool HasUnitMovementFlag(int flag){return flags&flag;}void AddUnitMovementFlag(int flag){flags|=flag;} + unsigned GetByteValue(int,int){return tier;}void SetAnimTier(AnimTier t){tier=unsigned(t);++animations;} + Position GetPosition(){return {};}void WriteMovementInfo(WorldPacket&){assert(flags&MOVEMENTFLAG_FLYING);} + void SendMessageToSet(WorldPacket*,bool){++updates;} +}; +struct Battlefield{bool allowed=true;bool CanFlyIn(){return allowed;}}battle; +struct BattleMgr{Battlefield*GetBattlefieldToZoneId(Map*,unsigned){return &battle;}}battleMgr;auto sBattlefieldMgr=&battleMgr; +struct Profile{struct Mount{unsigned DisplayId,Weight;};std::vectorFlightMounts{{123,1}};}profile; +struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua; +struct Travel{unsigned cancels=0;void CancelTravel(unsigned){++cancels;}}travel;auto sClientlessTravelMgr=&travel; +struct SpellMgr{void*GetSpellInfo(unsigned){return nullptr;}}spells;auto sSpellMgr=&spells; +unsigned FlightType(void*){return 1;}unsigned urand(unsigned low,unsigned){return low;} +struct ClientlessFlightMgr{struct Record{bool Moving=false;};bool enabled=true; + bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*);void HoldFollowing(Player*,Record&); +}; +'''+method('bool ClientlessFlightMgr::PrepareFollowingMount(')+method('void ClientlessFlightMgr::HoldFollowing(')+r''' +int main(){ + ClientlessFlightMgr m;Player p; + assert(m.PrepareFollowingMount(&p)&&p.casts==1&&p.removals==1&&travel.cancels==1); + p.mounted=p.fly=true;assert(!m.PrepareFollowingMount(&p)&&p.casts==1); // retain native flight mount + p.fly=false;p.casting=true;assert(m.PrepareFollowingMount(&p)&&p.casts==1);p.casting=false; + p.combat=true;assert(!m.PrepareFollowingMount(&p));p.combat=false; + p.outdoors=false;assert(!m.PrepareFollowingMount(&p));p.outdoors=true; + p.transport=true;assert(!m.PrepareFollowingMount(&p));p.transport=false; + p.taxi=true;assert(!m.PrepareFollowingMount(&p));p.taxi=false; + p.falling=true;assert(!m.PrepareFollowingMount(&p));p.falling=false; + p.water=true;assert(!m.PrepareFollowingMount(&p));p.water=false; + battle.allowed=false;assert(!m.PrepareFollowingMount(&p));battle.allowed=true; + p.learned=false;assert(!m.PrepareFollowingMount(&p));p.learned=true; + p.capability.Flags=0;assert(!m.PrepareFollowingMount(&p));p.capability.Flags=1; + profile.FlightMounts[0].Weight=0;assert(!m.PrepareFollowingMount(&p));profile.FlightMounts[0].Weight=1; + p.castResult=1;assert(!m.PrepareFollowingMount(&p)); + ClientlessFlightMgr::Record r;r.Moving=true; + auto stops=p.stops;m.HoldFollowing(&p,r); + assert(!r.Moving&&p.stops==stops+1&&p.flags==MOVEMENTFLAG_FLYING&&p.tier==unsigned(AnimTier::Fly)&&p.updates==1); + m.HoldFollowing(&p,r);assert(p.updates==1&&p.stops==stops+1&&p.animations==1); // no idle packet flood + p.tier=0;m.HoldFollowing(&p,r);assert(p.updates==2&&p.animations==2); // restore interrupted animation +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Group flight mount: native admission, mount retention, casting wait, rejected capabilities, stationary flight animation and bounded publication passed') diff --git a/tests/clientless/test_rescue_executor.py b/tests/clientless/test_rescue_executor.py new file mode 100644 index 0000000000..89aadbdc32 --- /dev/null +++ b/tests/clientless/test_rescue_executor.py @@ -0,0 +1,70 @@ +"""Exercise the real rescue executor with explicit graph and native transfer doubles.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text() +a=s.index('bool ClientlessActionExecutorMgr::TryRescueTravel(');b=s.index('bool ClientlessActionExecutorMgr::RecoverTravel(',a) +code=r''' +#include +#include +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_WARN(...) ((void)0) +struct Node{unsigned MapId=369;std::string Type="rescue";float X=33,Y=40,Z=-4,O=0;}node; +struct Session{unsigned acks=0;void CompleteClientlessTeleport(){++acks;}}session; +struct Player{ + unsigned map=369,transfers=0,stops=0;bool alive=true,combat=false,teleport=false,transport=false,taxi=false,flying=false,transferOK=true; + bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;} + bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}unsigned GetMapId(){return map;} + void StopMoving(){++stops;}Session*GetSession(){return &session;} + bool TeleportTo(unsigned m,float,float,float,float){assert(m==369);++transfers;return transferOK;} +}; +struct ClientlessTravelRouteStep{std::string NodeKey;}; +struct Graph{bool available=true,transfer=false,forceSeen=false;unsigned calls=0; + bool PlanRescueRoute(Player*,std::vector&r,std::string&t,bool force,std::unordered_setconst*){ + ++calls;forceSeen=force;if(!available)return false;r={{"rescue"},{"exit"}};t=(transfer||force)?"rescue":"";return true;} + Node*FindNode(std::string const&){return &node;} +}graph;auto sClientlessTravelGraphMgr=&graph; +struct Areas{bool allowed=true;bool AllowsNode(Player*,Node const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas; +struct Core{bool managed=true;bool IsManaged(unsigned){return managed;}}core;auto sClientlessPlayerMgr=&core; +struct Travel{unsigned cancels=0;void CancelTravel(unsigned){++cancels;}}travel;auto sClientlessTravelMgr=&travel; +enum class ClientlessActionExecutionState{Moving,Failed}; +struct ClientlessActionExecutorMgr{ + struct Record{ + bool RescueActive=false,RescueWalkFailed=false,StopAtNode=true,Active=false; + unsigned RescueAttempts=0;std::unordered_setFailedEdgeDatabaseIds; + std::vectorRoute;unsigned RouteStep=99;std::vectorEdgePath; + bool DeferredStartActive=true,GroundExitActive=true,GroundResumeActive=true,TaxiActive=true,RecoveryWanderActive=true; + bool TransportActive=true,FlightActive=true,FlightApproachActive=true,PortalUseActive=true,FormationWaiting=true,GroundChunk=true,ReplanAtGroundEnd=true,GroundFlightExit=true; + bool FinalLocalLegActive=true,FinalLocalLegPending=true,RouteComplete=false; + std::string originalGoal="original-intent"; + struct{ClientlessActionExecutionState State=ClientlessActionExecutionState::Failed;std::string Detail;}Status; + };unsigned starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;} + bool TryRescueTravel(Player*,unsigned,Record&); +}; +'''+s[a:b]+r''' +int main(){ + ClientlessActionExecutorMgr m;Player p;ClientlessActionExecutorMgr::Record r; + assert(m.TryRescueTravel(&p,58,r)&&r.RescueActive&&r.RescueAttempts==1&&p.transfers==0); + assert(r.Route.size()==2&&r.RouteStep==0&&!r.StopAtNode&&!r.FinalLocalLegPending&&!r.TransportActive&&!r.ReplanAtGroundEnd); + assert(r.originalGoal=="original-intent");assert(!m.TryRescueTravel(&p,58,r)); + r.RescueWalkFailed=true;assert(m.TryRescueTravel(&p,58,r)&&graph.forceSeen&&p.transfers==1&&session.acks==1); + assert(!m.TryRescueTravel(&p,58,r)); // bounded attempts + r={};graph.transfer=true;p.map=0;assert(!m.TryRescueTravel(&p,58,r)&&p.transfers==1);p.map=369; + r={};areas.allowed=false;assert(!m.TryRescueTravel(&p,58,r));areas.allowed=true; + r={};node.Type="ground";assert(!m.TryRescueTravel(&p,58,r));node.Type="rescue"; + r={};p.combat=true;auto calls=graph.calls;assert(!m.TryRescueTravel(&p,58,r)&&graph.calls==calls);p.combat=false; + p.transport=true;assert(!m.TryRescueTravel(&p,58,r));p.transport=false; + p.flying=true;assert(!m.TryRescueTravel(&p,58,r));p.flying=false; + p.alive=false;assert(!m.TryRescueTravel(&p,58,r));p.alive=true; + core.managed=false;assert(!m.TryRescueTravel(&p,58,r));core.managed=true; + graph.available=false;assert(!m.TryRescueTravel(&p,58,r)); +} +''' +with tempfile.TemporaryDirectory()as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Rescue executor: walk first, tram-only validated transfer, native ACK, original goal retention, state reset, access and bounded retry guards passed') diff --git a/tests/clientless/test_rescue_graph.py b/tests/clientless/test_rescue_graph.py new file mode 100644 index 0000000000..ac6310534c --- /dev/null +++ b/tests/clientless/test_rescue_graph.py @@ -0,0 +1,61 @@ +"""Execute production adaptive planning and explicit recorded rescue search.""" +from pathlib import Path +import runpy,subprocess,tempfile +root=Path(__file__).resolve().parents[2] +e=runpy.run_path(str(root/'tests/clientless/test_free_flight_planner.py')) +s=e['s'] +def method(sig): + a=s.index(sig);b=s.index('{',a);n=1;c=b+1 + while n:n+=(s[c]=='{')-(s[c]=='}');c+=1 + return s[a:c] +preamble=e['preamble'].replace('uint32 GetMapId()const{return 0;}','uint32 map=369; uint32 GetMapId()const{return map;} float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}') +cls=e['cls'].replace('\n};',''' +std::vectorconst*GetEdgePoints(uint32 id)const{auto i=_edgePoints.find(id);return i==_edgePoints.end()?nullptr:&i->second;} +bool PlanRescueRoute(Player*,std::vector&,std::string&,bool=false,std::unordered_setconst* =nullptr); +bool FindNearestNodeToPosition(uint32,float,float,float,std::string&,float&)const; +};''') +code=preamble+e['structs']+cls+e['method']+method('bool ClientlessTravelGraphMgr::FindNearestNodeToPosition(')+method('bool ClientlessTravelGraphMgr::PlanRescueRoute(')+r''' +void node(ClientlessTravelGraphMgr&g,std::string k,float x,std::string type="ground"){ + ClientlessTravelNode n;n.Key=k;n.Type=type;n.X=x;n.MapId=369;g._nodes[k]=n; +} +void edge(ClientlessTravelGraphMgr&g,std::string a,std::string b,unsigned id){ + g._edges[a].push_back({id,a,b,"ground",10,false}); + ClientlessTravelEdgePoint p;p.X=g._nodes.at(a).X;p.MapId=369;g._edgePoints[id].push_back(p); + p.X=g._nodes.at(b).X;g._edgePoints[id].push_back(p); +} +int main(){ + ClientlessTravelGraphMgr g;Player p;p.x=5000; + node(g,"sw",0,"rescue");node(g,"if",2500,"rescue");node(g,"exit_sw",10);node(g,"exit_if",2490); + edge(g,"sw","exit_sw",1);edge(g,"if","exit_if",2);edge(g,"sw","if",3); + std::vectorr;std::string transfer,error,reached;bool complete=false; + // Rescue roots are excluded from ordinary nearest anchors and destinations. + float distance;assert(g.FindNearestNodeToPosition(369,0,0,0,reached,distance)&&reached=="exit_sw"); + assert(!g.PlanAdaptiveRoute(&p,"sw",4000,200,r,complete,reached,error)); + // A recorded rescue corridor cannot be borrowed as a normal shortcut, + // including virtual entries into the interior of its ground edges. + p.x=0;g.PlanAdaptiveRoute(&p,"exit_if",1,200,r,complete,reached,error);assert(!complete);p.x=5000; + // No traversable local start: only map369 may transfer to the nearest + // permitted authored rescue node with a recorded escape edge. + travel.complete=false; + assert(g.PlanRescueRoute(&p,r,transfer)&&transfer=="if"&&r.size()==2&&r.back().EdgeDatabaseId==2); + assert(!g._rescuePlanning); // scoped privilege cannot leak into later plans + p.map=0;assert(!g.PlanRescueRoute(&p,r,transfer));p.map=369; + // A reachable MMAP approach walks; it must not transfer a healthy bot. + travel.complete=true;p.x=2497; + assert(g.PlanRescueRoute(&p,r,transfer)&&transfer.empty()&&r.back().NodeKey=="exit_if"); + assert(g.PlanRescueRoute(&p,r,transfer,true)&&transfer=="if"); + // Blocked recording and faction/area denial select another legal escape; + // no bypass of a denied rescue root or native edge is allowed. + std::unordered_setblocked{2};travel.complete=false; + assert(g.PlanRescueRoute(&p,r,transfer,true,&blocked)&&transfer=="sw"); + access.blocked.insert("sw");assert(!g.PlanRescueRoute(&p,r,transfer,true,&blocked));access.blocked.clear(); + blocked={1,2,3};assert(!g.PlanRescueRoute(&p,r,transfer,true,&blocked)); + // A node without a recorded edge cannot become a teleport destination. + g._edgePoints.clear();assert(!g.PlanRescueRoute(&p,r,transfer,true)); +} +''' +with tempfile.TemporaryDirectory()as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Rescue graph: normal exclusion, walking first, recorded-only escapes, bounded tram transfer, access/blocked-edge checks and scoped planning passed')