diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 29896d0287..73ccbb237f 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace @@ -127,6 +128,15 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c record.Status = {}; record.Status.ActionName = action.Name; record.Status.State = ClientlessActionExecutionState::SelectingTarget; + record.FinalLocalLegPending = false; + record.FinalLocalLegActive = false; + record.FinalMapId = 0; + record.FinalX = 0.0f; + record.FinalY = 0.0f; + record.FinalZ = 0.0f; + record.FinalO = 0.0f; + record.FinalDatabaseId = 0; + record.FinalLocationId.clear(); if (action.Name == "approach") return StartApproach(player, action, record); @@ -137,6 +147,9 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c if (action.Name == "travel_to_node") return StartTravelToNode(player, action, record); + if (action.Name == "travel_to_location") + return StartTravelToLocation(player, action, record); + record.Status.State = ClientlessActionExecutionState::Failed; record.Status.Detail = "unsupported action"; record.Active = false; @@ -496,6 +509,200 @@ bool ClientlessActionExecutorMgr::StartTravelToNode( return StartRouteStep(player, characterGuid, record); } + +bool ClientlessActionExecutorMgr::StartTravelToLocation( + Player* player, + ClientlessAction const& action, + Record& record) +{ + uint32 const characterGuid = player->GetGUID().GetCounter(); + + auto find = [&](char const* key) -> std::string const* + { + auto itr = action.Parameters.find(key); + return itr != action.Parameters.end() ? &itr->second : nullptr; + }; + + std::string locationId; + if (std::string const* id = find("id")) + locationId = *id; + else if (std::string const* location = find("location")) + locationId = *location; + + if (locationId.empty()) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "travel_to_location requires id"; + record.Active = false; + return false; + } + + ClientlessWorldPoint const* point = sClientlessWorldPointMgr->FindById(locationId); + if (!point) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "unknown location: " + locationId; + record.Active = false; + return false; + } + + float startRadius = 500.0f; + float directRadius = 300.0f; + float anchorRadius = 1000.0f; + + if (std::string const* radius = find("start_radius")) + startRadius = ParseFloat(*radius, startRadius); + + // Backward compatibility with the first C8.9 API. Legacy local_radius + // initializes both concepts; the new explicit parameters below win. + if (std::string const* radius = find("local_radius")) + { + float const legacyRadius = ParseFloat(*radius, 0.0f); + if (legacyRadius > 0.0f) + { + directRadius = legacyRadius; + anchorRadius = legacyRadius; + } + } + + if (std::string const* radius = find("direct_radius")) + directRadius = ParseFloat(*radius, directRadius); + + if (std::string const* radius = find("anchor_radius")) + anchorRadius = ParseFloat(*radius, anchorRadius); + + if (startRadius <= 0.0f) + startRadius = 500.0f; + if (directRadius <= 0.0f) + directRadius = 300.0f; + if (anchorRadius <= 0.0f) + anchorRadius = 1000.0f; + + record.FinalMapId = point->MapId; + record.FinalX = point->X; + record.FinalY = point->Y; + record.FinalZ = point->Z; + record.FinalO = point->O; + record.FinalDatabaseId = point->DatabaseId; + record.FinalLocationId = point->Id; + record.Status.TargetName = "location:" + point->Id; + record.Status.TargetGuid = 0; + record.Status.TargetEntry = point->DatabaseId; + + float directDistance = std::numeric_limits::infinity(); + if (player->GetMapId() == point->MapId) + { + float const dx = player->GetPositionX() - point->X; + float const dy = player->GetPositionY() - point->Y; + float const dz = player->GetPositionZ() - point->Z; + directDistance = std::sqrt(dx * dx + dy * dy + dz * dz); + } + + // Only genuinely local destinations may bypass the macro graph. The + // destination anchor may intentionally be much farther from the exact + // Location, so directRadius must stay independent from anchorRadius. + if (directDistance <= directRadius) + { + record.FinalLocalLegPending = true; + record.Active = true; + return StartFinalLocalLeg(player, characterGuid, record); + } + + record.Route.clear(); + record.RouteStep = 0; + record.EdgePath.clear(); + + std::string anchorNode; + std::string error; + if (!sClientlessTravelGraphMgr->PlanRouteToPosition( + player, + point->MapId, + point->X, + point->Y, + point->Z, + startRadius, + anchorRadius, + record.Route, + anchorNode, + error)) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = error; + record.Active = false; + + TC_LOG_INFO("server.adventurebot", + "[AdventureBot] travel-location-failed guid=%u name='%s' location='%s' reason='%s'", + characterGuid, + player->GetName().c_str(), + locationId.c_str(), + error.c_str()); + return false; + } + + record.FinalLocalLegPending = true; + record.Active = true; + + TC_LOG_INFO("server.adventurebot", + "[AdventureBot] travel-location-planned guid=%u name='%s' location='%s' anchor='%s' steps=%u directRadius=%.1f anchorRadius=%.1f", + characterGuid, + player->GetName().c_str(), + point->Id.c_str(), + anchorNode.c_str(), + uint32(record.Route.size()), + directRadius, + anchorRadius); + + return StartRouteStep(player, characterGuid, record); +} + +bool ClientlessActionExecutorMgr::StartFinalLocalLeg( + Player* player, + uint32 characterGuid, + Record& record) +{ + if (!player || player->GetMapId() != record.FinalMapId) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "location final leg is on another map"; + record.Active = false; + record.FinalLocalLegPending = false; + return false; + } + + Position destination; + destination.Relocate( + record.FinalX, + record.FinalY, + record.FinalZ, + record.FinalO); + + if (!sClientlessTravelMgr->StartTravel(player, destination)) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "location final local travel start failed"; + record.Active = false; + record.FinalLocalLegPending = false; + return false; + } + + record.FinalLocalLegPending = false; + record.FinalLocalLegActive = true; + record.Status.State = ClientlessActionExecutionState::Moving; + record.Status.TargetName = "location:" + record.FinalLocationId; + record.Status.TargetGuid = 0; + record.Status.TargetEntry = record.FinalDatabaseId; + record.Status.Detail = "local navigation to final location"; + record.Active = true; + + TC_LOG_INFO("server.adventurebot", + "[AdventureBot] travel-location-local-leg guid=%u name='%s' location='%s'", + characterGuid, + player->GetName().c_str(), + record.FinalLocationId.c_str()); + + return true; +} + bool ClientlessActionExecutorMgr::StartRouteStep( Player* player, uint32 characterGuid, @@ -503,6 +710,9 @@ bool ClientlessActionExecutorMgr::StartRouteStep( { if (record.RouteStep >= record.Route.size()) { + if (record.FinalLocalLegPending) + return StartFinalLocalLeg(player, characterGuid, record); + record.Status.State = ClientlessActionExecutionState::Completed; record.Status.Detail = "travel route completed"; record.Active = false; @@ -688,7 +898,11 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode( return; } - if (!player->IsStopped()) + // A clientless Player can be reported as "stopped" while a server- + // controlled MoveSpline is still running. Unit::UpdateSplineMovement() + // owns the authoritative server position for this movement and updates + // it until the spline is finalized. Do not evaluate arrival before then. + if (player->IsSplineEnabled()) return; float dx = player->GetPositionX() - destination.X; @@ -740,6 +954,57 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode( StartRouteStep(player, characterGuid, record); } + +void ClientlessActionExecutorMgr::UpdateTravelToLocation( + Player* player, + uint32 characterGuid, + Record& record) +{ + if (!record.FinalLocalLegActive) + { + UpdateTravelToNode(player, characterGuid, record); + return; + } + + ClientlessTravelStatus const* travel = sClientlessTravelMgr->GetStatus(characterGuid); + if (!travel) + { + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = "location local travel state disappeared"; + record.Active = false; + record.FinalLocalLegActive = false; + return; + } + + if (travel->State == ClientlessTravelState::Moving) + return; + + if (travel->State == ClientlessTravelState::Arrived) + { + record.Status.State = ClientlessActionExecutionState::Completed; + record.Status.TargetName = "location:" + record.FinalLocationId; + record.Status.TargetGuid = 0; + record.Status.TargetEntry = record.FinalDatabaseId; + record.Status.Detail = "arrived at location"; + record.Active = false; + record.FinalLocalLegActive = false; + + TC_LOG_INFO("server.adventurebot", + "[AdventureBot] travel-location-complete guid=%u name='%s' location='%s'", + characterGuid, + player->GetName().c_str(), + record.FinalLocationId.c_str()); + return; + } + + record.Status.State = ClientlessActionExecutionState::Failed; + record.Status.Detail = + std::string("location local travel ended in state ") + + ClientlessTravelMgr::StateName(travel->State); + record.Active = false; + record.FinalLocalLegActive = false; +} + void ClientlessActionExecutorMgr::UpdateActive( Player* player, uint32 characterGuid, @@ -751,6 +1016,12 @@ void ClientlessActionExecutorMgr::UpdateActive( return; } + if (record.Status.ActionName == "travel_to_location") + { + UpdateTravelToLocation(player, characterGuid, record); + return; + } + ClientlessTravelStatus const* travel = sClientlessTravelMgr->GetStatus(characterGuid); if (!travel) { diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index 75160fe907..8127d3f4be 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -53,15 +53,28 @@ private: std::size_t RouteStep = 0; std::vector EdgePath; + + bool FinalLocalLegPending = false; + bool FinalLocalLegActive = false; + uint32 FinalMapId = 0; + float FinalX = 0.0f; + float FinalY = 0.0f; + float FinalZ = 0.0f; + float FinalO = 0.0f; + uint32 FinalDatabaseId = 0; + std::string FinalLocationId; }; bool StartAction(Player* player, ClientlessAction const& action, Record& record); bool StartApproach(Player* player, ClientlessAction const& action, Record& record); bool StartMoveToPoint(Player* player, ClientlessAction const& action, Record& record); bool StartTravelToNode(Player* player, ClientlessAction const& action, Record& record); + bool StartTravelToLocation(Player* player, ClientlessAction const& action, Record& record); + bool StartFinalLocalLeg(Player* player, uint32 characterGuid, Record& record); bool StartRouteStep(Player* player, uint32 characterGuid, Record& record); bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record); void UpdateTravelToNode(Player* player, uint32 characterGuid, Record& record); + void UpdateTravelToLocation(Player* player, uint32 characterGuid, Record& record); void UpdateActive(Player* player, uint32 characterGuid, Record& record); std::unordered_map _records; diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index a82fc88af5..fc989b60db 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -329,6 +329,219 @@ bool ClientlessTravelGraphMgr::PlanRoute( } +bool ClientlessTravelGraphMgr::PlanRouteToPosition( + Player const* player, + uint32 destinationMapId, + float destinationX, + float destinationY, + float destinationZ, + float startRadius, + float anchorRadius, + std::vector& route, + std::string& anchorNode, + std::string& error) +{ + route.clear(); + anchorNode.clear(); + error.clear(); + + EnsureLoaded(); + + if (!player || !player->IsInWorld()) + { + error = "player is not in world"; + return false; + } + + if (startRadius <= 0.0f) + startRadius = 500.0f; + + if (anchorRadius <= 0.0f) + anchorRadius = 1000.0f; + + struct QueueItem + { + float Cost; + std::string Node; + + bool operator>(QueueItem const& other) const + { + return Cost > other.Cost; + } + }; + + struct Previous + { + std::string Node; + std::string EdgeType; + uint32 EdgeDatabaseId = 0; + bool EdgeReverse = false; + }; + + std::priority_queue, std::greater> open; + std::unordered_map distance; + std::unordered_map previous; + + for (auto const& pair : _nodes) + distance[pair.first] = std::numeric_limits::infinity(); + + // Multi-source Dijkstra: every graph node close enough to the player's + // actual position is a possible entry point. Seed cost includes the local + // distance needed to reach that node, so nearer usable entries are still + // preferred without letting an isolated node block the route entirely. + uint32 startCandidateCount = 0; + for (auto const& pair : _nodes) + { + ClientlessTravelNode const& node = pair.second; + if (node.MapId != player->GetMapId()) + continue; + + float const dx = node.X - player->GetPositionX(); + float const dy = node.Y - player->GetPositionY(); + float const dz = node.Z - player->GetPositionZ(); + float const localStartDistance = std::sqrt(dx * dx + dy * dy + dz * dz); + + if (localStartDistance > startRadius) + continue; + + distance[node.Key] = localStartDistance; + open.push({ localStartDistance, node.Key }); + ++startCandidateCount; + } + + if (startCandidateCount == 0) + { + error = "no travel node near current position"; + return false; + } + + while (!open.empty()) + { + QueueItem current = open.top(); + open.pop(); + + if (current.Cost != distance[current.Node]) + continue; + + auto edgeItr = _edges.find(current.Node); + if (edgeItr == _edges.end()) + continue; + + for (ClientlessTravelEdge const& edge : edgeItr->second) + { + float const candidate = current.Cost + edge.Cost; + if (candidate >= distance[edge.ToNode]) + continue; + + distance[edge.ToNode] = candidate; + previous[edge.ToNode] = { + current.Node, edge.Type, edge.DatabaseId, edge.Reverse + }; + open.push({ candidate, edge.ToNode }); + } + } + + // The destination anchor's primary job is to get the bot close to the + // requested Location. Do not combine macro-route cost and final local + // distance into one score: that can make the current/start node win simply + // because reaching it costs zero, leaving MMAP with a very long final leg. + // + // Instead choose the reachable node closest to the destination first. + // Graph cost is only a tie-breaker between equally suitable anchors. + float bestDestinationDistance = std::numeric_limits::infinity(); + float bestGraphCost = std::numeric_limits::infinity(); + + for (auto const& pair : _nodes) + { + ClientlessTravelNode const& node = pair.second; + if (node.MapId != destinationMapId) + continue; + + auto distanceItr = distance.find(node.Key); + if (distanceItr == distance.end() || !std::isfinite(distanceItr->second)) + continue; + + float const dx = node.X - destinationX; + float const dy = node.Y - destinationY; + float const dz = node.Z - destinationZ; + float const localDestinationDistance = std::sqrt(dx * dx + dy * dy + dz * dz); + + if (localDestinationDistance > anchorRadius) + continue; + + float const graphCost = distanceItr->second; + + constexpr float DistanceTieEpsilon = 0.5f; + bool const closerToDestination = + localDestinationDistance + DistanceTieEpsilon < bestDestinationDistance; + + bool const sameDestinationDistance = + std::fabs(localDestinationDistance - bestDestinationDistance) <= DistanceTieEpsilon; + + if (!closerToDestination && + !(sameDestinationDistance && graphCost < bestGraphCost)) + continue; + + bestDestinationDistance = localDestinationDistance; + bestGraphCost = graphCost; + anchorNode = node.Key; + } + + if (anchorNode.empty()) + { + error = "no reachable travel node near destination"; + return false; + } + + // Reconstruct until we reach whichever seeded start candidate actually won. + // Seed roots deliberately have no Previous entry. + std::vector nodeKeys; + std::string cursor = anchorNode; + nodeKeys.push_back(cursor); + + while (true) + { + auto previousItr = previous.find(cursor); + if (previousItr == previous.end()) + break; + + cursor = previousItr->second.Node; + nodeKeys.push_back(cursor); + } + + std::reverse(nodeKeys.begin(), nodeKeys.end()); + route.reserve(nodeKeys.size()); + + for (std::size_t i = 0; i < nodeKeys.size(); ++i) + { + ClientlessTravelNode const& node = _nodes.at(nodeKeys[i]); + + 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; + + if (i == 0) + step.EdgeType = "start"; + else + { + Previous const& prev = previous.at(nodeKeys[i]); + step.EdgeType = prev.EdgeType; + step.EdgeDatabaseId = prev.EdgeDatabaseId; + step.EdgeReverse = prev.EdgeReverse; + } + + route.push_back(std::move(step)); + } + + return true; +} + std::vector const* ClientlessTravelGraphMgr::GetEdgePoints(uint32 edgeDatabaseId) const { diff --git a/src/server/game/World/ClientlessTravelGraphMgr.h b/src/server/game/World/ClientlessTravelGraphMgr.h index a16846a4b0..9cf57e8c79 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.h +++ b/src/server/game/World/ClientlessTravelGraphMgr.h @@ -82,6 +82,18 @@ public: std::vector& route, std::string& error); + bool PlanRouteToPosition( + Player const* player, + uint32 destinationMapId, + float destinationX, + float destinationY, + float destinationZ, + float startRadius, + float anchorRadius, + std::vector& route, + std::string& anchorNode, + std::string& error); + uint32 GetNodeCount() const { return uint32(_nodes.size()); } uint32 GetEdgeCount() const { return _edgeCount; } diff --git a/src/server/game/World/ClientlessWorldPointMgr.cpp b/src/server/game/World/ClientlessWorldPointMgr.cpp index bb144276fd..2681ffc072 100644 --- a/src/server/game/World/ClientlessWorldPointMgr.cpp +++ b/src/server/game/World/ClientlessWorldPointMgr.cpp @@ -106,3 +106,17 @@ ClientlessWorldPoint const* ClientlessWorldPointMgr::FindNearest( return best; } + +ClientlessWorldPoint const* ClientlessWorldPointMgr::FindById(std::string const& id) +{ + if (id.empty()) + return nullptr; + + EnsureLoaded(); + + for (ClientlessWorldPoint const& point : _points) + if (point.Id == id) + return &point; + + return nullptr; +} diff --git a/src/server/game/World/ClientlessWorldPointMgr.h b/src/server/game/World/ClientlessWorldPointMgr.h index 76b5a0445e..0a60d3ec6d 100644 --- a/src/server/game/World/ClientlessWorldPointMgr.h +++ b/src/server/game/World/ClientlessWorldPointMgr.h @@ -41,6 +41,8 @@ public: Player const* player, ClientlessWorldPointQuery const& query); + ClientlessWorldPoint const* FindById(std::string const& id); + uint32 GetPointCount() const { return uint32(_points.size()); } float GetDefaultSearchRadius() const { return DefaultSearchRadius; } diff --git a/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua b/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua index c7da48724b..12c0989ca8 100644 --- a/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua +++ b/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua @@ -9,9 +9,8 @@ intend("socialize", { city = "Ironforge", venue = "tavern", activity = "drink", -- max_distance = 40, -- approach_distance = 2 --}) -queue_action("travel_to_node", { - node = "goldshire_center", - start_radius = 500 +queue_action("travel_to_location", { + id = "goldshire_tavern" }) -- C8.6 incremental travel-graph example. -- Enable after the corresponding graph nodes/edges exist in cata_world.