fixed travel to location
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-14 13:07:04 +00:00
1 parent b14c864814
commit 4c04cd88de
7 files changed
+528 -4

No files matched your search

@@ -17,6 +17,7 @@
#include <array>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <utility>
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<float>::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)
{
@@ -53,15 +53,28 @@ private:
std::size_t RouteStep = 0;
std::vector<ClientlessTravelEdgePoint> 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<uint32, Record> _records;
@@ -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<ClientlessTravelRouteStep>& 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<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> open;
std::unordered_map<std::string, float> distance;
std::unordered_map<std::string, Previous> previous;
for (auto const& pair : _nodes)
distance[pair.first] = std::numeric_limits<float>::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<float>::infinity();
float bestGraphCost = std::numeric_limits<float>::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<std::string> 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<ClientlessTravelEdgePoint> const*
ClientlessTravelGraphMgr::GetEdgePoints(uint32 edgeDatabaseId) const
{
@@ -82,6 +82,18 @@ public:
std::vector<ClientlessTravelRouteStep>& route,
std::string& error);
bool PlanRouteToPosition(
Player const* player,
uint32 destinationMapId,
float destinationX,
float destinationY,
float destinationZ,
float startRadius,
float anchorRadius,
std::vector<ClientlessTravelRouteStep>& route,
std::string& anchorNode,
std::string& error);
uint32 GetNodeCount() const { return uint32(_nodes.size()); }
uint32 GetEdgeCount() const { return _edgeCount; }
@@ -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;
}
@@ -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; }
@@ -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.