diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index cecd01f2d6..14359a5474 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,43 @@ +# Aktuell: Quest-unabhängige Portalnutzung für AdventureBots ab Stufe 80 + +AdventureBots können keinen Questfortschritt erwerben. Graphgebundene öffentliche +`portal_use`-Verbindungen gelten deshalb für AdventureBots ab Stufe80 als +freigeschaltet. Normale Spieler, Testbots und niedrigere AdventureBots behalten +die nativen Spell-/Questbedingungen. Fraktion, Gebietskatalog, Node/Edge-Zugang, +sichtbares/erreichbares Portalobjekt und bestätigte Zielankunft bleiben zwingend. +Sind native Bedingungen erfüllt, läuft weiterhin der echte CMSG_GAMEOBJ_USE-Pfad. +Andernfalls teleportiert ausschließlich der qualifizierte AdventureBot zum beim +Import validierten festen Portalziel; der Executor verlangt danach neue Teleport- +Sequenz, richtige Karte und maximal5Yards Zielfehler. Portal-, Prerequisite-, +Flug- und Ground-Exit-Tests PASS, Build-j10 PASS, installiert. Frischer PID174775 +ready, Ports8085/8087. Elf Restbots nach Restart ohne alte Flug/Taxi/Transport- +Zustände geprüft. Temporäres Elanora-Override entfernt; Loader/Testbetrieb +wiederhergestellt (`enabled=1`, `schema=1`, `busy=0`, Pools `50/5/50/5`). Details unter Checkpoint +`doc/clientless/checkpoints/2026-11-01-adventure-portal-unlock/`. Kein Commit. + +--- + +# Aktuell: generischer Flug-Wiedereinstieg gebaut, installiert und live geprüft + +PlanAdaptiveRoute kann nun jeden erlaubten Graphknoten als begrenzten freien +Flug-Wiedereinstieg verwenden, ohne daraus einen vollständigen Luftgraphen zu +materialisieren. Makroanschlüsse werden priorisiert, maximal16 Flugschätzungen +pro Plan und6 pro Ausgangsknoten; der konkrete Zielknoten wird in jedem lokalen +Fenster zuerst geprüft. Portalquellen werden über ihre tatsächlichen ausgehenden +`portal`/`portal_use`-Kanten erkannt statt über Namenskonventionen. Am erreichten +Portal sind nur diese Kanten zulässig. Regression deckt Flug->beliebig benannter +Portalquellknoten->portal_use->Flug ab. Drei gezielte Tests, diff-check und zwei +abschließende Builds ausschließlich-j10 bestanden. Installiert; frischer PID +172446 ready, Ports8085/8087. Restbots77/153 nach Restart ohne alte Flug- oder +Transportzustände. Elanora erreichte live eine gültige freie Flugroute zu einem +regulären Zielknoten. Das SW-Twilight-Portal wurde wegen nativer Spell-/Quest- +Voraussetzungen für Elanora nicht gewählt, nicht wegen des Plannerbudgets. +Temporäres Override entfernt, Schedule wieder an, Loader/Testbetrieb restauriert; +Status enabled1/schema1/busy0, Pools50/5/50/5. Details im Checkpoint +`doc/clientless/checkpoints/2026-11-01-generic-flight-entry/`. Kein Commit/Push. + +--- + # Aktuell: finaler Gruppenbildungsmanager implementiert und installiert Nutzerauftrag: Ab zehn Minuten vor Ende von `groupformation` sollen Botgruppen diff --git a/doc/clientless/checkpoints/2026-11-01-adventure-portal-unlock/build-recovery.md b/doc/clientless/checkpoints/2026-11-01-adventure-portal-unlock/build-recovery.md new file mode 100644 index 0000000000..75710f38ca --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-01-adventure-portal-unlock/build-recovery.md @@ -0,0 +1,50 @@ +# Adventure portal unlock build recovery + +Safe logout was requested while the `-j10` build started in parallel. The build +completed successfully. At the five-real-minute installation boundary eleven +managed sessions remained (`combat=4`, `transport=0`, `flight/taxi=7`). `saveall` +completed before shutdown. + +| GUID | Name | Faction | Saved position | Observed transition/action | +|---:|---|---|---|---| +| 26 | Arthoran | Alliance | map0 `-11080.00,-483.45,54.49` | failed bounded free flight; mounted hold | +| 60 | Ashani | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 | +| 77 | Drakogzug | Horde | map1 `1852.91,-4570.06,24.98` | no flight/transport; failed departure recovery | +| 80 | Noralin | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 | +| 90 | Dorndin | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 | +| 93 | Gargar | Horde | map571 `2277.05,5202.07,12.49` | flight arrived; zeppelin state arrived | +| 96 | Darden | Alliance | map0 `-9381.78,-59.96,93.02` | failed bounded free flight; mounted hold | +| 97 | Talzul | Horde | map1 `2035.92,-3467.29,171.71` | failed bounded free flight; mounted hold | +| 132 | Braneron | Horde | map0 `2602.82,-112.11,34.02` | idle; no flight/transport | +| 135 | Ragnhild | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 | +| 153 | Ragnli | Alliance | map0 `-11787.06,-3180.12,-29.71` | idle; no flight/transport | + +Loader configuration and test admission remained disabled/paused through the +build, installation and restart window. Post-start recovery outcomes are added +below after verification. + +## Post-start verification + +- Fresh worldserver PID 174775 reached ready state; TCP 8085 and 8087 listen. +- All eleven recorded characters were loaded. Every prior free-flight, native + taxi and transport state was absent after login; ordinary schedules then + produced new behavior independently of the saved transition state. +- Targeted production-method tests exercise both paths: native GameObject use + when prerequisites pass, and the importer-validated fixed teleport when a + level-80+ AdventureBot lacks quest prerequisites. Arrival still requires a + changed clientless teleport sequence, correct map and <=5-yard target error. +- A level-85 Elanora live probe planned and traversed graph-bound capital portal + travel. The explicit Twilight Highlands acceptance and its actual native + condition state are documented below. + +## Explicit Twilight Highlands acceptance + +Elanora (level 85) was placed within native interaction range of Stormwind spawn +200627 and given only target `portal_go_200627_target`. Production navigation +log at 2026-11-01 16:57:17 records edge 1801 `portal_use`, spawn 200627, spell +85545, followed in the same second by `EDGE_COMPLETE` on map 0 with +`arrivalError=0.00`. Runtime area query at the destination resolved area 5140, +allowed, level band 84-85. The World DB has no spell-condition rows for 85545, +so this particular portal correctly used the unchanged native click path rather +than `PORTAL_ADVENTURE_UNLOCK`. The questless unlock path remains covered by the +production-method regression. Temporary Elanora profile and override removed. diff --git a/doc/clientless/checkpoints/2026-11-01-generic-flight-entry/build-recovery.md b/doc/clientless/checkpoints/2026-11-01-generic-flight-entry/build-recovery.md new file mode 100644 index 0000000000..d4293e4746 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-01-generic-flight-entry/build-recovery.md @@ -0,0 +1,103 @@ +# Generic flight-entry build recovery + +- Safe logout requested at approximately 2026-11-01 12:02 CET after pausing + `Clientless.Adventure.Loader` and TestBot admission. +- The `-j10` build ran in parallel with the safe drain, per the corrected build + policy in `AGENTS.md`, and completed successfully. +- At 12:08 CET, more than five real minutes later, 13 managed sessions remained: + 3 combat flags, 0 active transports and 9 native flight flags. The database + positions below are from the immediately preceding online snapshot; native + action/flight/transport diagnostics were also captured in the tmux console. + +| GUID | Name | faction | saved map / position | observed state | +|---:|---|---|---|---| +| 40 | Drakagmok | Horde | 1 / -4777.22 -1264.89 15.78 | native taxi 22->42; no free-flight/transport record | +| 48 | Elaniel | Horde | 1 / -4647.84 -1609.87 23.93 | native taxi 22->42; no free-flight/transport record | +| 54 | Falaeriel | Horde | 0 / 1875.88 -5018.08 166.81 | native taxi chain; prior free flight arrived | +| 77 | Drakogzug | Horde | 1 / 1857.05 -4568.03 24.98 | travel completed; no active flight/transport | +| 80 | Noralin | Alliance | 1 / 7990.23 1296.75 59.63 | native taxi; old ship record already Arrived | +| 83 | Mahuwa | Horde | 571 / 4234.13 3346.45 391.19 | failed free flight/permission timeout | +| 87 | Moggra | Horde | 1 / -3230.20 -1945.24 156.47 | native taxi to node55 | +| 91 | Norara | Horde | 571 / 3588.17 6692.83 208.30 | failed bounded flight corridor | +| 104 | Lillini | Alliance | 1 / 2669.46 1667.13 393.02 | native taxi; old flight/ship records Arrived | +| 118 | Lillina | Alliance | 0 / -8768.04 583.35 133.27 | failed bounded flight corridor | +| 132 | Braneron | Horde | 0 / 2539.96 -53.78 28.11 | idle; no flight/transport state | +| 146 | Loraria | Horde | 1 / -4215.50 -1913.36 131.73 | native taxi 22->42 | +| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state | + +No bot was individually force-logged-out, teleported, detached, or removed to +enable the build. Installation/restart uses the documented five-minute clean +shutdown exception. Post-start recovery results must be appended below. + +## Restart outcome + +Pending. + +## Second build drain + +- A second safe drain was requested at approximately 12:15 CET for the + flight-budget correction. The incremental `-j10` build again ran in parallel + and succeeded. +- After the five-minute gate, only two sessions remained online; neither had an + active free-flight or transport record: + +| GUID | Name | faction | saved map / position | observed state | +|---:|---|---|---|---| +| 77 | Drakogzug | Horde | 1 / 1852.91 -4570.06 24.98 | completed location travel; outdoors=0 | +| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state | + +The other three members of the earlier five-bot snapshot logged out safely +before this final snapshot. No individual force logout was used. + +## Third build drain + +- Final macro-connector prioritization build started in parallel with a safe + drain at approximately 15:47 CET and completed successfully with `-j10`. +- At the 15:52 CET installation gate, GUID 77 Drakogzug and GUID 153 Ragnli + remained in combat at the same positions recorded above. Both had zero active + free-flight and transport records. They were not interrupted or individually + logged out; the documented clean-shutdown exception was used. +## Fourth build drain (portal-continuation budget fix) + +- Safe logout requested at approximately 2026-11-01 15:54:30 CET. The `-j10` + build started immediately in parallel and completed successfully. +- At the five-minute installation boundary, two clientless sessions remained; + neither was in free flight, taxi travel or on a transport: + - GUID 77, Drakogzug, Horde, map 1 at `(1852.91, -4570.06, 24.98)`, area + 1637. Action had failed at `flight_departure`; no free-flight or transport + state was present. + - GUID 153, Ragnli, Alliance, map 0 at `(-11787.06, -3180.12, -29.71)`, area + 72. Action executor was idle; no free-flight or transport state was present. +- GUID 168 Elanora completed her active movement state and logged out safely + before the boundary. The temporary route-test Lua override was removed from + the source tree before this build. +- Testbot admission remained paused. AdventureBot loader remained disabled in + runtime configuration throughout the build and restart window. + +## Fifth build drain (destination-first probe fix) + +- Safe logout requested at approximately 2026-11-01 16:04:15 CET. Build + `-j10` again ran immediately in parallel and succeeded. +- GUID 168 Elanora completed her active free-flight transition and logged out + safely. At the five-minute installation boundary GUID 77 Drakogzug and GUID + 153 Ragnli remained online in the same saved positions recorded above. +- `saveall` completed before shutdown. Both remaining bots again reported no + free-flight or transport state. Drakogzug had a failed travel action at + `flight_departure`; Ragnli was idle. Test admission and the AdventureBot + loader remained paused/disabled. + +## Final runtime result + +- Fresh worldserver PID 172446 reached `(worldserver-daemon) ready`; TCP 8085 + and 8087 are listening. +- GUIDs 77 and 153 were loaded after restart. Both had no stale free-flight or + transport state and resumed ordinary behavior. +- The controlled Elanora plan produced a valid direct free-flight route from a + regular graph node to `portal_at_6354_source`; no MMAP attachment was needed. + The Stormwind Twilight Highlands portal did not compete because edge 1801 + correctly failed `PortalPrerequisites` for this character's native spell/ + quest state. The relevant portal is otherwise the first geometric connector, + so this was not a probe-budget failure. +- Temporary Elanora override removed. Schedule restored. Loader configuration + restored to enabled and test admission resumed. Loader status confirmed + `enabled=1 schema=1 busy=0`, pools `50/5/50/5`. diff --git a/doc/clientless/checkpoints/2026-11-01-instance-navigation/build-recovery.md b/doc/clientless/checkpoints/2026-11-01-instance-navigation/build-recovery.md new file mode 100644 index 0000000000..59c852a51f --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-01-instance-navigation/build-recovery.md @@ -0,0 +1,36 @@ +# Instance/navigation first-fix build recovery + +- Safe logout requested approximately 2026-11-01 11:26 CET after pausing + `Clientless.Adventure.Loader` and TestBot admission. +- Build was allowed to run in parallel by the user's explicit clarification; + installation/restart remained gated by zero bots or the five-minute limit. +- At 11:31 CET, nine managed bots remained after more than five minutes. + The server was therefore eligible for a clean shutdown under the agreed rule. +- The separately logged-in human `Allitester` (GUID 21) is not a managed bot. + +| GUID | Name | faction | saved map/position | observed state before shutdown | +|---:|---|---|---|---| +| 40 | Drakagmok | Horde | 1 / -4009.801 -2003.480 121.296 | groupformation; native taxi chain; no free-flight/transport record | +| 43 | Fizzira | Alliance | 0 / -8830.011 635.672 119.259 | social; failed held free flight, 94 segments | +| 60 | Ashani | Alliance | 1 / 7990.230 1296.750 59.628 | leisure; no flight capability/state | +| 77 | Drakogzug | Horde | 1 / 1852.910 -4570.060 24.981 | economy; flight and local travel arrived | +| 83 | Mahuwa | Horde | 571 / 2906.229 6057.960 80.671 | profession; zeppelin arrived; free flight failed touchdown confirmation | +| 96 | Darden | Alliance | 0 / -9337.900 12.744 105.163 | profession; failed held free flight, 162 segments | +| 121 | Braneven | Horde | 0 / 1964.780 -1567.223 123.819 | rest; failed held free flight, 462 segments | +| 132 | Braneron | Horde | 0 / 2539.960 -53.784 28.107 | no intent/travel/flight/transport state | +| 153 | Ragnli | Alliance | 0 / -11787.057 -3180.120 -29.714 | no intent/travel/flight/transport state | + +Aggregate immediately before diagnostics: nine sessions, three combat and six +flight flags. No bot was individually force-logged-out, teleported, detached or +removed to enable the build. + +## Restart outcome + +- Build `-j10`, installation and fresh startup succeeded. +- Fresh worldserver PID 153100 reached `(worldserver-daemon) ready...`; TCP + listeners 8085 and 8087 were verified. +- All nine bots were loaded explicitly before restoring the population loader. +- None retained an old free-flight or transport controller record. Normal new + intent travel/combat resumed, so no recovery teleport was necessary. +- Loader and TestBot admission were restored. Population status reported + `enabled=1 schema=1 busy=0`, targets `50/5/50/5`; staggered loading resumed. diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index 208757c457..8cd363bbdc 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -510,7 +510,11 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r) if ((r.Intent=="instance" || r.Intent=="raid")) { if (!ResolveInstance(player,r)) {Fallback(player,r);return;} - Queue(player,r,"travel_to_location","id",l->BaseKey.empty()?r.Location:l->BaseKey,true); + // The entrance is a first-class TravelGraph transition. Route to its + // source node instead of an approximate catalog point; the native + // AreaTrigger radius remains the final arrival contract. + Queue(player,r,"travel_to_node","node", + "portal_at_"+std::to_string(r.Instance.Entry)+"_source",true); } else if (l->IsArea) { @@ -1619,11 +1623,36 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) if (Busy(guid)) return; auto status = sClientlessActionExecutorMgr->GetStatus(guid); auto location = sAdventureLocationMgr->Find(r.Location); - bool arrived = status && status->State == ClientlessActionExecutionState::Completed && location && player->GetMapId() == location->Point.MapId; - if (arrived && location->IsArea) arrived = location->AreaId &&(player->GetAreaId() == location->AreaId || player->GetZoneId() == location->AreaId) && status->Detail != "best-effort travel completed at nearest useful reachable point"; - else if (arrived) arrived = player->GetExactDist(location->Point.X, location->Point.Y, location->Point.Z) <= 6; + bool const completed=status && status->State==ClientlessActionExecutionState::Completed; + bool arrived=completed && location && player->GetMapId()==location->Point.MapId; + char const* arrivalContract="catalog-radius"; + if (arrived && (r.Intent=="instance" || r.Intent=="raid") && r.Instance.Active) + { + arrivalContract="native-area-trigger"; + auto entrance=sAreaTriggerStore.LookupEntry(r.Instance.Entry); + arrived=entrance && player->GetMapId()==uint32(entrance->ContinentID) && + player->IsInAreaTriggerRadius(entrance); + } + else if (arrived && location->IsArea) + { + arrivalContract="area-or-zone"; + arrived=location->AreaId && (player->GetAreaId()==location->AreaId || + player->GetZoneId()==location->AreaId) && + status->Detail!="best-effort travel completed at nearest useful reachable point"; + } + else if (arrived) + arrived=player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z)<=6; if (! arrived) { + TC_LOG_WARN("server.adventurebot.navigation", + "INTENT_ARRIVAL_REJECT guid=%u intent='%s' location='%s' contract='%s' completed=%u status='%s' map=%u expectedMap=%u distance=%.2f entry=%u inTrigger=%u", + guid,r.Intent.c_str(),r.Location.c_str(),arrivalContract,completed?1u:0u, + status?status->Detail.c_str():"missing",player->GetMapId(), + location?location->Point.MapId:0u, + location?player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z):-1.0f, + r.Instance.Active?r.Instance.Entry:0u, + (r.Instance.Active && sAreaTriggerStore.LookupEntry(r.Instance.Entry) && + player->IsInAreaTriggerRadius(sAreaTriggerStore.LookupEntry(r.Instance.Entry)))?1u:0u); Fallback(player, r); return; } @@ -1632,7 +1661,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) r.State = Phase::OnSite; r.Detail = "performing intent at destination"; sClientlessLuaEngine->UpdateWorldPresence(player,r.Intent,true); - TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s'", guid, r.Intent.c_str(), r.Location.c_str()); + TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s' contract='%s'", guid, r.Intent.c_str(), r.Location.c_str(),arrivalContract); } if (r.State == Phase::OnSite) { diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 40c360969d..a1d69aa77e 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -990,6 +990,12 @@ bool ClientlessActionExecutorMgr::StartRouteStep( } if(!access) { + TC_LOG_WARN("server.adventurebot.navigation", + "ROUTE_ACCESS_DENIED guid=%u name='%s' step=%u/%u node='%s' edgeType='%s' " + "edgeDbId=%u nodeAllowed=%u", + characterGuid, player->GetName().c_str(), uint32(record.RouteStep + 1), + uint32(record.Route.size()), step.NodeKey.c_str(), step.EdgeType.c_str(), + step.EdgeDatabaseId, accessNode && sAdventureAreaMgr->AllowsNode(player,*accessNode) ? 1u : 0u); record.Status.State=ClientlessActionExecutionState::Failed; record.Status.Detail="route denied by area/node catalog";record.Active=false; return false; @@ -1583,7 +1589,11 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player, record.Status.Detail = reason; return false; }; - if(portal && !sClientlessTravelGraphMgr->PortalPrerequisites(player,step.EdgeDatabaseId)) + bool const nativePrerequisites = portal && + sClientlessTravelGraphMgr->PortalNativePrerequisites(player,step.EdgeDatabaseId); + bool const adventureUnlock = portal && !nativePrerequisites && + sClientlessTravelGraphMgr->AdventurePortalUnlockAllowed(player,step.EdgeDatabaseId); + if(portal && !nativePrerequisites && !adventureUnlock) return fail("native portal prerequisites not satisfied"); if (!portal || !record.RouteStep || step.EdgeReverse || player->GetMapId() != portal->Source.MapId || !player->IsAlive() || @@ -1626,11 +1636,27 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player, TC_LOG_INFO("server.adventurebot.navigation", "PORTAL_USE_START guid=%u edge=%u spawn=%u entry=%u spell=%u sourceMap=%u targetMap=%u", characterGuid, step.EdgeDatabaseId, portal->SpawnId, portal->Entry, portal->SpellId, portal->Source.MapId, step.MapId); - // Same packet handler as a player's click. Native Use/AI, party checks, - // spell cast restrictions and scripted conditions remain authoritative. - WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8); - usePacket << object->GetGUID(); - player->GetSession()->HandleGameObjectUseOpcode(usePacket); + if (adventureUnlock) + { + // These performative bots never complete quests. Reproduce the fixed, + // importer-validated destination of the bound public portal while the + // normal executor still verifies source object, range, faction/area + // policy and the resulting teleport sequence/arrival coordinates. + TC_LOG_INFO("server.adventurebot.navigation", + "PORTAL_ADVENTURE_UNLOCK guid=%u level=%u edge=%u spawn=%u", + characterGuid, player->getLevel(), step.EdgeDatabaseId, portal->SpawnId); + if (!player->TeleportTo(portal->Target.MapId, portal->Target.X, + portal->Target.Y, portal->Target.Z, portal->Target.O)) + return fail("adventure portal unlock teleport failed"); + } + else + { + // Same packet handler as a player's click. Native Use/AI, party checks, + // spell cast restrictions and scripted conditions remain authoritative. + WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8); + usePacket << object->GetGUID(); + player->GetSession()->HandleGameObjectUseOpcode(usePacket); + } return true; } @@ -2664,7 +2690,10 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode( float dz = player->GetPositionZ() - destination.Z; float remaining = std::sqrt(dx * dx + dy * dy + dz * dz); - if (remaining > 2.0f) + bool const portalApproach = record.RouteStep + 1 < record.Route.size() && + record.Route[record.RouteStep + 1].EdgeType == "portal_use"; + float const arrivalTolerance = portalApproach ? 5.0f : 2.0f; + if (remaining > arrivalTolerance) { uint32 const failedEdgeId = record.RouteStep < record.Route.size() diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index 19e48a445f..ca3f241ced 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -66,6 +66,18 @@ namespace if(!ClearRay(player,{x+offset.X,y+offset.Y,surface.Ground+0.8f},{x+offset.X,y+offset.Y,top}))return false; return true; } + bool OpenColumnFrom(Player* player,float x,float y,float z,float height) + { + // A height query from MAX_HEIGHT can legitimately return a bridge, + // tree canopy or other collision surface far above the player. Such a + // surface is useful as an obstacle, but it is not the player's support + // floor. Test the actual take-off column from the player's feet. + if (!std::isfinite(z) || height < 5.0f) return false; + for (auto offset : std::array{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}}) + if (!ClearRay(player,{x+offset.X,y+offset.Y,z+0.8f}, + {x+offset.X,y+offset.Y,z+height})) return false; + return true; + } char const * StateName(ClientlessFlightState state) { switch (state) @@ -398,13 +410,30 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f && player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5}); float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6); - if (!origin.Valid || (!airborne && (!std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3))) + bool const localSupport=std::isfinite(feet) && feet>INVALID_HEIGHT+1 && + std::fabs(feet-player->GetPositionZ())<=3; + bool const overheadSurface=origin.Valid && origin.Ground>player->GetPositionZ()+3; + bool const openFromFeet=overheadSurface && OpenColumnFrom(player,player->GetPositionX(), + player->GetPositionY(),player->GetPositionZ(), + std::min(record.Settings.Clearance,origin.Ground-player->GetPositionZ()-1.0f)); + // Not falling/in water was verified above. If the global height sample + // selected a remote surface above us, a clear local vertical column is a + // valid departure even though that remote surface cannot be our floor. + bool const supportedDeparture=localSupport || openFromFeet; + if (!origin.Valid || (!airborne && !supportedDeparture)) { - if(std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true; + if(localSupport && ApproachDeparture(player,record,following))return true; error = "departure-unavailable: no supported open takeoff point"; - TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u mounted=%u canFly=%u",player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,origin.Valid?1:0,player->IsMounted()?1:0,player->CanFly()?1:0); + TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u localSupport=%u overhead=%u openFromFeet=%u mounted=%u canFly=%u", + player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet, + origin.Valid?1:0,localSupport?1:0,overheadSurface?1:0,openFromFeet?1:0, + player->IsMounted()?1:0,player->CanFly()?1:0); return false; } + if(openFromFeet) + ClientlessTravelTelemetry::Note(player,"flight-departure-overhead-surface", + "z="+std::to_string(player->GetPositionZ())+" surface="+std::to_string(origin.Ground)+ + " feet="+std::to_string(feet)); bool foundLanding = false; std::array landingRejected{}; // surface, water, column, MMAP, area auto landingSummary=[&]() diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index a84a7ea20b..d084604a98 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -3,6 +3,7 @@ #include "ClientlessTravelGraphMgr.h" #include "ClientlessTransportMgr.h" #include "ClientlessFlightMgr.h" +#include "ClientlessLuaEngine.h" #include "DatabaseEnv.h" #include "Log.h" @@ -125,7 +126,7 @@ ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDat return itr != _portalUses.end() ? &itr->second : nullptr; } -bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const +bool ClientlessTravelGraphMgr::PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const { auto portal=GetPortalUse(edgeDatabaseId); auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr; @@ -136,6 +137,23 @@ bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 e return !portal->Requirements.empty(); } +bool ClientlessTravelGraphMgr::AdventurePortalUnlockAllowed(Player const* player, + uint32 edgeDatabaseId) const +{ + // AdventureBots cannot acquire quest progress. Once they have reached the + // Cataclysm level range, graph-bound public travel portals are therefore + // considered unlocked. This does not bypass faction/area/node policy or + // make arbitrary, unbound GameObjects usable. + return player && player->getLevel() >= 80 && GetPortalUse(edgeDatabaseId) && + sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); +} + +bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const +{ + return PortalNativePrerequisites(player,edgeDatabaseId) || + AdventurePortalUnlockAllowed(player,edgeDatabaseId); +} + bool ClientlessTravelGraphMgr::Reload() { _positionProbes.Clear(); @@ -623,6 +641,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( } bool const flightEnabled = sClientlessFlightMgr->Enabled(player); constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16; + // Preserve part of the global budget for nodes reached later in the + // search. In particular, a flight to a portal source must still leave + // enough probes for the flight from its destination map to the goal. + constexpr std::uint64_t MaxFlightEstimatesPerOrigin = 6; auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false) { if (!flightEnabled || from.MapId != map || @@ -815,9 +837,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( ClientlessTravelNode departure; departure.Key="flight_departure"; departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ(); - float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)? - std::numeric_limits::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable); - if(std::isfinite(air) && (!reachable || air::infinity(); + if (!reachable && !(blockedBridgeKeys && + blockedBridgeKeys->count("flight:flight_departure->"+node.Key))) + air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true); + if(std::isfinite(air) && !reachable) {flightRoots.insert(node.Key);startCost=air;} else { @@ -1220,29 +1248,37 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( float bestGoalCost = std::numeric_limits::infinity(); std::string bestGoalState; bool bestFinalFlight = false; - // Free flight is a macro connector, not a reason to turn every graph map - // into a complete directed graph. A flyer only needs direct links to the - // requested destination and to nodes that enter a map-changing or physical - // transport edge (portal, ship, zeppelin or tram). Taxi nodes are excluded: - // a free flyer can already reach their same-map destination directly, and - // adding every taxi source recreates an almost-complete graph. Ordinary - // same-map ground - // nodes remain reachable through their recorded graph, while a location - // goal is evaluated directly from every expanded node below. - std::unordered_set flightConnectorNodes; - flightConnectorNodes.insert(destinationNode); - for (auto const& edgeGroup : _edges) - for (ClientlessTravelEdge const& edge : edgeGroup.second) + // Every permitted node is a potential way back into the persistent graph. + // Do not materialize an all-to-all air graph, though: candidates are ranked + // on demand below and the shared per-plan Estimate cap remains authoritative. + // This is important for isolated anchors such as the capital Cataclysm + // portals, but is deliberately not portal-specific. + auto flightRemainderLowerBound = [&](ClientlessTravelNode const& candidate) + { + ClientlessTravelNode const& destination = destinationItr->second; + auto lowerBoundToGoal = [&](ClientlessTravelNode const& node) { - auto fromItr = _nodes.find(edge.FromNode); - auto toItr = _nodes.find(edge.ToNode); - bool const changesMap = fromItr != _nodes.end() && toItr != _nodes.end() && - fromItr->second.MapId != toItr->second.MapId; - bool const physicalTransport = edge.Type == "portal" || edge.Type == "ship" || - edge.Type == "zeppelin" || edge.Type == "tram" || edge.Type == "transport"; - if (changesMap || physicalTransport) - flightConnectorNodes.insert(edge.FromNode); - } + if (node.MapId != destination.MapId) + return std::numeric_limits::infinity(); + float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z; + return 40.0f + std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f; + }; + float best = lowerBoundToGoal(candidate); + auto edges = _edges.find(candidate.Key); + if (edges != _edges.end()) + for (ClientlessTravelEdge const& edge : edges->second) + { + if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) + continue; + if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId)) + continue; + auto next = _nodes.find(edge.ToNode); + if (next == _nodes.end()) + continue; + best = std::min(best, edge.Cost + lowerBoundToGoal(next->second)); + } + return best; + }; // C8.20: a virtual entry is represented in the queue by its FAR // endpoint. Check exits BEFORE that endpoint independently: otherwise // an exit halfway along the road would pay for the entire remaining edge. @@ -1379,10 +1415,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // ground transit. Once reached during a planned route, its only legal // continuation is the portal edge itself. Physical AreaTriggers may // fire even before the route executor advances to that edge. - bool const isPortalSource = - current.Value.Node.rfind("portal_at_", 0) == 0 && - current.Value.Node.size() >= 7 && - current.Value.Node.compare(current.Value.Node.size() - 7, 7, "_source") == 0; + auto const sourceEdges = _edges.find(current.Value.Node); + bool const isPortalSource = sourceEdges != _edges.end() && + std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(), + [](ClientlessTravelEdge const& edge) + { return edge.Type == "portal" || edge.Type == "portal_use"; }); bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end(); // Persistent TravelGraph edges always reset the bridge guard. @@ -1393,7 +1430,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( for (ClientlessTravelEdge const& edge : edgeItr->second) { if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue; - if (portalArrival && edge.Type != "portal") + if (portalArrival && edge.Type != "portal" && edge.Type != "portal_use") continue; if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId)) continue; @@ -1421,15 +1458,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // Dynamic flights connect real nodes; never the synthetic entry waypoints. // Include them before the local-bridge guard: a flight is a separate // macro travel mode with its own native departure and landing checks. - if (flightEnabled && !portalArrival) + if (flightEnabled && !portalArrival && trace.FlightCalls < MaxFlightEstimatesPerPlan) { auto const& origin=_nodes.at(current.Value.Node); - for (std::string const& targetKey : flightConnectorNodes) + std::vector> macroFlightCandidates; + std::vector> flightCandidates; + flightCandidates.reserve(_nodes.size()); + for (auto const& candidate : _nodes) { - auto targetItr = _nodes.find(targetKey); - if (targetItr == _nodes.end()) - continue; - auto const& target=targetItr->second; + auto const& target=candidate.second; if (target.Key==origin.Key || target.MapId!=origin.MapId) continue; State next{target.Key,false};std::string key=stateKey(next); auto old=distance.find(key); @@ -1441,6 +1478,58 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue; if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue; if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue; + float const remainder = flightRemainderLowerBound(target); + if (!std::isfinite(remainder)) + continue; + bool macroConnector = target.Key == destinationNode; + auto outgoing = _edges.find(target.Key); + if (outgoing != _edges.end()) + for (ClientlessTravelEdge const& edge : outgoing->second) + { + auto next = _nodes.find(edge.ToNode); + bool const changesMap = next != _nodes.end() && + next->second.MapId != target.MapId; + bool const physical = edge.Type == "portal" || + edge.Type == "portal_use" || edge.Type == "ship" || + edge.Type == "zeppelin" || edge.Type == "tram" || + edge.Type == "transport"; + if (changesMap || physical) + { + macroConnector = true; + break; + } + } + auto& candidates = macroConnector ? macroFlightCandidates : flightCandidates; + candidates.emplace_back(lowerBound + remainder, target.Key); + } + std::sort(macroFlightCandidates.begin(), macroFlightCandidates.end(), + [&](auto const& left, auto const& right) + { + // Every newly reached portal/transport endpoint must get + // one chance to finish the requested journey before the + // bounded probe window is spent on further connectors. + bool const leftIsDestination = left.second == destinationNode; + bool const rightIsDestination = right.second == destinationNode; + if (leftIsDestination != rightIsDestination) + return leftIsDestination; + return left < right; + }); + std::sort(flightCandidates.begin(), flightCandidates.end()); + macroFlightCandidates.insert(macroFlightCandidates.end(), + flightCandidates.begin(), flightCandidates.end()); + std::uint64_t const callsAtOrigin = trace.FlightCalls; + for (auto const& ranked : macroFlightCandidates) + { + if (trace.FlightCalls >= MaxFlightEstimatesPerPlan || + trace.FlightCalls - callsAtOrigin >= MaxFlightEstimatesPerOrigin) + break; + auto const& target = _nodes.at(ranked.second); + State next{target.Key,false};std::string key=stateKey(next); + auto old=distance.find(key); + auto const blockedFlight = blockedBridgeKeys && + blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key); + if (blockedFlight) + continue; float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z); if (!std::isfinite(cost)) continue; float score=current.Cost+cost; @@ -1464,10 +1553,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // road at an MMAP-reachable waypoint. Only remaining portions of // stored, directed ground edges are admitted; no off-graph shortcut. // A portal source may not be treated as an ordinary intermediate node. - bool const futurePortalSource = - from.Key.rfind("portal_at_", 0) == 0 && - from.Key.size() >= 7 && - from.Key.compare(from.Key.size() - 7, 7, "_source") == 0; + auto const futureEdges = _edges.find(from.Key); + bool const futurePortalSource = futureEdges != _edges.end() && + std::any_of(futureEdges->second.begin(), futureEdges->second.end(), + [](ClientlessTravelEdge const& edge) + { return edge.Type == "portal" || edge.Type == "portal_use"; }); if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 && !(blockedBridgeKeys && blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry")))) diff --git a/src/server/game/World/ClientlessTravelGraphMgr.h b/src/server/game/World/ClientlessTravelGraphMgr.h index 3a414fea6b..aa68af6836 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.h +++ b/src/server/game/World/ClientlessTravelGraphMgr.h @@ -177,6 +177,8 @@ public: static bool ResolvePortalUse(uint32 spawnId, ClientlessPortalUse& portal, std::string& error); ClientlessPortalUse const* GetPortalUse(uint32 edgeDatabaseId) const; + bool PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const; + bool AdventurePortalUnlockAllowed(Player const* player,uint32 edgeDatabaseId) const; bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const; private: diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 208d39723e..d2bdd612f6 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -3980,6 +3980,7 @@ Appender.Console=1,3,0 Appender.Server=2,2,0,Server.log,w Appender.GM=2,2,15,gm/gm_%s.log Appender.DBErrors=2,2,0,DBErrors.log +Appender.AdventureBotNavigation=2,2,7,adventurebot-navigation.log,a,67108864 # Logger config values: Given a logger "name" # Logger.name @@ -4005,6 +4006,7 @@ Logger.commands.gm=3,Console GM Logger.scripts.hotswap=3,Console Server Logger.sql.sql=5,Console DBErrors Logger.sql.updates=3,Console Server +Logger.server.adventurebot.navigation=2,AdventureBotNavigation #Logger.mmaps=3,Server #Logger.achievement=3,Console Server diff --git a/tests/clientless/test_adventure_intent_fallbacks.py b/tests/clientless/test_adventure_intent_fallbacks.py index 69810c684e..c78e95ed22 100644 --- a/tests/clientless/test_adventure_intent_fallbacks.py +++ b/tests/clientless/test_adventure_intent_fallbacks.py @@ -22,6 +22,19 @@ assert "r.GroupOverride" in follower_hold assert "r.Dirty=false" in follower_hold assert "GROUP_MEMBER_FALLBACK_HOLD" in follower_hold +begin_start = source.index("void AdventureBehaviorMgr::Begin(") +begin_end = source.index("void AdventureBehaviorMgr::Fallback(", begin_start) +begin = source[begin_start:begin_end] +assert '"portal_at_"+std::to_string(r.Instance.Entry)+"_source"' in begin +assert 'Queue(player,r,"travel_to_node","node"' in begin + +update_start = source.index("void AdventureBehaviorMgr::Update(Player * player") +update_end = source.index("void AdventureBehaviorMgr::FinishInstance(", update_start) +update = source[update_start:update_end] +assert 'arrivalContract="native-area-trigger"' in update +assert "player->IsInAreaTriggerRadius(entrance)" in update +assert "INTENT_ARRIVAL_REJECT" in update + protect_start = source.index("bool AdventureBehaviorMgr::ProtectGroupLeave(") protect_end = source.index("void AdventureBehaviorMgr::InterruptForInteraction(", protect_start) protection = source[protect_start:protect_end] @@ -35,4 +48,9 @@ for invariant in ( ): assert invariant in protection -print("Instance visit retention, inherited group-target hold and formation leave protection passed.") +flight = (root / "src/server/game/World/ClientlessFlightMgr.cpp").read_text() +assert "OpenColumnFrom" in flight +assert "overheadSurface" in flight +assert "flight-departure-overhead-surface" in flight + +print("Instance graph entrance/arrival contract, visit retention, flight overhead handling, group-target hold and formation leave protection passed.") diff --git a/tests/clientless/test_free_flight_planner.py b/tests/clientless/test_free_flight_planner.py index afa6315de1..166e7ba91d 100644 --- a/tests/clientless/test_free_flight_planner.py +++ b/tests/clientless/test_free_flight_planner.py @@ -34,12 +34,14 @@ struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious struct Travel {bool complete=true;void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);n.Complete=complete;}} travel; auto sClientlessTravelMgr=&travel; struct Flight { - bool enabled=false;float minimumX=-10000; + bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9; + float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9; void BeginPlanning(Player const* = nullptr){} bool Enabled(Player const*){return enabled;} bool Preferred(Player const*,uint32,Position const&){return false;} float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){ - float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits::infinity(); + bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f; + float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&std::fabs(b.x-forbiddenTargetX)>0.1f&&!forbiddenPair&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits::infinity(); } } flight; auto sClientlessFlightMgr=&flight; @@ -162,6 +164,29 @@ travel.complete=true;route.clear(); assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); assert(route.front().NodeKey!="flight_departure"); travel.complete=true; +// Any allowed graph node can be a bounded, on-demand flight re-entry. The +// planner must not require a portal/transport node type or an MMAP bridge. +flight.forbiddenTargetX=2000; +ClientlessTravelGraphMgr genericEntry;node(genericEntry,"A",0);node(genericEntry,"R",500);node(genericEntry,"B",2000); +edge(genericEntry,"R","B",88,10,false,"ground"); +travel.complete=false;route.clear(); +assert(genericEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); +assert(route.size()==3&&route[0].NodeKey=="A"&&route[1].NodeKey=="R"); +assert(route[1].EdgeType=="free_flight"&&route[2].EdgeDatabaseId==88); +flight.forbiddenTargetX=1e9;travel.complete=true; +// Keep probe budget for a later graph state: fly to a generic portal source, +// cross it, and then fly to the destination. A rejected direct flight must +// not consume all estimates at the initial node. +flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000; +ClientlessTravelGraphMgr portalEntry;node(portalEntry,"A",0);node(portalEntry,"P",500); +node(portalEntry,"Q",1500);node(portalEntry,"B",2000); +edge(portalEntry,"P","Q",89,1,false,"portal_use"); +travel.complete=false;route.clear(); +assert(portalEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done); +assert(route.size()==4&&route[1].NodeKey=="P"&&route[1].EdgeType=="free_flight"); +assert(route[2].NodeKey=="Q"&&route[2].EdgeType=="portal_use"); +assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight"); +flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true; std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n"; } ''' diff --git a/tests/clientless/test_ground_exit_continuation.py b/tests/clientless/test_ground_exit_continuation.py index 096ba85477..bed958865b 100644 --- a/tests/clientless/test_ground_exit_continuation.py +++ b/tests/clientless/test_ground_exit_continuation.py @@ -77,7 +77,7 @@ struct Flight { bool enabled=false,starts=false,recovery=false;int launches=0;FlightStatus status; void BeginPlanning(){} float Estimate(Player*,uint32,Position const&,Position const&,bool fallback=false){recovery=fallback;return enabled?100:std::numeric_limits::infinity();} - bool Start(Player*,Position const&,std::string&){++launches;return starts;} + bool Start(Player*,Position const&,std::string&,bool=false){++launches;return starts;} FlightStatus const* GetStatus(uint32){return &status;} } flight; auto sClientlessFlightMgr=&flight; diff --git a/tests/clientless/test_portal_use.py b/tests/clientless/test_portal_use.py index c6b8ab0b90..552e30e34e 100644 --- a/tests/clientless/test_portal_use.py +++ b/tests/clientless/test_portal_use.py @@ -68,6 +68,8 @@ struct Player { bool IsInPhase(GameObject*){return phase;}bool CanSeeOrDetect(GameObject*){return visible;} GameObject* GetGameObjectIfCanInteractWith(int guid){return inRange&&guid==go.guid?&go:nullptr;} float GetPositionX(){return x;}float GetPositionY(){return y;}float GetPositionZ(){return z;} + unsigned getLevel(){return 85;} + bool TeleportTo(uint32 map,float tx,float ty,float tz,float){pending=true;world=false;mapId=map;x=tx;y=ty;z=tz;return true;} std::string GetName(){return "Arthoran";}void StopMoving(){} }; void Session::HandleGameObjectUseOpcode(WorldPacket& packet) { @@ -80,8 +82,11 @@ void Session::CompleteClientlessTeleport(){ player->mapId=wrongTarget?1:0;player->x=-8998;player->y=861;player->z=29; } struct Node {std::string Key;uint32 MapId=530;}; -struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;}; -struct Graph {bool prerequisites=true;bool PortalPrerequisites(Player*,uint32){return prerequisites;}bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph; +struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;struct{uint32 MapId=0;float X=-8998,Y=861,Z=29,O=0;}Target;}; +struct Graph {bool nativePrerequisites=true,adventureUnlock=false; + bool PortalNativePrerequisites(Player*,uint32){return nativePrerequisites;} + bool AdventurePortalUnlockAllowed(Player*,uint32){return adventureUnlock;} + bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph; struct Travel {void CancelTravel(uint32){}} travel; struct Lua {void DismountGroundMount(Player*,const char*){}} lua; auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua; @@ -105,7 +110,11 @@ harness += methods harness += r''' int main(){ ClientlessActionExecutorMgr mgr; - {Player p;Record r;graph.prerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.prerequisites=true;} + {Player p;Record r;graph.nativePrerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.nativePrerequisites=true;} + {Player p;Record r;graph.nativePrerequisites=false;graph.adventureUnlock=true; + assert(mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0&&p.pending); + mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.sequence==1); + graph.nativePrerequisites=true;graph.adventureUnlock=false;} {Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world); mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);} // The global lifecycle may have ACKed a delayed cast before our next update.