importet Areateleports as Nodes
This commit is contained in:
1 parent
93c3df1c95
commit
fe7b5e0f02
12 files changed
+2087
-218
No files matched your search
@@ -2,10 +2,14 @@
|
||||
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessWorldPointMgr.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Log.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Opcodes.h"
|
||||
#include "Player.h"
|
||||
#include "MotionMaster.h"
|
||||
|
||||
@@ -111,6 +115,29 @@ namespace
|
||||
taxiNodeId = uint32(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool ParsePortalSourceNodeKey(std::string const& key, uint32& triggerId)
|
||||
{
|
||||
static std::string const Prefix = "portal_at_";
|
||||
static std::string const Suffix = "_source";
|
||||
|
||||
if (key.rfind(Prefix, 0) != 0 || key.size() <= Prefix.size() + Suffix.size())
|
||||
return false;
|
||||
if (key.compare(key.size() - Suffix.size(), Suffix.size(), Suffix) != 0)
|
||||
return false;
|
||||
|
||||
std::string const idText = key.substr(
|
||||
Prefix.size(), key.size() - Prefix.size() - Suffix.size());
|
||||
|
||||
char* end = nullptr;
|
||||
unsigned long const parsed = std::strtoul(idText.c_str(), &end, 10);
|
||||
if (!end || *end != '\0' || parsed == 0 || parsed > 0xFFFFFFFFul)
|
||||
return false;
|
||||
|
||||
triggerId = uint32(parsed);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ClientlessActionExecutorMgr* ClientlessActionExecutorMgr::instance()
|
||||
@@ -153,6 +180,19 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c
|
||||
record.FinalO = 0.0f;
|
||||
record.FinalDatabaseId = 0;
|
||||
record.FinalLocationId.clear();
|
||||
record.FailedEdgeDatabaseIds.clear();
|
||||
record.RecoveryCount = 0;
|
||||
record.RecoveryWanderAttempts = 0;
|
||||
record.RecoveryWanderActive = false;
|
||||
record.PlannedStartRadius = 500.0f;
|
||||
record.PlannedAnchorRadius = 1000.0f;
|
||||
record.PlannedDestinationNode.clear();
|
||||
record.FailedBridgeKeys.clear();
|
||||
record.RouteComplete = true;
|
||||
record.HardDestination = false;
|
||||
record.LocalBridgeRadius = 300.0f;
|
||||
record.LocalBridgePathMax = 500.0f;
|
||||
record.AdaptiveReachedNode.clear();
|
||||
|
||||
if (action.Name == "approach")
|
||||
return StartApproach(player, action, record);
|
||||
@@ -493,6 +533,16 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
|
||||
startRadius = parsed;
|
||||
}
|
||||
|
||||
record.PlannedDestinationNode = destinationNode;
|
||||
record.PlannedStartRadius = startRadius;
|
||||
|
||||
if (std::string const* hard = find("hard_destination"))
|
||||
record.HardDestination = (*hard == "1" || *hard == "true" || *hard == "yes");
|
||||
if (std::string const* radius = find("bridge_radius"))
|
||||
record.LocalBridgeRadius = ParseFloat(*radius, record.LocalBridgeRadius);
|
||||
if (std::string const* pathMax = find("bridge_path_max"))
|
||||
record.LocalBridgePathMax = ParseFloat(*pathMax, record.LocalBridgePathMax);
|
||||
|
||||
record.Route.clear();
|
||||
record.RouteStep = 0;
|
||||
record.EdgePath.clear();
|
||||
@@ -501,29 +551,42 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
|
||||
record.TaxiSourceNodeId = 0;
|
||||
record.TaxiDestinationNodeId = 0;
|
||||
std::string error;
|
||||
if (!sClientlessTravelGraphMgr->PlanRoute(
|
||||
player, destinationNode, startRadius, record.Route, error))
|
||||
if (sClientlessTravelGraphMgr->PlanRoute(
|
||||
player, destinationNode, startRadius, record.Route, error,
|
||||
&record.FailedEdgeDatabaseIds))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = error;
|
||||
record.RouteComplete = true;
|
||||
record.AdaptiveReachedNode = destinationNode;
|
||||
}
|
||||
else if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, destinationNode, startRadius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_node' destination='%s' reason='%s'",
|
||||
characterGuid, player->GetName().c_str(), destinationNode.c_str(), error.c_str());
|
||||
|
||||
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;
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot] travel-route-failed guid=%u name='%s' destination='%s' reason='%s'",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
destinationNode.c_str(),
|
||||
error.c_str());
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot] travel-route-planned guid=%u name='%s' destination='%s' steps=%u",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
destinationNode.c_str(),
|
||||
uint32(record.Route.size()));
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"TRAVEL_PLAN guid=%u name='%s' action='travel_to_node' destination='%s' "
|
||||
"complete=%u reached='%s' steps=%u bridgesBlocked=%u",
|
||||
characterGuid, player->GetName().c_str(), destinationNode.c_str(),
|
||||
record.RouteComplete ? 1u : 0u, record.AdaptiveReachedNode.c_str(),
|
||||
uint32(record.Route.size()), uint32(record.FailedBridgeKeys.size()));
|
||||
|
||||
record.Active = true;
|
||||
return StartRouteStep(player, characterGuid, record);
|
||||
@@ -605,6 +668,14 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
record.FinalO = point->O;
|
||||
record.FinalDatabaseId = point->DatabaseId;
|
||||
record.FinalLocationId = point->Id;
|
||||
record.PlannedStartRadius = startRadius;
|
||||
record.PlannedAnchorRadius = anchorRadius;
|
||||
if (std::string const* hard = find("hard_destination"))
|
||||
record.HardDestination = (*hard == "1" || *hard == "true" || *hard == "yes");
|
||||
if (std::string const* radius = find("bridge_radius"))
|
||||
record.LocalBridgeRadius = ParseFloat(*radius, record.LocalBridgeRadius);
|
||||
if (std::string const* pathMax = find("bridge_path_max"))
|
||||
record.LocalBridgePathMax = ParseFloat(*pathMax, record.LocalBridgePathMax);
|
||||
record.Status.TargetName = "location:" + point->Id;
|
||||
record.Status.TargetGuid = 0;
|
||||
record.Status.TargetEntry = point->DatabaseId;
|
||||
@@ -638,7 +709,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
|
||||
std::string anchorNode;
|
||||
std::string error;
|
||||
if (!sClientlessTravelGraphMgr->PlanRouteToPosition(
|
||||
if (sClientlessTravelGraphMgr->PlanRouteToPosition(
|
||||
player,
|
||||
point->MapId,
|
||||
point->X,
|
||||
@@ -648,22 +719,50 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
anchorRadius,
|
||||
record.Route,
|
||||
anchorNode,
|
||||
error))
|
||||
error,
|
||||
&record.FailedEdgeDatabaseIds))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = error;
|
||||
record.Active = false;
|
||||
record.RouteComplete = true;
|
||||
record.AdaptiveReachedNode = anchorNode;
|
||||
record.FinalLocalLegPending = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float proxyDistance = std::numeric_limits<float>::infinity();
|
||||
if (!sClientlessTravelGraphMgr->FindNearestNodeToPosition(
|
||||
point->MapId, point->X, point->Y, point->Z, anchorNode, proxyDistance))
|
||||
{
|
||||
record.Status.State = record.HardDestination
|
||||
? ClientlessActionExecutionState::Failed
|
||||
: ClientlessActionExecutionState::Completed;
|
||||
record.Status.Detail = "no graph node exists on destination map";
|
||||
record.Active = false;
|
||||
return !record.HardDestination;
|
||||
}
|
||||
|
||||
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.PlannedDestinationNode = anchorNode;
|
||||
if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, anchorNode, startRadius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"TRAVEL_PLAN_FAILED guid=%u name='%s' action='travel_to_location' location='%s' reason='%s'",
|
||||
characterGuid, player->GetName().c_str(), locationId.c_str(), error.c_str());
|
||||
|
||||
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 (record.RouteComplete && proxyDistance > anchorRadius)
|
||||
record.RouteComplete = false;
|
||||
record.FinalLocalLegPending = record.RouteComplete;
|
||||
}
|
||||
|
||||
record.FinalLocalLegPending = true;
|
||||
record.Active = true;
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
@@ -700,7 +799,9 @@ bool ClientlessActionExecutorMgr::StartFinalLocalLeg(
|
||||
record.FinalZ,
|
||||
record.FinalO);
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination))
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "final-local-leg");
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination, false))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "location final local travel start failed";
|
||||
@@ -737,6 +838,29 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
if (record.FinalLocalLegPending)
|
||||
return StartFinalLocalLeg(player, characterGuid, record);
|
||||
|
||||
if (!record.RouteComplete)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"TRAVEL_PARTIAL guid=%u name='%s' action='%s' requested='%s' reached='%s' "
|
||||
"map=%u pos=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), record.Status.ActionName.c_str(),
|
||||
record.PlannedDestinationNode.c_str(), record.AdaptiveReachedNode.c_str(),
|
||||
player->GetMapId(), player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
if (record.HardDestination)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "hard destination only partially reachable";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Completed;
|
||||
record.Status.Detail = "best-effort travel completed at nearest useful reachable point";
|
||||
record.Active = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Completed;
|
||||
record.Status.Detail = "travel route completed";
|
||||
record.Active = false;
|
||||
@@ -749,7 +873,17 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
|
||||
ClientlessTravelRouteStep const& step = record.Route[record.RouteStep];
|
||||
|
||||
if (step.EdgeType != "start" && step.EdgeType != "ground" && step.EdgeType != "taxi")
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"EDGE_START guid=%u name='%s' action='%s' step=%u/%u node='%s' nodeType='%s' "
|
||||
"edgeType='%s' edgeDbId=%u reverse=%u recovery=%u",
|
||||
characterGuid, player->GetName().c_str(), record.Status.ActionName.c_str(),
|
||||
uint32(record.RouteStep + 1), uint32(record.Route.size()),
|
||||
step.NodeKey.c_str(), step.NodeType.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, step.EdgeReverse ? 1u : 0u, record.RecoveryCount);
|
||||
|
||||
if (step.EdgeType != "start" && step.EdgeType != "ground" &&
|
||||
step.EdgeType != "taxi" && step.EdgeType != "portal" &&
|
||||
step.EdgeType != "local_bridge")
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
@@ -765,6 +899,211 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (step.EdgeType == "local_bridge")
|
||||
{
|
||||
if (record.RouteStep == 0)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "local bridge has no source route step";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientlessTravelRouteStep const& source = record.Route[record.RouteStep - 1];
|
||||
std::string const bridgeKey = source.NodeKey + "->" + step.NodeKey;
|
||||
|
||||
if (player->GetMapId() != step.MapId)
|
||||
{
|
||||
record.FailedBridgeKeys.insert(bridgeKey);
|
||||
return RecoverTravel(player, characterGuid, record, 0, "local-bridge-map-mismatch");
|
||||
}
|
||||
|
||||
Position destination;
|
||||
destination.Relocate(step.X, step.Y, step.Z, step.O);
|
||||
|
||||
ClientlessPathDiagnostics nav;
|
||||
bool const calculated = sClientlessTravelMgr->AnalyzePath(player, destination, nav);
|
||||
bool const usable =
|
||||
calculated && nav.Complete && nav.FinalDistance <= 3.0f &&
|
||||
nav.DirectDistance <= record.LocalBridgeRadius + 1.0f &&
|
||||
nav.PathLength <= record.LocalBridgePathMax;
|
||||
|
||||
if (!usable)
|
||||
{
|
||||
record.FailedBridgeKeys.insert(bridgeKey);
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"LOCAL_BRIDGE_REJECT guid=%u name='%s' from='%s' to='%s' direct=%.2f path=%.2f "
|
||||
"complete=%u finalDistance=%.2f reason='%s' blockedBridges=%u",
|
||||
characterGuid, player->GetName().c_str(), source.NodeKey.c_str(), step.NodeKey.c_str(),
|
||||
nav.DirectDistance, nav.PathLength, nav.Complete ? 1u : 0u,
|
||||
nav.FinalDistance, nav.Reason.c_str(), uint32(record.FailedBridgeKeys.size()));
|
||||
|
||||
return RecoverTravel(player, characterGuid, record, 0, "local-bridge-path-invalid");
|
||||
}
|
||||
|
||||
record.EdgePath.clear();
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.TargetGuid = 0;
|
||||
record.Status.TargetEntry = 0;
|
||||
record.Status.Detail = "dynamic local bridge";
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination, true))
|
||||
{
|
||||
record.FailedBridgeKeys.insert(bridgeKey);
|
||||
return RecoverTravel(player, characterGuid, record, 0, "local-bridge-start-failed");
|
||||
}
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"LOCAL_BRIDGE_START guid=%u name='%s' from='%s' to='%s' direct=%.2f path=%.2f",
|
||||
characterGuid, player->GetName().c_str(), source.NodeKey.c_str(), step.NodeKey.c_str(),
|
||||
nav.DirectDistance, nav.PathLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (step.EdgeType == "portal")
|
||||
{
|
||||
if (record.RouteStep == 0)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "portal edge has no source route step";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientlessTravelRouteStep const& sourceStep = record.Route[record.RouteStep - 1];
|
||||
uint32 triggerId = 0;
|
||||
if (!ParsePortalSourceNodeKey(sourceStep.NodeKey, triggerId))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "portal edge source is not portal_at_<id>_source";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
AreaTriggerEntry const* atEntry = sAreaTriggerStore.LookupEntry(triggerId);
|
||||
AreaTriggerStruct const* teleport = sObjectMgr->GetAreaTrigger(triggerId);
|
||||
if (!atEntry || !teleport)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "portal AreaTrigger metadata missing for id " + std::to_string(triggerId);
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// C8.13a only imports far map transitions. Keeping this invariant here
|
||||
// prevents a clientless Player from getting stuck waiting for the near
|
||||
// MOVE_TELEPORT_ACK path, which is intentionally not implemented yet.
|
||||
if (uint32(atEntry->ContinentID) == teleport->target_mapId)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "same-map portal transition is not supported by C8.13a";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player->GetMapId() != uint32(atEntry->ContinentID) ||
|
||||
!player->IsInAreaTriggerRadius(atEntry))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='portal-not-in-trigger' edgeDbId=%u "
|
||||
"trigger=%u currentMap=%u expectedMap=%u current=(%.3f %.3f %.3f) trigger=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId,
|
||||
triggerId, player->GetMapId(), uint32(atEntry->ContinentID),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
atEntry->Pos.X, atEntry->Pos.Y, atEntry->Pos.Z);
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + sourceStep.NodeKey;
|
||||
record.Status.Detail = "portal source AreaTrigger was not reached";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (teleport->target_mapId != step.MapId)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "portal graph target map does not match AreaTrigger target";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
record.EdgePath.clear();
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "portal-edge");
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"PORTAL_TRIGGER guid=%u name='%s' edgeDbId=%u trigger=%u sourceMap=%u targetMap=%u "
|
||||
"source=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId,
|
||||
triggerId, uint32(atEntry->ContinentID), teleport->target_mapId,
|
||||
atEntry->Pos.X, atEntry->Pos.Y, atEntry->Pos.Z,
|
||||
teleport->target_X, teleport->target_Y, teleport->target_Z);
|
||||
|
||||
WorldPacket areaTriggerPacket(CMSG_AREATRIGGER, 4);
|
||||
areaTriggerPacket << triggerId;
|
||||
player->GetSession()->HandleAreaTriggerOpcode(areaTriggerPacket);
|
||||
|
||||
if (player->IsBeingTeleportedFar())
|
||||
{
|
||||
if (!player->GetSession()->IsClientless())
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "portal executor expected a clientless WorldSession";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A real client would answer the far transfer with CMSG_MOVE_WORLDPORT_ACK.
|
||||
// Clientless sessions have no socket, so finish the exact same server-side
|
||||
// transfer path explicitly after the normal AreaTrigger handler accepted it.
|
||||
player->GetSession()->HandleMoveWorldportAck();
|
||||
}
|
||||
|
||||
if (!player->IsInWorld() || player->GetMapId() != teleport->target_mapId)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='portal-transfer-not-completed' edgeDbId=%u "
|
||||
"trigger=%u expectedMap=%u actualMap=%u farPending=%u",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId,
|
||||
triggerId, teleport->target_mapId, player->GetMapId(),
|
||||
player->IsBeingTeleportedFar() ? 1u : 0u);
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail =
|
||||
"AreaTrigger " + std::to_string(triggerId) +
|
||||
" did not complete its map transfer (access/script condition may have denied it)";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
float const dx = player->GetPositionX() - teleport->target_X;
|
||||
float const dy = player->GetPositionY() - teleport->target_Y;
|
||||
float const dz = player->GetPositionZ() - teleport->target_Z;
|
||||
float const arrivalError = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.TargetGuid = 0;
|
||||
record.Status.TargetEntry = triggerId;
|
||||
record.Status.Detail = "AreaTrigger portal " + std::to_string(triggerId);
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"EDGE_COMPLETE guid=%u name='%s' edgeType='portal' edgeDbId=%u node='%s' "
|
||||
"trigger=%u map=%u arrivalError=%.2f recovery=%u",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId,
|
||||
step.NodeKey.c_str(), triggerId, player->GetMapId(), arrivalError,
|
||||
record.RecoveryCount);
|
||||
|
||||
++record.RouteStep;
|
||||
return StartRouteStep(player, characterGuid, record);
|
||||
}
|
||||
|
||||
if (step.EdgeType == "taxi")
|
||||
{
|
||||
if (record.RouteStep == 0)
|
||||
@@ -811,6 +1150,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
record.EdgePath.clear();
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "taxi-edge");
|
||||
|
||||
// AdventureBots are performative world actors, not simulated
|
||||
// participants in the player economy. Give the normal Trinity taxi
|
||||
@@ -846,24 +1186,16 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
|
||||
if (!taxiStarted)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail =
|
||||
"ActivateTaxiPathTo failed for path=" +
|
||||
std::to_string(taxiPathId) +
|
||||
" source=" + std::to_string(sourceTaxiNodeId) +
|
||||
" destination=" + std::to_string(destinationTaxiNodeId);
|
||||
record.Active = false;
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='taxi-start-failed' edgeDbId=%u "
|
||||
"path=%u source=%u destination=%u cost=%u current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId,
|
||||
taxiPathId, sourceTaxiNodeId, destinationTaxiNodeId, taxiCost,
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot] taxi-start-failed guid=%u name='%s' path=%u source=%u destination=%u cost=%u",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
taxiPathId,
|
||||
sourceTaxiNodeId,
|
||||
destinationTaxiNodeId,
|
||||
taxiCost);
|
||||
return false;
|
||||
return RecoverTravel(
|
||||
player, characterGuid, record, step.EdgeDatabaseId,
|
||||
"taxi-start-failed");
|
||||
}
|
||||
|
||||
record.TaxiActive = true;
|
||||
@@ -930,7 +1262,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
Position destination;
|
||||
destination.Relocate(step.X, step.Y, step.Z, step.O);
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination))
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination, true))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "travel start failed for route node " + step.NodeKey;
|
||||
@@ -1010,6 +1342,17 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
|
||||
return false;
|
||||
}
|
||||
|
||||
float recordedDistance = 0.0f;
|
||||
for (std::size_t i = 1; i < path.size(); ++i)
|
||||
{
|
||||
float const dx = path[i].GetPositionX() - path[i - 1].GetPositionX();
|
||||
float const dy = path[i].GetPositionY() - path[i - 1].GetPositionY();
|
||||
float const dz = path[i].GetPositionZ() - path[i - 1].GetPositionZ();
|
||||
recordedDistance += std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
sClientlessLuaEngine->PrepareGroundMount(player, recordedDistance, "recorded-ground-edge");
|
||||
|
||||
player->GetMotionMaster()->MoveSmoothPath(
|
||||
1,
|
||||
path.data(),
|
||||
@@ -1034,11 +1377,268 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartRecoveryWander(
|
||||
Player* player,
|
||||
uint32 characterGuid,
|
||||
Record& record,
|
||||
std::string const& reason)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
|
||||
constexpr uint32 MaxWanderAttempts = 6;
|
||||
if (record.RecoveryWanderAttempts >= MaxWanderAttempts)
|
||||
return false;
|
||||
|
||||
++record.RecoveryWanderAttempts;
|
||||
|
||||
constexpr uint32 CandidateCount = 16;
|
||||
for (uint32 i = 0; i < CandidateCount; ++i)
|
||||
{
|
||||
float const angle =
|
||||
float((characterGuid * 37u + record.RecoveryWanderAttempts * 71u + i * 23u) % 360u)
|
||||
* 0.01745329251994329577f;
|
||||
float const distance = 35.0f + float((i % 4u) * 15u);
|
||||
|
||||
Position target;
|
||||
target.Relocate(
|
||||
player->GetPositionX() + std::cos(angle) * distance,
|
||||
player->GetPositionY() + std::sin(angle) * distance,
|
||||
player->GetPositionZ(),
|
||||
player->GetOrientation());
|
||||
|
||||
ClientlessPathDiagnostics nav;
|
||||
sClientlessTravelMgr->AnalyzePath(player, target, nav);
|
||||
|
||||
if (!nav.Calculated || !nav.Complete || nav.PointCount < 2 ||
|
||||
nav.FinalDistance > 3.0f || nav.PathLength > 180.0f)
|
||||
continue;
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, target))
|
||||
continue;
|
||||
|
||||
record.RecoveryWanderActive = true;
|
||||
record.Active = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.Detail = "recovery wander";
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"RECOVERY_WANDER guid=%u name='%s' attempt=%u reason='%s' "
|
||||
"target=(%.3f %.3f %.3f) pathLength=%.2f",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.RecoveryWanderAttempts, reason.c_str(),
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||
nav.PathLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
Player* player,
|
||||
uint32 characterGuid,
|
||||
Record& record,
|
||||
uint32 failedEdgeDatabaseId,
|
||||
std::string const& reason)
|
||||
{
|
||||
constexpr uint32 MaxRecoveries = 5;
|
||||
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
|
||||
if (failedEdgeDatabaseId)
|
||||
record.FailedEdgeDatabaseIds.insert(failedEdgeDatabaseId);
|
||||
|
||||
bool const bridgeFailure = reason.rfind("local-bridge-", 0) == 0;
|
||||
|
||||
if (!bridgeFailure && record.RecoveryCount >= MaxRecoveries)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "travel recovery exhausted: " + reason;
|
||||
record.Active = false;
|
||||
record.RecoveryWanderActive = false;
|
||||
|
||||
TC_LOG_ERROR("server.adventurebot.navigation",
|
||||
"RECOVERY_FAILED guid=%u name='%s' action='%s' reason='%s' "
|
||||
"recoveries=%u blockedEdges=%u current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.ActionName.c_str(), reason.c_str(),
|
||||
record.RecoveryCount, uint32(record.FailedEdgeDatabaseIds.size()),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bridgeFailure)
|
||||
++record.RecoveryCount;
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
record.EdgePath.clear();
|
||||
record.TaxiActive = false;
|
||||
record.TaxiPathId = 0;
|
||||
record.TaxiSourceNodeId = 0;
|
||||
record.TaxiDestinationNodeId = 0;
|
||||
record.RecoveryWanderActive = false;
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"RECOVERY_START guid=%u name='%s' action='%s' reason='%s' recovery=%u/%u "
|
||||
"failedEdgeDbId=%u blockedEdges=%u current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.ActionName.c_str(), reason.c_str(),
|
||||
record.RecoveryCount, MaxRecoveries, failedEdgeDatabaseId,
|
||||
uint32(record.FailedEdgeDatabaseIds.size()),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
float radii[4] =
|
||||
{
|
||||
std::max(500.0f, record.PlannedStartRadius),
|
||||
1000.0f,
|
||||
2000.0f,
|
||||
4000.0f
|
||||
};
|
||||
|
||||
std::string lastError = "no recovery route";
|
||||
|
||||
for (float radius : radii)
|
||||
{
|
||||
record.Route.clear();
|
||||
record.RouteStep = 0;
|
||||
|
||||
bool planned = false;
|
||||
std::string error;
|
||||
|
||||
if (record.Status.ActionName == "travel_to_location")
|
||||
{
|
||||
std::string anchorNode;
|
||||
planned = sClientlessTravelGraphMgr->PlanRouteToPosition(
|
||||
player,
|
||||
record.FinalMapId,
|
||||
record.FinalX,
|
||||
record.FinalY,
|
||||
record.FinalZ,
|
||||
radius,
|
||||
record.PlannedAnchorRadius,
|
||||
record.Route,
|
||||
anchorNode,
|
||||
error,
|
||||
&record.FailedEdgeDatabaseIds);
|
||||
|
||||
if (planned)
|
||||
{
|
||||
record.RouteComplete = true;
|
||||
record.AdaptiveReachedNode = anchorNode;
|
||||
}
|
||||
else if (!record.PlannedDestinationNode.empty())
|
||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys);
|
||||
|
||||
if (planned)
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"RECOVERY_REPLAN guid=%u name='%s' action='travel_to_location' "
|
||||
"location='%s' anchor='%s' radius=%.1f steps=%u blockedEdges=%u",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.FinalLocationId.c_str(), anchorNode.c_str(), radius,
|
||||
uint32(record.Route.size()),
|
||||
uint32(record.FailedEdgeDatabaseIds.size()));
|
||||
}
|
||||
else if (record.Status.ActionName == "travel_to_node")
|
||||
{
|
||||
planned = sClientlessTravelGraphMgr->PlanRoute(
|
||||
player,
|
||||
record.PlannedDestinationNode,
|
||||
radius,
|
||||
record.Route,
|
||||
error,
|
||||
&record.FailedEdgeDatabaseIds);
|
||||
|
||||
if (planned)
|
||||
{
|
||||
record.RouteComplete = true;
|
||||
record.AdaptiveReachedNode = record.PlannedDestinationNode;
|
||||
}
|
||||
else
|
||||
planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute(
|
||||
player, record.PlannedDestinationNode, radius, record.LocalBridgeRadius,
|
||||
record.Route, record.RouteComplete, record.AdaptiveReachedNode, error,
|
||||
&record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys);
|
||||
|
||||
if (planned)
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"RECOVERY_REPLAN guid=%u name='%s' action='travel_to_node' "
|
||||
"destination='%s' radius=%.1f steps=%u blockedEdges=%u",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.PlannedDestinationNode.c_str(), radius,
|
||||
uint32(record.Route.size()),
|
||||
uint32(record.FailedEdgeDatabaseIds.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "recovery unsupported for action";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!planned)
|
||||
{
|
||||
lastError = error;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.Status.ActionName == "travel_to_location")
|
||||
{
|
||||
record.FinalLocalLegPending = record.RouteComplete;
|
||||
record.FinalLocalLegActive = false;
|
||||
}
|
||||
|
||||
record.Active = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.Detail = "travel recovery replanned";
|
||||
return StartRouteStep(player, characterGuid, record);
|
||||
}
|
||||
|
||||
if (StartRecoveryWander(player, characterGuid, record, lastError))
|
||||
return true;
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "no recovery route or local wander path: " + lastError;
|
||||
record.Active = false;
|
||||
|
||||
TC_LOG_ERROR("server.adventurebot.navigation",
|
||||
"RECOVERY_FAILED guid=%u name='%s' action='%s' reason='%s' "
|
||||
"recoveries=%u blockedEdges=%u current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.ActionName.c_str(), lastError.c_str(),
|
||||
record.RecoveryCount, uint32(record.FailedEdgeDatabaseIds.size()),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
Player* player,
|
||||
uint32 characterGuid,
|
||||
Record& record)
|
||||
{
|
||||
if (record.RecoveryWanderActive)
|
||||
{
|
||||
ClientlessTravelStatus const* wander =
|
||||
sClientlessTravelMgr->GetStatus(characterGuid);
|
||||
|
||||
if (wander && wander->State == ClientlessTravelState::Moving)
|
||||
return;
|
||||
|
||||
record.RecoveryWanderActive = false;
|
||||
|
||||
std::string const reason =
|
||||
wander
|
||||
? std::string("wander-ended-") + ClientlessTravelMgr::StateName(wander->State)
|
||||
: "wander-travel-state-missing";
|
||||
|
||||
RecoverTravel(player, characterGuid, record, 0, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.TaxiActive)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
@@ -1060,13 +1660,17 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
|
||||
if (player->GetMapId() != destination.MapId)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail =
|
||||
"taxi ended on unexpected map; expected=" +
|
||||
std::to_string(destination.MapId) +
|
||||
" actual=" + std::to_string(player->GetMapId());
|
||||
record.Active = false;
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='taxi-unexpected-map' edgeDbId=%u "
|
||||
"expectedMap=%u actualMap=%u path=%u source=%u destination=%u",
|
||||
characterGuid, player->GetName().c_str(), destination.EdgeDatabaseId,
|
||||
destination.MapId, player->GetMapId(),
|
||||
record.TaxiPathId, record.TaxiSourceNodeId, record.TaxiDestinationNodeId);
|
||||
|
||||
record.TaxiActive = false;
|
||||
RecoverTravel(
|
||||
player, characterGuid, record, destination.EdgeDatabaseId,
|
||||
"taxi-unexpected-map");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1078,21 +1682,21 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
constexpr float TaxiArrivalSanityRadius = 120.0f;
|
||||
if (remaining > TaxiArrivalSanityRadius)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail =
|
||||
"taxi stopped away from destination node; remaining=" +
|
||||
std::to_string(remaining);
|
||||
record.Active = false;
|
||||
record.TaxiActive = false;
|
||||
uint32 const failedEdgeId = destination.EdgeDatabaseId;
|
||||
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot] taxi-ended-away-from-node guid=%u name='%s' path=%u source=%u destination=%u remaining=%.2f",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
record.TaxiPathId,
|
||||
record.TaxiSourceNodeId,
|
||||
record.TaxiDestinationNodeId,
|
||||
remaining);
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='taxi-ended-away-from-node' edgeDbId=%u "
|
||||
"path=%u source=%u destination=%u current=(%.3f %.3f %.3f) "
|
||||
"expected=(%.3f %.3f %.3f) remaining=%.2f",
|
||||
characterGuid, player->GetName().c_str(), failedEdgeId,
|
||||
record.TaxiPathId, record.TaxiSourceNodeId, record.TaxiDestinationNodeId,
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
destination.X, destination.Y, destination.Z, remaining);
|
||||
|
||||
record.TaxiActive = false;
|
||||
RecoverTravel(
|
||||
player, characterGuid, record, failedEdgeId,
|
||||
"taxi-ended-away-from-node");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1105,6 +1709,13 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
record.TaxiDestinationNodeId,
|
||||
remaining);
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"EDGE_COMPLETE guid=%u name='%s' edgeType='taxi' edgeDbId=%u "
|
||||
"node='%s' remaining=%.2f recovery=%u",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
destination.EdgeDatabaseId, destination.NodeKey.c_str(),
|
||||
remaining, record.RecoveryCount);
|
||||
|
||||
record.TaxiActive = false;
|
||||
record.TaxiPathId = 0;
|
||||
record.TaxiSourceNodeId = 0;
|
||||
@@ -1141,11 +1752,20 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
|
||||
if (remaining > 2.0f)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail =
|
||||
"recorded ground spline stopped early; remaining=" +
|
||||
std::to_string(remaining);
|
||||
record.Active = false;
|
||||
uint32 const failedEdgeId =
|
||||
record.RouteStep < record.Route.size()
|
||||
? record.Route[record.RouteStep].EdgeDatabaseId
|
||||
: 0u;
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='recorded-ground-stopped-early' "
|
||||
"edgeDbId=%u remaining=%.2f current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), failedEdgeId, remaining,
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
RecoverTravel(
|
||||
player, characterGuid, record, failedEdgeId,
|
||||
"recorded-ground-stopped-early");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1171,14 +1791,35 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
|
||||
if (travel->State != ClientlessTravelState::Arrived)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail =
|
||||
std::string("route step ended in state ") +
|
||||
ClientlessTravelMgr::StateName(travel->State);
|
||||
record.Active = false;
|
||||
uint32 const failedEdgeId =
|
||||
record.RouteStep < record.Route.size()
|
||||
? record.Route[record.RouteStep].EdgeDatabaseId
|
||||
: 0u;
|
||||
|
||||
std::string const reason =
|
||||
std::string("route-local-") + ClientlessTravelMgr::StateName(travel->State);
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='local-navigation' edgeDbId=%u "
|
||||
"state=%s current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), failedEdgeId,
|
||||
ClientlessTravelMgr::StateName(travel->State),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
RecoverTravel(player, characterGuid, record, failedEdgeId, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.RouteStep < record.Route.size())
|
||||
{
|
||||
ClientlessTravelRouteStep const& completed = record.Route[record.RouteStep];
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"EDGE_COMPLETE guid=%u name='%s' edgeType='%s' edgeDbId=%u node='%s' recovery=%u",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
completed.EdgeType.c_str(), completed.EdgeDatabaseId,
|
||||
completed.NodeKey.c_str(), record.RecoveryCount);
|
||||
}
|
||||
|
||||
++record.RouteStep;
|
||||
StartRouteStep(player, characterGuid, record);
|
||||
}
|
||||
@@ -1226,12 +1867,19 @@ void ClientlessActionExecutorMgr::UpdateTravelToLocation(
|
||||
return;
|
||||
}
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail =
|
||||
std::string("location local travel ended in state ") +
|
||||
ClientlessTravelMgr::StateName(travel->State);
|
||||
record.Active = false;
|
||||
std::string const reason =
|
||||
std::string("final-local-") + ClientlessTravelMgr::StateName(travel->State);
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='final-local-navigation' location='%s' "
|
||||
"state=%s current=(%.3f %.3f %.3f)",
|
||||
characterGuid, player->GetName().c_str(), record.FinalLocationId.c_str(),
|
||||
ClientlessTravelMgr::StateName(travel->State),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
record.FinalLocalLegActive = false;
|
||||
record.FinalLocalLegPending = true;
|
||||
RecoverTravel(player, characterGuid, record, 0, reason);
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::UpdateActive(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class Player;
|
||||
@@ -69,6 +70,24 @@ private:
|
||||
float FinalO = 0.0f;
|
||||
uint32 FinalDatabaseId = 0;
|
||||
std::string FinalLocationId;
|
||||
|
||||
// C8.11b travel recovery. Failed edges are blacklisted only for the
|
||||
// current action; nothing is disabled persistently in the database.
|
||||
std::unordered_set<uint32> FailedEdgeDatabaseIds;
|
||||
uint32 RecoveryCount = 0;
|
||||
uint32 RecoveryWanderAttempts = 0;
|
||||
bool RecoveryWanderActive = false;
|
||||
float PlannedStartRadius = 500.0f;
|
||||
float PlannedAnchorRadius = 1000.0f;
|
||||
std::string PlannedDestinationNode;
|
||||
|
||||
// C8.13b adaptive graph bridging / best-effort travel.
|
||||
std::unordered_set<std::string> FailedBridgeKeys;
|
||||
bool RouteComplete = true;
|
||||
bool HardDestination = false;
|
||||
float LocalBridgeRadius = 300.0f;
|
||||
float LocalBridgePathMax = 500.0f;
|
||||
std::string AdaptiveReachedNode;
|
||||
};
|
||||
|
||||
bool StartAction(Player* player, ClientlessAction const& action, Record& record);
|
||||
@@ -79,6 +98,8 @@ private:
|
||||
bool StartFinalLocalLeg(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartRouteStep(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverTravel(Player* player, uint32 characterGuid, Record& record, uint32 failedEdgeDatabaseId, std::string const& reason);
|
||||
bool StartRecoveryWander(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
void UpdateTravelToNode(Player* player, uint32 characterGuid, Record& record);
|
||||
void UpdateTravelToLocation(Player* player, uint32 characterGuid, Record& record);
|
||||
void UpdateActive(Player* player, uint32 characterGuid, Record& record);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include "Random.h"
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -86,10 +87,92 @@ void BoundFn(lua_State* L, int env, char const* name, lua_CFunction fn, uint32 g
|
||||
lua_setfield(L, env, name);
|
||||
}
|
||||
|
||||
std::string ReadStringField(lua_State* L, int table, char const* key, std::string const& fallback = {})
|
||||
{
|
||||
table = lua_absindex(L, table);
|
||||
lua_getfield(L, table, key);
|
||||
std::string value = lua_isstring(L, -1) ? lua_tostring(L, -1) : fallback;
|
||||
lua_pop(L, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ReadBoolField(lua_State* L, int table, char const* key, bool fallback)
|
||||
{
|
||||
table = lua_absindex(L, table);
|
||||
lua_getfield(L, table, key);
|
||||
bool value = lua_isboolean(L, -1) ? lua_toboolean(L, -1) != 0 : fallback;
|
||||
lua_pop(L, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
float ReadFloatField(lua_State* L, int table, char const* key, float fallback)
|
||||
{
|
||||
table = lua_absindex(L, table);
|
||||
lua_getfield(L, table, key);
|
||||
float value = lua_isnumber(L, -1) ? float(lua_tonumber(L, -1)) : fallback;
|
||||
lua_pop(L, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, float> ReadFloatDictionary(lua_State* L, int table, char const* key)
|
||||
{
|
||||
std::unordered_map<std::string, float> out;
|
||||
table = lua_absindex(L, table);
|
||||
lua_getfield(L, table, key);
|
||||
if (!lua_istable(L, -1))
|
||||
{
|
||||
lua_pop(L, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
int dict = lua_absindex(L, -1);
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, dict) != 0)
|
||||
{
|
||||
if (lua_type(L, -2) == LUA_TSTRING && lua_isnumber(L, -1))
|
||||
out[lua_tostring(L, -2)] = float(lua_tonumber(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movementTable)
|
||||
{
|
||||
std::vector<ClientlessWeightedMount> mounts;
|
||||
movementTable = lua_absindex(L, movementTable);
|
||||
lua_getfield(L, movementTable, "ground_mounts");
|
||||
if (!lua_istable(L, -1))
|
||||
{
|
||||
lua_pop(L, 1);
|
||||
return mounts;
|
||||
}
|
||||
|
||||
int dict = lua_absindex(L, -1);
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, dict) != 0)
|
||||
{
|
||||
if (lua_isinteger(L, -2) && lua_isnumber(L, -1))
|
||||
{
|
||||
lua_Integer id = lua_tointeger(L, -2);
|
||||
lua_Number weight = lua_tonumber(L, -1);
|
||||
if (id > 0 && id <= 0xFFFFFFFFll && weight > 0.0)
|
||||
mounts.push_back({ uint32(id), uint32(std::max<lua_Number>(1.0, weight)) });
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return mounts;
|
||||
}
|
||||
|
||||
class ClientlessAdventureLuaController final : public ClientlessPlayerController
|
||||
{
|
||||
public:
|
||||
void OnPlayerOnline(Player* player) override { sClientlessLuaEngine->ExecuteAdventureScript(player); }
|
||||
void OnPlayerOnline(Player* player) override
|
||||
{
|
||||
sClientlessLuaEngine->LoadAdventureProfile(player);
|
||||
sClientlessLuaEngine->ExecuteAdventureScript(player);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,6 +255,184 @@ bool ClientlessLuaEngine::ExecuteScript(Player* player, std::string const& path,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
|
||||
{
|
||||
if (!_state || !player)
|
||||
return false;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
std::string lowerName = player->GetName();
|
||||
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
|
||||
[](unsigned char c) { return char(std::tolower(c)); });
|
||||
|
||||
fs::path root(_scriptRoot);
|
||||
fs::path specific = root / "adventure" / "profiles" / (lowerName + ".lua");
|
||||
fs::path generic = root / "adventure" / "profiles" / "generic.lua";
|
||||
fs::path chosen = fs::exists(specific) ? specific : generic;
|
||||
|
||||
int base = lua_gettop(_state);
|
||||
if (luaL_loadfile(_state, chosen.string().c_str()) != LUA_OK)
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u name='%s' load failed path='%s': %s",
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(),
|
||||
chosen.string().c_str(), lua_tostring(_state, -1));
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
|
||||
PushSandboxEnvironment(player);
|
||||
if (!lua_setupvalue(_state, base + 1, 1))
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u path='%s' has no _ENV upvalue",
|
||||
player->GetGUID().GetCounter(), chosen.string().c_str());
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lua_pcall(_state, 0, 1, 0) != LUA_OK)
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u runtime error path='%s': %s",
|
||||
player->GetGUID().GetCounter(), chosen.string().c_str(), lua_tostring(_state, -1));
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lua_istable(_state, -1))
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u path='%s' must return a table",
|
||||
player->GetGUID().GetCounter(), chosen.string().c_str());
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
|
||||
int rootTable = lua_absindex(_state, -1);
|
||||
ClientlessAdventureProfile profile;
|
||||
profile.CharacterName = player->GetName();
|
||||
profile.SourcePath = chosen.string();
|
||||
|
||||
lua_getfield(_state, rootTable, "identity");
|
||||
if (lua_istable(_state, -1))
|
||||
{
|
||||
profile.CharacterName = ReadStringField(_state, -1, "name", profile.CharacterName);
|
||||
profile.HomeRegion = ReadStringField(_state, -1, "home_region");
|
||||
}
|
||||
lua_pop(_state, 1);
|
||||
|
||||
lua_getfield(_state, rootTable, "movement");
|
||||
if (lua_istable(_state, -1))
|
||||
{
|
||||
profile.CanGroundMount = ReadBoolField(_state, -1, "can_ground_mount", false);
|
||||
profile.GroundMountMinDistance = std::max(0.0f,
|
||||
ReadFloatField(_state, -1, "ground_mount_min_distance", 60.0f));
|
||||
profile.GroundMountSpeedRate = std::max(1.0f,
|
||||
ReadFloatField(_state, -1, "ground_mount_speed_rate", 1.6f));
|
||||
profile.GroundMounts = ReadMountDictionary(_state, -1);
|
||||
}
|
||||
lua_pop(_state, 1);
|
||||
|
||||
profile.Personality = ReadFloatDictionary(_state, rootTable, "personality");
|
||||
profile.RegionAffinity = ReadFloatDictionary(_state, rootTable, "region_affinity");
|
||||
profile.ActivityAffinity = ReadFloatDictionary(_state, rootTable, "activity_affinity");
|
||||
|
||||
uint32 const guid = player->GetGUID().GetCounter();
|
||||
_profiles[guid] = std::move(profile);
|
||||
ClientlessAdventureProfile const& stored = _profiles.at(guid);
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot][Profile] loaded guid=%u name='%s' source='%s' home='%s' "
|
||||
"canGroundMount=%u mounts=%u mountMinDistance=%.1f mountSpeedRate=%.2f "
|
||||
"personality=%u regionAffinity=%u activityAffinity=%u",
|
||||
guid, player->GetName().c_str(), stored.SourcePath.c_str(), stored.HomeRegion.c_str(),
|
||||
stored.CanGroundMount ? 1u : 0u, uint32(stored.GroundMounts.size()),
|
||||
stored.GroundMountMinDistance, stored.GroundMountSpeedRate,
|
||||
uint32(stored.Personality.size()), uint32(stored.RegionAffinity.size()),
|
||||
uint32(stored.ActivityAffinity.size()));
|
||||
|
||||
if (stored.CanGroundMount && stored.GroundMounts.empty())
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u enables ground mounts but defines no ground_mounts", guid);
|
||||
|
||||
lua_settop(_state, base);
|
||||
return true;
|
||||
}
|
||||
|
||||
ClientlessAdventureProfile const* ClientlessLuaEngine::GetAdventureProfile(uint32 characterGuid) const
|
||||
{
|
||||
auto itr = _profiles.find(characterGuid);
|
||||
return itr != _profiles.end() ? &itr->second : nullptr;
|
||||
}
|
||||
|
||||
uint32 ClientlessLuaEngine::SelectGroundMountDisplayId(uint32 characterGuid) const
|
||||
{
|
||||
ClientlessAdventureProfile const* profile = GetAdventureProfile(characterGuid);
|
||||
if (!profile || profile->GroundMounts.empty())
|
||||
return 0;
|
||||
|
||||
uint64 total = 0;
|
||||
for (ClientlessWeightedMount const& mount : profile->GroundMounts)
|
||||
total += mount.Weight;
|
||||
if (!total)
|
||||
return 0;
|
||||
|
||||
uint32 const cappedTotal = uint32(std::min<uint64>(total, 0xFFFFFFFFull));
|
||||
uint32 roll = urand(1u, cappedTotal);
|
||||
for (ClientlessWeightedMount const& mount : profile->GroundMounts)
|
||||
{
|
||||
if (roll <= mount.Weight)
|
||||
return mount.DisplayId;
|
||||
roll -= std::min(roll, mount.Weight);
|
||||
}
|
||||
return profile->GroundMounts.back().DisplayId;
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::PrepareGroundMount(Player* player, float travelDistance, char const* context)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
|
||||
ClientlessAdventureProfile const* profile = GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if (!profile || !profile->CanGroundMount || profile->GroundMounts.empty())
|
||||
return false;
|
||||
if (travelDistance < profile->GroundMountMinDistance || !player->IsOutdoors())
|
||||
return false;
|
||||
if (player->IsMounted())
|
||||
return true;
|
||||
|
||||
uint32 const displayId = SelectGroundMountDisplayId(player->GetGUID().GetCounter());
|
||||
if (!displayId)
|
||||
return false;
|
||||
|
||||
// The AdventureBot profile is authoritative for this capability. Intentionally
|
||||
// no level, riding-skill, spellbook, inventory or economy eligibility check.
|
||||
player->Mount(displayId);
|
||||
player->SetSpeedRate(MOVE_RUN, profile->GroundMountSpeedRate);
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"GROUND_MOUNT guid=%u name='%s' displayId=%u distance=%.2f speedRate=%.2f context='%s'",
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(), displayId,
|
||||
travelDistance, profile->GroundMountSpeedRate, context ? context : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessLuaEngine::DismountGroundMount(Player* player, char const* reason)
|
||||
{
|
||||
if (!player || !player->IsMounted())
|
||||
return;
|
||||
|
||||
uint32 const oldDisplay = player->GetMountID();
|
||||
player->Dismount();
|
||||
player->UpdateSpeed(MOVE_RUN);
|
||||
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"GROUND_DISMOUNT guid=%u name='%s' displayId=%u reason='%s'",
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(), oldDisplay,
|
||||
reason ? reason : "");
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ExecuteAdventureScript(Player* player)
|
||||
{
|
||||
if (!player) return false;
|
||||
|
||||
@@ -5,11 +5,39 @@
|
||||
#include "Define.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ClientlessPlayerController;
|
||||
class Player;
|
||||
struct lua_State;
|
||||
|
||||
// CLIENTLESS_PLAYER_CORE_C8_12_ADVENTURE_PROFILE
|
||||
// Lua describes who a performative AdventureBot is; generic C++ systems decide
|
||||
// how those values become visible behavior.
|
||||
struct TC_GAME_API ClientlessWeightedMount
|
||||
{
|
||||
uint32 DisplayId = 0;
|
||||
uint32 Weight = 0;
|
||||
};
|
||||
|
||||
struct TC_GAME_API ClientlessAdventureProfile
|
||||
{
|
||||
std::string CharacterName;
|
||||
std::string HomeRegion;
|
||||
std::string SourcePath;
|
||||
|
||||
bool CanGroundMount = false;
|
||||
float GroundMountMinDistance = 60.0f;
|
||||
float GroundMountSpeedRate = 1.6f;
|
||||
std::vector<ClientlessWeightedMount> GroundMounts;
|
||||
|
||||
// Parsed now; consumed later by the weighted intent/day planner.
|
||||
std::unordered_map<std::string, float> Personality;
|
||||
std::unordered_map<std::string, float> RegionAffinity;
|
||||
std::unordered_map<std::string, float> ActivityAffinity;
|
||||
};
|
||||
|
||||
class TC_GAME_API ClientlessLuaEngine
|
||||
{
|
||||
public:
|
||||
@@ -18,6 +46,13 @@ public:
|
||||
|
||||
bool ExecuteAdventureScript(Player* player);
|
||||
bool ExecuteTestScript(Player* player, std::string const& scriptName);
|
||||
|
||||
bool LoadAdventureProfile(Player* player);
|
||||
ClientlessAdventureProfile const* GetAdventureProfile(uint32 characterGuid) const;
|
||||
uint32 SelectGroundMountDisplayId(uint32 characterGuid) const;
|
||||
bool PrepareGroundMount(Player* player, float travelDistance, char const* context);
|
||||
void DismountGroundMount(Player* player, char const* reason);
|
||||
|
||||
std::string const& GetScriptRoot() const { return _scriptRoot; }
|
||||
|
||||
private:
|
||||
@@ -27,6 +62,7 @@ private:
|
||||
|
||||
lua_State* _state = nullptr;
|
||||
std::string _scriptRoot;
|
||||
std::unordered_map<uint32, ClientlessAdventureProfile> _profiles;
|
||||
};
|
||||
|
||||
TC_GAME_API std::unique_ptr<ClientlessPlayerController> CreateClientlessAdventureLuaController();
|
||||
|
||||
@@ -184,7 +184,8 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
||||
std::string const& destinationNode,
|
||||
float startRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& error)
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds)
|
||||
{
|
||||
route.clear();
|
||||
error.clear();
|
||||
@@ -259,6 +260,11 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
||||
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
{
|
||||
if (blockedEdgeIds &&
|
||||
edge.DatabaseId &&
|
||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||
continue;
|
||||
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
if (candidate >= distance[edge.ToNode])
|
||||
continue;
|
||||
@@ -329,6 +335,381 @@ bool ClientlessTravelGraphMgr::PlanRoute(
|
||||
}
|
||||
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string MakeLocalBridgeKey(std::string const& from, std::string const& to)
|
||||
{
|
||||
return from + "->" + to;
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::FindNearestNodeToPosition(
|
||||
uint32 mapId,
|
||||
float x,
|
||||
float y,
|
||||
float z,
|
||||
std::string& nodeKey,
|
||||
float& resultDistance) const
|
||||
{
|
||||
nodeKey.clear();
|
||||
resultDistance = std::numeric_limits<float>::infinity();
|
||||
|
||||
for (auto const& pair : _nodes)
|
||||
{
|
||||
ClientlessTravelNode const& node = pair.second;
|
||||
if (node.MapId != mapId)
|
||||
continue;
|
||||
|
||||
float const dx = node.X - x;
|
||||
float const dy = node.Y - y;
|
||||
float const dz = node.Z - z;
|
||||
float const distance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (distance >= resultDistance)
|
||||
continue;
|
||||
|
||||
resultDistance = distance;
|
||||
nodeKey = node.Key;
|
||||
}
|
||||
|
||||
return !nodeKey.empty();
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
Player const* player,
|
||||
std::string const& destinationNode,
|
||||
float startRadius,
|
||||
float bridgeRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
bool& complete,
|
||||
std::string& reachedNode,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds,
|
||||
std::unordered_set<std::string> const* blockedBridgeKeys)
|
||||
{
|
||||
route.clear();
|
||||
reachedNode.clear();
|
||||
error.clear();
|
||||
complete = false;
|
||||
|
||||
EnsureLoaded();
|
||||
|
||||
if (!player || !player->IsInWorld())
|
||||
{
|
||||
error = "player is not in world";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto destinationItr = _nodes.find(destinationNode);
|
||||
if (destinationItr == _nodes.end())
|
||||
{
|
||||
error = "unknown destination node: " + destinationNode;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startRadius <= 0.0f)
|
||||
startRadius = 500.0f;
|
||||
if (bridgeRadius <= 0.0f)
|
||||
bridgeRadius = 300.0f;
|
||||
|
||||
struct State
|
||||
{
|
||||
std::string Node;
|
||||
bool LastWasBridge = false;
|
||||
};
|
||||
|
||||
auto stateKey = [](State const& state)
|
||||
{
|
||||
return state.Node + (state.LastWasBridge ? "\x1f" "B" : "\x1f" "R");
|
||||
};
|
||||
|
||||
struct QueueItem
|
||||
{
|
||||
float Cost = 0.0f;
|
||||
State Value;
|
||||
bool operator>(QueueItem const& other) const { return Cost > other.Cost; }
|
||||
};
|
||||
|
||||
struct Previous
|
||||
{
|
||||
std::string StateKey;
|
||||
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;
|
||||
std::unordered_map<std::string, State> states;
|
||||
std::unordered_map<std::string, uint32> persistentHops;
|
||||
|
||||
uint32 startCount = 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 localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (localDistance > startRadius)
|
||||
continue;
|
||||
|
||||
State state{ node.Key, false };
|
||||
std::string const key = stateKey(state);
|
||||
auto found = distance.find(key);
|
||||
if (found != distance.end() && found->second <= localDistance)
|
||||
continue;
|
||||
|
||||
distance[key] = localDistance;
|
||||
states[key] = state;
|
||||
persistentHops[key] = 0;
|
||||
open.push({ localDistance, state });
|
||||
++startCount;
|
||||
}
|
||||
|
||||
if (!startCount)
|
||||
{
|
||||
error = "no travel node near current position";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string bestGoalState;
|
||||
|
||||
while (!open.empty())
|
||||
{
|
||||
QueueItem current = open.top();
|
||||
open.pop();
|
||||
|
||||
std::string const currentKey = stateKey(current.Value);
|
||||
auto distanceItr = distance.find(currentKey);
|
||||
if (distanceItr == distance.end() || current.Cost != distanceItr->second)
|
||||
continue;
|
||||
|
||||
if (current.Value.Node == destinationNode)
|
||||
{
|
||||
bestGoalState = currentKey;
|
||||
break;
|
||||
}
|
||||
|
||||
// Persistent TravelGraph edges always reset the bridge guard.
|
||||
auto edgeItr = _edges.find(current.Value.Node);
|
||||
if (edgeItr != _edges.end())
|
||||
{
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
{
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
|
||||
State next{ edge.ToNode, false };
|
||||
std::string const nextKey = stateKey(next);
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && candidate >= old->second)
|
||||
continue;
|
||||
|
||||
distance[nextKey] = candidate;
|
||||
states[nextKey] = next;
|
||||
persistentHops[nextKey] = persistentHops[currentKey] + 1;
|
||||
previous[nextKey] = {
|
||||
currentKey, current.Value.Node, edge.Type,
|
||||
edge.DatabaseId, edge.Reverse
|
||||
};
|
||||
open.push({ candidate, next });
|
||||
}
|
||||
}
|
||||
|
||||
// A local bridge is deliberately impossible immediately after another
|
||||
// local bridge. This prevents MMAP from silently becoming a long-range
|
||||
// planner while still allowing unlimited bridges over a long journey
|
||||
// when real transport/ground edges lie between them.
|
||||
if (current.Value.LastWasBridge)
|
||||
continue;
|
||||
|
||||
ClientlessTravelNode const& from = _nodes.at(current.Value.Node);
|
||||
for (auto const& pair : _nodes)
|
||||
{
|
||||
ClientlessTravelNode const& to = pair.second;
|
||||
if (to.Key == from.Key || to.MapId != from.MapId)
|
||||
continue;
|
||||
|
||||
float const dx = to.X - from.X;
|
||||
float const dy = to.Y - from.Y;
|
||||
float const dz = to.Z - from.Z;
|
||||
float const direct = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (direct > bridgeRadius)
|
||||
continue;
|
||||
|
||||
std::string const bridgeKey = MakeLocalBridgeKey(from.Key, to.Key);
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count(bridgeKey))
|
||||
continue;
|
||||
|
||||
// A bridge must lead back into real TravelGraph infrastructure.
|
||||
// The sole exception is the actual requested destination, where a
|
||||
// local bridge may be the final leg of the graph journey.
|
||||
bool hasPersistentContinuation = to.Key == destinationNode;
|
||||
auto continuationItr = _edges.find(to.Key);
|
||||
if (!hasPersistentContinuation && continuationItr != _edges.end())
|
||||
{
|
||||
for (ClientlessTravelEdge const& continuation : continuationItr->second)
|
||||
{
|
||||
if (blockedEdgeIds && continuation.DatabaseId && blockedEdgeIds->count(continuation.DatabaseId))
|
||||
continue;
|
||||
hasPersistentContinuation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasPersistentContinuation)
|
||||
continue;
|
||||
|
||||
State next{ to.Key, true };
|
||||
std::string const nextKey = stateKey(next);
|
||||
|
||||
// Large fixed penalty makes a real graph path preferable whenever
|
||||
// one exists; distance then ranks alternative local bridges.
|
||||
float const candidate = current.Cost + 1000.0f + direct;
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && candidate >= old->second)
|
||||
continue;
|
||||
|
||||
distance[nextKey] = candidate;
|
||||
states[nextKey] = next;
|
||||
persistentHops[nextKey] = persistentHops[currentKey];
|
||||
previous[nextKey] = {
|
||||
currentKey, current.Value.Node, "local_bridge", 0, false
|
||||
};
|
||||
open.push({ candidate, next });
|
||||
}
|
||||
}
|
||||
|
||||
std::string selectedState = bestGoalState;
|
||||
if (!selectedState.empty())
|
||||
complete = true;
|
||||
|
||||
if (selectedState.empty())
|
||||
{
|
||||
ClientlessTravelNode const& destination = destinationItr->second;
|
||||
float bestSameMapDistance = std::numeric_limits<float>::infinity();
|
||||
uint32 bestPersistentHops = 0;
|
||||
float bestCost = std::numeric_limits<float>::infinity();
|
||||
|
||||
// Prefer any reachable point on the destination map. This gives a
|
||||
// meaningful geographic partial route (e.g. Ironforge when Kharanos
|
||||
// itself is disconnected). If the destination map was never reached,
|
||||
// fall back to the state that made the most progress over real edges.
|
||||
for (auto const& pair : states)
|
||||
{
|
||||
auto distItr = distance.find(pair.first);
|
||||
if (distItr == distance.end() || !std::isfinite(distItr->second))
|
||||
continue;
|
||||
|
||||
ClientlessTravelNode const& node = _nodes.at(pair.second.Node);
|
||||
if (node.MapId != destination.MapId)
|
||||
continue;
|
||||
|
||||
float const dx = node.X - destination.X;
|
||||
float const dy = node.Y - destination.Y;
|
||||
float const dz = node.Z - destination.Z;
|
||||
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
if (localDistance < bestSameMapDistance ||
|
||||
(std::fabs(localDistance - bestSameMapDistance) < 0.5f && distItr->second < bestCost))
|
||||
{
|
||||
bestSameMapDistance = localDistance;
|
||||
bestCost = distItr->second;
|
||||
selectedState = pair.first;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedState.empty())
|
||||
{
|
||||
for (auto const& pair : states)
|
||||
{
|
||||
auto distItr = distance.find(pair.first);
|
||||
if (distItr == distance.end() || !std::isfinite(distItr->second))
|
||||
continue;
|
||||
|
||||
uint32 const hops = persistentHops[pair.first];
|
||||
if (hops > bestPersistentHops ||
|
||||
(hops == bestPersistentHops && distItr->second < bestCost))
|
||||
{
|
||||
bestPersistentHops = hops;
|
||||
bestCost = distItr->second;
|
||||
selectedState = pair.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedState.empty())
|
||||
{
|
||||
error = "no adaptive route candidate";
|
||||
return false;
|
||||
}
|
||||
|
||||
reachedNode = states.at(selectedState).Node;
|
||||
|
||||
struct Reconstructed
|
||||
{
|
||||
State Value;
|
||||
std::string IncomingEdgeType;
|
||||
uint32 EdgeDatabaseId = 0;
|
||||
bool EdgeReverse = false;
|
||||
};
|
||||
|
||||
std::vector<Reconstructed> reversed;
|
||||
std::string cursor = selectedState;
|
||||
while (true)
|
||||
{
|
||||
State const& state = states.at(cursor);
|
||||
auto prevItr = previous.find(cursor);
|
||||
if (prevItr == previous.end())
|
||||
{
|
||||
reversed.push_back({ state, "start", 0, false });
|
||||
break;
|
||||
}
|
||||
|
||||
reversed.push_back({
|
||||
state,
|
||||
prevItr->second.EdgeType,
|
||||
prevItr->second.EdgeDatabaseId,
|
||||
prevItr->second.EdgeReverse
|
||||
});
|
||||
cursor = prevItr->second.StateKey;
|
||||
}
|
||||
|
||||
std::reverse(reversed.begin(), reversed.end());
|
||||
route.reserve(reversed.size());
|
||||
|
||||
for (Reconstructed const& item : reversed)
|
||||
{
|
||||
ClientlessTravelNode const& node = _nodes.at(item.Value.Node);
|
||||
ClientlessTravelRouteStep step;
|
||||
step.NodeKey = node.Key;
|
||||
step.NodeType = node.Type;
|
||||
step.WorldPointKey = node.WorldPointKey;
|
||||
step.EdgeType = item.IncomingEdgeType;
|
||||
step.EdgeDatabaseId = item.EdgeDatabaseId;
|
||||
step.EdgeReverse = item.EdgeReverse;
|
||||
step.MapId = node.MapId;
|
||||
step.X = node.X;
|
||||
step.Y = node.Y;
|
||||
step.Z = node.Z;
|
||||
step.O = node.O;
|
||||
route.push_back(std::move(step));
|
||||
}
|
||||
|
||||
if (!complete)
|
||||
error = "destination not fully reachable; returning best-effort partial route";
|
||||
|
||||
return !route.empty();
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
Player const* player,
|
||||
uint32 destinationMapId,
|
||||
@@ -339,7 +720,8 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
float anchorRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& anchorNode,
|
||||
std::string& error)
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds)
|
||||
{
|
||||
route.clear();
|
||||
anchorNode.clear();
|
||||
@@ -429,6 +811,11 @@ bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
||||
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
{
|
||||
if (blockedEdgeIds &&
|
||||
edge.DatabaseId &&
|
||||
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
||||
continue;
|
||||
|
||||
float const candidate = current.Cost + edge.Cost;
|
||||
if (candidate >= distance[edge.ToNode])
|
||||
continue;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class Player;
|
||||
@@ -86,7 +87,8 @@ public:
|
||||
std::string const& destinationNode,
|
||||
float startRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& error);
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
|
||||
|
||||
bool PlanRouteToPosition(
|
||||
Player const* player,
|
||||
@@ -98,7 +100,33 @@ public:
|
||||
float anchorRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
std::string& anchorNode,
|
||||
std::string& error);
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr);
|
||||
|
||||
// C8.13b: transient local bridges connect nearby graph islands for one
|
||||
// route plan. Bridges are never persisted and may never follow another
|
||||
// bridge directly; at least one real TravelGraph edge must lie between
|
||||
// two local bridges. If the destination remains unreachable, a useful
|
||||
// partial route is returned when possible.
|
||||
bool PlanAdaptiveRoute(
|
||||
Player const* player,
|
||||
std::string const& destinationNode,
|
||||
float startRadius,
|
||||
float bridgeRadius,
|
||||
std::vector<ClientlessTravelRouteStep>& route,
|
||||
bool& complete,
|
||||
std::string& reachedNode,
|
||||
std::string& error,
|
||||
std::unordered_set<uint32> const* blockedEdgeIds = nullptr,
|
||||
std::unordered_set<std::string> const* blockedBridgeKeys = nullptr);
|
||||
|
||||
bool FindNearestNodeToPosition(
|
||||
uint32 mapId,
|
||||
float x,
|
||||
float y,
|
||||
float z,
|
||||
std::string& nodeKey,
|
||||
float& distance) const;
|
||||
|
||||
uint32 GetNodeCount() const { return uint32(_nodes.size()); }
|
||||
uint32 GetEdgeCount() const { return _edgeCount; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
|
||||
#include "Log.h"
|
||||
#include "MotionMaster.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -16,7 +18,42 @@ namespace
|
||||
constexpr float MIN_PROGRESS_DISTANCE = 0.5f;
|
||||
constexpr uint32 PROGRESS_CHECK_INTERVAL_MS = 1000;
|
||||
constexpr uint32 REPATH_AFTER_NO_PROGRESS_MS = 5000;
|
||||
constexpr uint32 MAX_REPATHS = 3;
|
||||
constexpr uint32 MAX_REPATHS = 5;
|
||||
|
||||
constexpr float MAX_LOCAL_SPLINE_LENGTH = 45.0f;
|
||||
constexpr float MAX_PATH_FINAL_ERROR = 3.0f;
|
||||
constexpr float MAX_EXECUTION_DEVIATION = 8.0f;
|
||||
constexpr float MAX_SEGMENT_END_ERROR = 2.5f;
|
||||
|
||||
float Distance3D(float ax, float ay, float az, float bx, float by, float bz)
|
||||
{
|
||||
float const dx = bx - ax;
|
||||
float const dy = by - ay;
|
||||
float const dz = bz - az;
|
||||
return std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
float DistancePointToSegment(float px, float py, float pz, Position const& a, Position const& b)
|
||||
{
|
||||
float const abx = b.GetPositionX() - a.GetPositionX();
|
||||
float const aby = b.GetPositionY() - a.GetPositionY();
|
||||
float const abz = b.GetPositionZ() - a.GetPositionZ();
|
||||
float const apx = px - a.GetPositionX();
|
||||
float const apy = py - a.GetPositionY();
|
||||
float const apz = pz - a.GetPositionZ();
|
||||
|
||||
float const len2 = abx * abx + aby * aby + abz * abz;
|
||||
if (len2 <= 0.0001f)
|
||||
return Distance3D(px, py, pz, a.GetPositionX(), a.GetPositionY(), a.GetPositionZ());
|
||||
|
||||
float t = (apx * abx + apy * aby + apz * abz) / len2;
|
||||
t = std::max(0.0f, std::min(1.0f, t));
|
||||
|
||||
float const qx = a.GetPositionX() + abx * t;
|
||||
float const qy = a.GetPositionY() + aby * t;
|
||||
float const qz = a.GetPositionZ() + abz * t;
|
||||
return Distance3D(px, py, pz, qx, qy, qz);
|
||||
}
|
||||
}
|
||||
|
||||
ClientlessTravelMgr* ClientlessTravelMgr::instance()
|
||||
@@ -29,11 +66,12 @@ char const* ClientlessTravelMgr::StateName(ClientlessTravelState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ClientlessTravelState::Idle: return "Idle";
|
||||
case ClientlessTravelState::Moving: return "Moving";
|
||||
case ClientlessTravelState::Arrived: return "Arrived";
|
||||
case ClientlessTravelState::Stuck: return "Stuck";
|
||||
case ClientlessTravelState::InvalidMap: return "InvalidMap";
|
||||
case ClientlessTravelState::Idle: return "Idle";
|
||||
case ClientlessTravelState::Moving: return "Moving";
|
||||
case ClientlessTravelState::Arrived: return "Arrived";
|
||||
case ClientlessTravelState::Stuck: return "Stuck";
|
||||
case ClientlessTravelState::InvalidMap: return "InvalidMap";
|
||||
case ClientlessTravelState::PathRejected: return "PathRejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
@@ -59,10 +97,7 @@ bool ClientlessTravelMgr::AnalyzePath(
|
||||
float const ty = target.GetPositionY();
|
||||
float const tz = target.GetPositionZ();
|
||||
|
||||
float const ddx = tx - sx;
|
||||
float const ddy = ty - sy;
|
||||
float const ddz = tz - sz;
|
||||
diagnostics.DirectDistance = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
|
||||
diagnostics.DirectDistance = Distance3D(sx, sy, sz, tx, ty, tz);
|
||||
|
||||
PathGenerator path(player);
|
||||
diagnostics.Calculated = path.CalculatePath(tx, ty, tz);
|
||||
@@ -75,21 +110,16 @@ bool ClientlessTravelMgr::AnalyzePath(
|
||||
{
|
||||
for (std::size_t i = 1; i < points.size(); ++i)
|
||||
{
|
||||
float const dx = points[i].x - points[i - 1].x;
|
||||
float const dy = points[i].y - points[i - 1].y;
|
||||
float const dz = points[i].z - points[i - 1].z;
|
||||
float const segment = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
float const segment = Distance3D(
|
||||
points[i - 1].x, points[i - 1].y, points[i - 1].z,
|
||||
points[i].x, points[i].y, points[i].z);
|
||||
diagnostics.PathLength += segment;
|
||||
diagnostics.MaxSegmentLength = std::max(diagnostics.MaxSegmentLength, segment);
|
||||
diagnostics.MaxZDelta = std::max(diagnostics.MaxZDelta, std::fabs(dz));
|
||||
diagnostics.MaxZDelta = std::max(diagnostics.MaxZDelta, std::fabs(points[i].z - points[i - 1].z));
|
||||
}
|
||||
|
||||
auto const& last = points.back();
|
||||
float const dx = last.x - tx;
|
||||
float const dy = last.y - ty;
|
||||
float const dz = last.z - tz;
|
||||
diagnostics.FinalDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
diagnostics.FinalDistance = Distance3D(last.x, last.y, last.z, tx, ty, tz);
|
||||
}
|
||||
else
|
||||
diagnostics.FinalDistance = diagnostics.DirectDistance;
|
||||
@@ -120,7 +150,7 @@ bool ClientlessTravelMgr::AnalyzePath(
|
||||
addReason("large-segment");
|
||||
if (diagnostics.MaxZDelta > 8.0f)
|
||||
addReason("large-z-delta");
|
||||
if (diagnostics.FinalDistance > 3.0f)
|
||||
if (diagnostics.FinalDistance > MAX_PATH_FINAL_ERROR)
|
||||
addReason("far-from-target");
|
||||
if (diagnostics.DirectDistance > 5.0f && diagnostics.PathLength > diagnostics.DirectDistance * 8.0f)
|
||||
addReason("extreme-detour");
|
||||
@@ -131,79 +161,198 @@ bool ClientlessTravelMgr::AnalyzePath(
|
||||
return diagnostics.Calculated;
|
||||
}
|
||||
|
||||
void ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record)
|
||||
float ClientlessTravelMgr::DistanceFromActiveSpline(Player const* player, TravelRecord const& record) const
|
||||
{
|
||||
if (!player)
|
||||
return;
|
||||
if (!player || record.ActiveSpline.empty())
|
||||
return 0.0f;
|
||||
|
||||
if (record.ActiveSpline.size() == 1)
|
||||
{
|
||||
Position const& p = record.ActiveSpline.front();
|
||||
return Distance3D(
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
p.GetPositionX(), p.GetPositionY(), p.GetPositionZ());
|
||||
}
|
||||
|
||||
float best = std::numeric_limits<float>::max();
|
||||
for (std::size_t i = 1; i < record.ActiveSpline.size(); ++i)
|
||||
best = std::min(best, DistancePointToSegment(
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
record.ActiveSpline[i - 1], record.ActiveSpline[i]));
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
bool ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
|
||||
player->AttackStop();
|
||||
player->CastStop();
|
||||
player->StopMoving();
|
||||
player->GetMotionMaster()->Clear();
|
||||
record.ActiveSpline.clear();
|
||||
|
||||
Position const& target = record.Status.Target;
|
||||
|
||||
AnalyzePath(player, target, record.Status.Path);
|
||||
ClientlessPathDiagnostics const& nav = record.Status.Path;
|
||||
record.Status.NavSequence = _nextNavSequence++;
|
||||
|
||||
if (nav.Suspicious)
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"NAV_PLAN seq=%llu guid=%u name='%s' map=%u segment=%u "
|
||||
"start=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f) "
|
||||
"type=0x%X calculated=%u complete=%u suspicious=%u points=%u "
|
||||
"direct=%.2f path=%.2f maxSegment=%.2f maxZDelta=%.2f finalDistance=%.2f "
|
||||
"repaths=%u reason='%s'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(), player->GetMapId(),
|
||||
record.Status.SegmentIndex,
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||
nav.PathType, nav.Calculated ? 1u : 0u, nav.Complete ? 1u : 0u,
|
||||
nav.Suspicious ? 1u : 0u, nav.PointCount,
|
||||
nav.DirectDistance, nav.PathLength, nav.MaxSegmentLength,
|
||||
nav.MaxZDelta, nav.FinalDistance, record.Status.RepathCount,
|
||||
nav.Reason.c_str());
|
||||
|
||||
if (!nav.Calculated || !nav.Complete || nav.PointCount < 2 || nav.FinalDistance > MAX_PATH_FINAL_ERROR)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot][Nav] path-suspicious guid=%u name='%s' map=%u "
|
||||
"start=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f) "
|
||||
"type=0x%X calculated=%u complete=%u points=%u direct=%.2f path=%.2f "
|
||||
"maxSegment=%.2f maxZDelta=%.2f finalDistance=%.2f repath=%u reason='%s'",
|
||||
record.Status.State = ClientlessTravelState::PathRejected;
|
||||
record.Status.NoProgressMs = 0;
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_REJECT seq=%llu guid=%u name='%s' map=%u target=(%.3f %.3f %.3f) "
|
||||
"type=0x%X points=%u finalDistance=%.2f reason='%s'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(), player->GetMapId(),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||
nav.PathType, nav.Calculated ? 1u : 0u, nav.Complete ? 1u : 0u,
|
||||
nav.PointCount, nav.DirectDistance, nav.PathLength,
|
||||
nav.MaxSegmentLength, nav.MaxZDelta, nav.FinalDistance,
|
||||
record.Status.RepathCount, nav.Reason.c_str());
|
||||
nav.PathType, nav.PointCount, nav.FinalDistance, nav.Reason.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
PathGenerator path(player);
|
||||
if (!path.CalculatePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::PathRejected;
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_REJECT seq=%llu guid=%u name='%s' reason='execution-path-calc-failed'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const& points = path.GetPath();
|
||||
uint32 const executionType = uint32(path.GetPathType());
|
||||
if (points.size() < 2 ||
|
||||
(executionType & uint32(PATHFIND_NOPATH)) != 0 ||
|
||||
(executionType & uint32(PATHFIND_INCOMPLETE)) != 0)
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::PathRejected;
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_REJECT seq=%llu guid=%u name='%s' reason='execution-path-not-complete' type=0x%X points=%u",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(),
|
||||
executionType, uint32(points.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Position> spline;
|
||||
spline.reserve(points.size() + 1);
|
||||
|
||||
Position current;
|
||||
current.Relocate(
|
||||
player->GetPositionX(), player->GetPositionY(),
|
||||
player->GetPositionZ(), player->GetOrientation());
|
||||
spline.push_back(current);
|
||||
|
||||
float lastX = current.GetPositionX();
|
||||
float lastY = current.GetPositionY();
|
||||
float lastZ = current.GetPositionZ();
|
||||
float accumulated = 0.0f;
|
||||
|
||||
for (std::size_t i = 1; i < points.size(); ++i)
|
||||
{
|
||||
float const px = points[i].x;
|
||||
float const py = points[i].y;
|
||||
float const pz = points[i].z;
|
||||
float const segment = Distance3D(lastX, lastY, lastZ, px, py, pz);
|
||||
|
||||
if (segment <= 0.05f)
|
||||
continue;
|
||||
|
||||
if (accumulated + segment > MAX_LOCAL_SPLINE_LENGTH)
|
||||
{
|
||||
float const remaining = MAX_LOCAL_SPLINE_LENGTH - accumulated;
|
||||
float const t = std::max(0.0f, std::min(1.0f, remaining / segment));
|
||||
|
||||
Position partial;
|
||||
partial.Relocate(
|
||||
lastX + (px - lastX) * t,
|
||||
lastY + (py - lastY) * t,
|
||||
lastZ + (pz - lastZ) * t,
|
||||
player->GetOrientation());
|
||||
spline.push_back(partial);
|
||||
accumulated = MAX_LOCAL_SPLINE_LENGTH;
|
||||
break;
|
||||
}
|
||||
|
||||
Position p;
|
||||
p.Relocate(px, py, pz, player->GetOrientation());
|
||||
spline.push_back(p);
|
||||
accumulated += segment;
|
||||
lastX = px;
|
||||
lastY = py;
|
||||
lastZ = pz;
|
||||
|
||||
if (accumulated >= MAX_LOCAL_SPLINE_LENGTH)
|
||||
break;
|
||||
}
|
||||
|
||||
if (spline.size() < 2)
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::PathRejected;
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_REJECT seq=%llu guid=%u name='%s' reason='no-usable-spline-points'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
record.ActiveSpline = spline;
|
||||
++record.Status.SegmentIndex;
|
||||
|
||||
if (record.AllowGroundMount)
|
||||
sClientlessLuaEngine->PrepareGroundMount(player, nav.DirectDistance, "local-ground");
|
||||
else
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot][Nav] path guid=%u name='%s' map=%u "
|
||||
"type=0x%X points=%u direct=%.2f path=%.2f maxSegment=%.2f "
|
||||
"maxZDelta=%.2f finalDistance=%.2f repath=%u",
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(), player->GetMapId(),
|
||||
nav.PathType, nav.PointCount, nav.DirectDistance, nav.PathLength,
|
||||
nav.MaxSegmentLength, nav.MaxZDelta, nav.FinalDistance,
|
||||
record.Status.RepathCount);
|
||||
}
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "local-ground-disabled");
|
||||
|
||||
PathGenerator debugPath(player);
|
||||
if (debugPath.CalculatePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
|
||||
{
|
||||
auto const& debugPoints = debugPath.GetPath();
|
||||
for (std::size_t i = 0; i < debugPoints.size(); ++i)
|
||||
TC_LOG_DEBUG("server.adventurebot",
|
||||
"[AdventureBot][Nav] path-point guid=%u repath=%u index=%u/%u xyz=(%.3f %.3f %.3f)",
|
||||
player->GetGUID().GetCounter(), record.Status.RepathCount,
|
||||
uint32(i + 1), uint32(debugPoints.size()),
|
||||
debugPoints[i].x, debugPoints[i].y, debugPoints[i].z);
|
||||
}
|
||||
player->GetMotionMaster()->MoveSmoothPath(
|
||||
1, record.ActiveSpline.data(), record.ActiveSpline.size(), false);
|
||||
|
||||
player->GetMotionMaster()->MovePoint(
|
||||
1,
|
||||
target.GetPositionX(),
|
||||
target.GetPositionY(),
|
||||
target.GetPositionZ(),
|
||||
true);
|
||||
Position const& segmentEnd = record.ActiveSpline.back();
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"NAV_EXEC seq=%llu guid=%u name='%s' segment=%u splinePoints=%u "
|
||||
"splineLength=%.2f segmentEnd=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f)",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
player->GetGUID().GetCounter(), player->GetName().c_str(),
|
||||
record.Status.SegmentIndex, uint32(record.ActiveSpline.size()), accumulated,
|
||||
segmentEnd.GetPositionX(), segmentEnd.GetPositionY(), segmentEnd.GetPositionZ(),
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||
|
||||
record.Status.State = ClientlessTravelState::Moving;
|
||||
record.LastDistance = player->GetDistance(
|
||||
target.GetPositionX(),
|
||||
target.GetPositionY(),
|
||||
target.GetPositionZ());
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||
record.Status.RemainingDistance = record.LastDistance;
|
||||
record.Status.NoProgressMs = 0;
|
||||
record.LastObservedPosition.Relocate(player->GetPosition());
|
||||
record.HasLastObservedPosition = true;
|
||||
record.CheckTimerMs = PROGRESS_CHECK_INTERVAL_MS;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessTravelMgr::StartTravel(Player* player, Position const& target)
|
||||
bool ClientlessTravelMgr::StartTravel(Player* player, Position const& target, bool allowGroundMount)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return false;
|
||||
@@ -211,21 +360,19 @@ bool ClientlessTravelMgr::StartTravel(Player* player, Position const& target)
|
||||
uint32 const characterGuid = player->GetGUID().GetCounter();
|
||||
|
||||
TravelRecord& record = _records[characterGuid];
|
||||
record.Status = {};
|
||||
record = {};
|
||||
record.Status.State = ClientlessTravelState::Moving;
|
||||
record.Status.MapId = player->GetMapId();
|
||||
record.Status.Target = target;
|
||||
record.AllowGroundMount = allowGroundMount;
|
||||
|
||||
LaunchPath(player, record);
|
||||
|
||||
TC_LOG_INFO("server.worldserver",
|
||||
"ClientlessTravel started character GUID %u map=%u target=(%.3f, %.3f, %.3f) distance=%.2f.",
|
||||
characterGuid,
|
||||
record.Status.MapId,
|
||||
target.GetPositionX(),
|
||||
target.GetPositionY(),
|
||||
target.GetPositionZ(),
|
||||
record.Status.RemainingDistance);
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"NAV_START guid=%u name='%s' map=%u target=(%.3f %.3f %.3f) remaining=%.2f state=%s",
|
||||
characterGuid, player->GetName().c_str(), record.Status.MapId,
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
|
||||
record.Status.RemainingDistance, StateName(record.Status.State));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -265,14 +412,13 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
if (player->GetMapId() != record.Status.MapId)
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::InvalidMap;
|
||||
TC_LOG_WARN("server.worldserver",
|
||||
"ClientlessTravel character GUID %u changed map during point travel (expected=%u actual=%u).",
|
||||
characterGuid, record.Status.MapId, player->GetMapId());
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_INVALID_MAP guid=%u name='%s' expected=%u actual=%u",
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.MapId, player->GetMapId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// AdventureBot maintenance diagnostics. Keep Trinity's recovery behavior,
|
||||
// but record suspicious large position discontinuities for route analysis.
|
||||
if (record.HasLastObservedPosition)
|
||||
{
|
||||
float const observedJump = player->GetExactDist(
|
||||
@@ -281,10 +427,10 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
record.LastObservedPosition.GetPositionZ());
|
||||
|
||||
if (observedJump >= 15.0f)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot] movement-recovery-jump guid=%u name='%s' map=%u jump=%.2f "
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_ANOMALY seq=%llu guid=%u name='%s' kind='position-jump' map=%u jump=%.2f "
|
||||
"from=(%.3f %.3f %.3f) to=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f) repaths=%u",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(), player->GetMapId(), observedJump,
|
||||
record.LastObservedPosition.GetPositionX(),
|
||||
record.LastObservedPosition.GetPositionY(),
|
||||
@@ -294,7 +440,6 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
record.Status.Target.GetPositionY(),
|
||||
record.Status.Target.GetPositionZ(),
|
||||
record.Status.RepathCount);
|
||||
}
|
||||
}
|
||||
|
||||
record.LastObservedPosition.Relocate(player->GetPosition());
|
||||
@@ -302,26 +447,85 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
|
||||
Position const& target = record.Status.Target;
|
||||
float const distance = player->GetDistance(
|
||||
target.GetPositionX(),
|
||||
target.GetPositionY(),
|
||||
target.GetPositionZ());
|
||||
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
|
||||
record.Status.RemainingDistance = distance;
|
||||
|
||||
// C8.2 arrival fix:
|
||||
// Do not accept "close enough" while the point movement is still active.
|
||||
// A bot blocked by geometry may get very near the target without actually
|
||||
// reaching it. Requiring a tight tolerance plus a stopped movement state
|
||||
// lets that case fall through into normal no-progress/repath/stuck handling.
|
||||
if (distance <= ARRIVAL_DISTANCE && player->IsStopped())
|
||||
float const deviation = DistanceFromActiveSpline(player, record);
|
||||
record.Status.MaxObservedDeviation = std::max(record.Status.MaxObservedDeviation, deviation);
|
||||
|
||||
TC_LOG_DEBUG("server.adventurebot.navigation",
|
||||
"NAV_SAMPLE seq=%llu guid=%u segment=%u pos=(%.3f %.3f %.3f) "
|
||||
"remaining=%.2f deviation=%.2f splineActive=%u repaths=%u",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, record.Status.SegmentIndex,
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
distance, deviation, player->IsSplineEnabled() ? 1u : 0u,
|
||||
record.Status.RepathCount);
|
||||
|
||||
if (deviation > MAX_EXECUTION_DEVIATION)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_ANOMALY seq=%llu guid=%u name='%s' kind='left-planned-path' "
|
||||
"deviation=%.2f remaining=%.2f repaths=%u",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(),
|
||||
deviation, distance, record.Status.RepathCount);
|
||||
|
||||
if (record.Status.RepathCount >= MAX_REPATHS)
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::Stuck;
|
||||
player->StopMoving();
|
||||
player->GetMotionMaster()->Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
++record.Status.RepathCount;
|
||||
LaunchPath(player, record);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (distance <= ARRIVAL_DISTANCE && !player->IsSplineEnabled())
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::Arrived;
|
||||
record.Status.NoProgressMs = 0;
|
||||
TC_LOG_INFO("server.worldserver",
|
||||
"ClientlessTravel character GUID %u arrived (remaining=%.2f, repaths=%u).",
|
||||
characterGuid, distance, record.Status.RepathCount);
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"NAV_COMPLETE seq=%llu guid=%u name='%s' remaining=%.2f repaths=%u "
|
||||
"segments=%u maxDeviation=%.2f",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(), distance,
|
||||
record.Status.RepathCount, record.Status.SegmentIndex,
|
||||
record.Status.MaxObservedDeviation);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!player->IsSplineEnabled() && !record.ActiveSpline.empty())
|
||||
{
|
||||
Position const& segmentEnd = record.ActiveSpline.back();
|
||||
float const endError = Distance3D(
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
|
||||
segmentEnd.GetPositionX(), segmentEnd.GetPositionY(), segmentEnd.GetPositionZ());
|
||||
|
||||
if (endError <= MAX_SEGMENT_END_ERROR)
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"NAV_SEGMENT_COMPLETE seq=%llu guid=%u name='%s' segment=%u "
|
||||
"endError=%.2f remaining=%.2f",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.SegmentIndex, endError, distance);
|
||||
|
||||
LaunchPath(player, record);
|
||||
continue;
|
||||
}
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_ANOMALY seq=%llu guid=%u name='%s' kind='spline-stopped-early' "
|
||||
"segment=%u endError=%.2f remaining=%.2f",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(),
|
||||
record.Status.SegmentIndex, endError, distance);
|
||||
}
|
||||
|
||||
float const progress = record.LastDistance - distance;
|
||||
if (progress >= MIN_PROGRESS_DISTANCE)
|
||||
record.Status.NoProgressMs = 0;
|
||||
@@ -337,12 +541,12 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
{
|
||||
record.Status.State = ClientlessTravelState::Stuck;
|
||||
player->StopMoving();
|
||||
TC_LOG_WARN("server.worldserver",
|
||||
"ClientlessTravel character GUID %u is stuck (remaining=%.2f, repaths=%u).",
|
||||
characterGuid, distance, record.Status.RepathCount);
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot][Nav] stuck guid=%u name='%s' map=%u remaining=%.2f "
|
||||
"repaths=%u lastPathType=0x%X lastPathPoints=%u lastReason='%s'",
|
||||
player->GetMotionMaster()->Clear();
|
||||
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_STUCK seq=%llu guid=%u name='%s' map=%u remaining=%.2f repaths=%u "
|
||||
"lastPathType=0x%X lastPathPoints=%u lastReason='%s'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(), player->GetMapId(), distance,
|
||||
record.Status.RepathCount, record.Status.Path.PathType,
|
||||
record.Status.Path.PointCount, record.Status.Path.Reason.c_str());
|
||||
@@ -350,14 +554,10 @@ void ClientlessTravelMgr::Update(uint32 diff)
|
||||
}
|
||||
|
||||
++record.Status.RepathCount;
|
||||
|
||||
TC_LOG_INFO("server.worldserver",
|
||||
"ClientlessTravel repathing character GUID %u (remaining=%.2f, repath=%u/%u).",
|
||||
characterGuid, distance, record.Status.RepathCount, MAX_REPATHS);
|
||||
|
||||
TC_LOG_WARN("server.adventurebot",
|
||||
"[AdventureBot][Nav] repath guid=%u name='%s' map=%u remaining=%.2f "
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"NAV_REPATH seq=%llu guid=%u name='%s' map=%u remaining=%.2f "
|
||||
"noProgressMs=%u repath=%u/%u reason='no-progress'",
|
||||
static_cast<unsigned long long>(record.Status.NavSequence),
|
||||
characterGuid, player->GetName().c_str(), player->GetMapId(), distance,
|
||||
record.Status.NoProgressMs, record.Status.RepathCount, MAX_REPATHS);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Position.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct TC_GAME_API ClientlessPathDiagnostics
|
||||
{
|
||||
@@ -33,7 +34,8 @@ enum class ClientlessTravelState : uint8
|
||||
Moving,
|
||||
Arrived,
|
||||
Stuck,
|
||||
InvalidMap
|
||||
InvalidMap,
|
||||
PathRejected
|
||||
};
|
||||
|
||||
struct TC_GAME_API ClientlessTravelStatus
|
||||
@@ -44,6 +46,9 @@ struct TC_GAME_API ClientlessTravelStatus
|
||||
float RemainingDistance = 0.0f;
|
||||
uint32 RepathCount = 0;
|
||||
uint32 NoProgressMs = 0;
|
||||
uint64 NavSequence = 0;
|
||||
uint32 SegmentIndex = 0;
|
||||
float MaxObservedDeviation = 0.0f;
|
||||
ClientlessPathDiagnostics Path;
|
||||
};
|
||||
|
||||
@@ -52,7 +57,7 @@ class TC_GAME_API ClientlessTravelMgr
|
||||
public:
|
||||
static ClientlessTravelMgr* instance();
|
||||
|
||||
bool StartTravel(Player* player, Position const& target);
|
||||
bool StartTravel(Player* player, Position const& target, bool allowGroundMount = false);
|
||||
bool AnalyzePath(Player* player, Position const& target, ClientlessPathDiagnostics& diagnostics) const;
|
||||
void CancelTravel(uint32 characterGuid);
|
||||
ClientlessTravelStatus const* GetStatus(uint32 characterGuid) const;
|
||||
@@ -61,6 +66,7 @@ public:
|
||||
static char const* StateName(ClientlessTravelState state);
|
||||
|
||||
private:
|
||||
// C8.11b: hardened explicit MMAP local navigation.
|
||||
struct TravelRecord
|
||||
{
|
||||
ClientlessTravelStatus Status;
|
||||
@@ -68,11 +74,15 @@ private:
|
||||
Position LastObservedPosition;
|
||||
bool HasLastObservedPosition = false;
|
||||
uint32 CheckTimerMs = 0;
|
||||
std::vector<Position> ActiveSpline;
|
||||
bool AllowGroundMount = false;
|
||||
};
|
||||
|
||||
void LaunchPath(Player* player, TravelRecord& record);
|
||||
bool LaunchPath(Player* player, TravelRecord& record);
|
||||
float DistanceFromActiveSpline(Player const* player, TravelRecord const& record) const;
|
||||
|
||||
std::unordered_map<uint32, TravelRecord> _records;
|
||||
uint64 _nextNavSequence = 1;
|
||||
};
|
||||
|
||||
#define sClientlessTravelMgr ClientlessTravelMgr::instance()
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
-- Arthoran completely overrides generic.lua.
|
||||
log("Arthoran adventure override loaded")
|
||||
intend("socialize", { city = "Ironforge", venue = "tavern", activity = "drink", purpose = "leisure" })
|
||||
log("Arthoran C8.13b bridge test loaded")
|
||||
|
||||
-- C8.4 end-to-end semantic action test:
|
||||
-- select the nearest perceived vendor and visibly walk to it.
|
||||
--queue_action("approach", {
|
||||
-- capability = "vendor",
|
||||
-- max_distance = 40,
|
||||
-- approach_distance = 2
|
||||
--})
|
||||
queue_action("travel_to_location", {
|
||||
id = "kharanos_tavern"
|
||||
})
|
||||
-- C8.6 incremental travel-graph example.
|
||||
-- Enable after the corresponding graph nodes/edges exist in cata_world.
|
||||
--
|
||||
-- queue_action("travel_to_node", {
|
||||
-- node = "stormwind_gate",
|
||||
-- start_radius = 500
|
||||
-- })
|
||||
|
||||
--queue_action("travel_to_location", {
|
||||
-- id = "kharanos_tavern"
|
||||
--})
|
||||
|
||||
queue_action("travel_to_node", {
|
||||
node = "portal_at_2173_target",
|
||||
bridge_radius = 400,
|
||||
bridge_path_max = 600
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
-- Arthoran: first named AdventureBot character profile.
|
||||
-- These values bias future weighted decisions; they do not prescribe a route.
|
||||
return {
|
||||
identity = {
|
||||
name = "Arthoran",
|
||||
home_region = "elwynn",
|
||||
},
|
||||
|
||||
movement = {
|
||||
can_ground_mount = true,
|
||||
ground_mount_min_distance = 45,
|
||||
ground_mount_speed_rate = 1.60,
|
||||
-- [mount display ID] = random weight
|
||||
ground_mounts = {
|
||||
[2404] = 100, -- Brown Horse
|
||||
},
|
||||
},
|
||||
|
||||
personality = {
|
||||
sociability = 0.65,
|
||||
adventurousness = 0.70,
|
||||
wanderlust = 0.30,
|
||||
tavern_affinity = 0.85,
|
||||
wilderness_affinity = 0.65,
|
||||
city_affinity = 0.35,
|
||||
danger_tolerance = 0.70,
|
||||
},
|
||||
|
||||
region_affinity = {
|
||||
elwynn = 1.00,
|
||||
stormwind = 0.70,
|
||||
dun_morogh = 0.35,
|
||||
blasted_lands = 0.65,
|
||||
},
|
||||
|
||||
activity_affinity = {
|
||||
tavern = 0.90,
|
||||
hunting = 0.75,
|
||||
travelling = 0.35,
|
||||
city_visit = 0.35,
|
||||
social = 0.60,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
-- C8.12 generic AdventureBot character profile.
|
||||
-- Character data/capabilities, not a hard-coded daily route.
|
||||
return {
|
||||
identity = {
|
||||
name = character_name,
|
||||
home_region = "",
|
||||
},
|
||||
|
||||
movement = {
|
||||
can_ground_mount = true,
|
||||
ground_mount_min_distance = 60,
|
||||
ground_mount_speed_rate = 1.60,
|
||||
ground_mounts = {
|
||||
[2404] = 100, -- Brown Horse display ID
|
||||
},
|
||||
},
|
||||
|
||||
personality = {
|
||||
sociability = 0.50,
|
||||
adventurousness = 0.50,
|
||||
wanderlust = 0.50,
|
||||
},
|
||||
region_affinity = {},
|
||||
activity_affinity = {},
|
||||
}
|
||||
@@ -1313,6 +1313,215 @@ static bool HandleClientlessTaxiInfo(ChatHandler* handler, char const* args)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessPortalImport(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
uint32 imported = 0;
|
||||
uint32 missingDbc = 0;
|
||||
uint32 skippedSameMap = 0;
|
||||
uint32 createdPoints = 0;
|
||||
uint32 updatedPoints = 0;
|
||||
uint32 createdNodes = 0;
|
||||
uint32 updatedNodes = 0;
|
||||
uint32 createdEdges = 0;
|
||||
uint32 existingEdges = 0;
|
||||
|
||||
QueryResult result = WorldDatabase.Query(
|
||||
"SELECT ID, Name, target_map, target_position_x, target_position_y, "
|
||||
"target_position_z, target_orientation "
|
||||
"FROM areatrigger_teleport ORDER BY ID");
|
||||
|
||||
if (!result)
|
||||
{
|
||||
handler->SendSysMessage("Portal import failed: areatrigger_teleport is empty.");
|
||||
return true;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint32 const triggerId = fields[0].GetUInt32();
|
||||
uint32 const targetMap = fields[2].GetUInt16();
|
||||
float const targetX = fields[3].GetFloat();
|
||||
float const targetY = fields[4].GetFloat();
|
||||
float const targetZ = fields[5].GetFloat();
|
||||
float const targetO = fields[6].GetFloat();
|
||||
|
||||
AreaTriggerEntry const* source = sAreaTriggerStore.LookupEntry(triggerId);
|
||||
if (!source)
|
||||
{
|
||||
++missingDbc;
|
||||
continue;
|
||||
}
|
||||
|
||||
// C8.13a deliberately imports map transitions only. Same-map AreaTrigger
|
||||
// teleports require the clientless equivalent of MOVE_TELEPORT_ACK and
|
||||
// are left for a later extension. The important world/instance/tram
|
||||
// transitions all use a far map transfer.
|
||||
if (uint32(source->ContinentID) == targetMap)
|
||||
{
|
||||
++skippedSameMap;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string const baseKey = "portal_at_" + std::to_string(triggerId);
|
||||
std::string const sourceKey = baseKey + "_source";
|
||||
std::string const targetKey = baseKey + "_target";
|
||||
|
||||
auto upsertPoint = [&](std::string const& key, uint32 mapId,
|
||||
float x, float y, float z, float o,
|
||||
char const* side)
|
||||
{
|
||||
QueryResult exists = WorldDatabase.PQuery(
|
||||
"SELECT id FROM vcore_clientless_world_point WHERE point_key='%s' LIMIT 1",
|
||||
key.c_str());
|
||||
|
||||
if (exists)
|
||||
{
|
||||
WorldDatabase.DirectPExecute(
|
||||
"UPDATE vcore_clientless_world_point "
|
||||
"SET category='travel_portal',map=%u,position_x=%.6f,position_y=%.6f,"
|
||||
"position_z=%.6f,orientation=%.6f,enabled=1,"
|
||||
"comment='Auto-imported AreaTrigger %u %s' WHERE point_key='%s'",
|
||||
mapId, x, y, z, o, triggerId, side, key.c_str());
|
||||
++updatedPoints;
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldDatabase.DirectPExecute(
|
||||
"INSERT INTO vcore_clientless_world_point "
|
||||
"(point_key,category,map,position_x,position_y,position_z,orientation,enabled,comment) "
|
||||
"VALUES ('%s','travel_portal',%u,%.6f,%.6f,%.6f,%.6f,1,"
|
||||
"'Auto-imported AreaTrigger %u %s')",
|
||||
key.c_str(), mapId, x, y, z, o, triggerId, side);
|
||||
++createdPoints;
|
||||
}
|
||||
};
|
||||
|
||||
auto upsertNode = [&](std::string const& key, char const* side)
|
||||
{
|
||||
QueryResult exists = WorldDatabase.PQuery(
|
||||
"SELECT id FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
|
||||
key.c_str());
|
||||
|
||||
if (exists)
|
||||
{
|
||||
WorldDatabase.DirectPExecute(
|
||||
"UPDATE vcore_clientless_travel_node "
|
||||
"SET node_type='portal',world_point_key='%s',enabled=1,"
|
||||
"comment='Auto-imported AreaTrigger %u %s' WHERE node_key='%s'",
|
||||
key.c_str(), triggerId, side, key.c_str());
|
||||
++updatedNodes;
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldDatabase.DirectPExecute(
|
||||
"INSERT INTO vcore_clientless_travel_node "
|
||||
"(node_key,node_type,world_point_key,enabled,comment) "
|
||||
"VALUES ('%s','portal','%s',1,'Auto-imported AreaTrigger %u %s')",
|
||||
key.c_str(), key.c_str(), triggerId, side);
|
||||
++createdNodes;
|
||||
}
|
||||
};
|
||||
|
||||
upsertPoint(sourceKey, uint32(source->ContinentID),
|
||||
source->Pos.X, source->Pos.Y, source->Pos.Z, 0.0f, "source");
|
||||
upsertPoint(targetKey, targetMap,
|
||||
targetX, targetY, targetZ, targetO, "target");
|
||||
upsertNode(sourceKey, "source");
|
||||
upsertNode(targetKey, "target");
|
||||
|
||||
QueryResult edgeExists = WorldDatabase.PQuery(
|
||||
"SELECT id FROM vcore_clientless_travel_edge "
|
||||
"WHERE from_node_key='%s' AND to_node_key='%s' AND edge_type='portal' LIMIT 1",
|
||||
sourceKey.c_str(), targetKey.c_str());
|
||||
|
||||
if (edgeExists)
|
||||
++existingEdges;
|
||||
else
|
||||
{
|
||||
WorldDatabase.DirectPExecute(
|
||||
"INSERT INTO vcore_clientless_travel_edge "
|
||||
"(from_node_key,to_node_key,edge_type,cost,bidirectional,enabled,comment) "
|
||||
"VALUES ('%s','%s','portal',1,0,1,'Auto-imported AreaTrigger %u')",
|
||||
sourceKey.c_str(), targetKey.c_str(), triggerId);
|
||||
++createdEdges;
|
||||
}
|
||||
|
||||
++imported;
|
||||
}
|
||||
while (result->NextRow());
|
||||
|
||||
bool const graphLoaded = sClientlessTravelGraphMgr->Reload();
|
||||
sClientlessWorldPointMgr->Reload();
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"Portal import complete: transitions=%u points(created=%u updated=%u) "
|
||||
"nodes(created=%u updated=%u) edges(created=%u existing=%u) "
|
||||
"skipped(sameMap=%u missingDBC=%u).",
|
||||
imported, createdPoints, updatedPoints, createdNodes, updatedNodes,
|
||||
createdEdges, existingEdges, skippedSameMap, missingDbc);
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"Travel graph reloaded: %u node(s), %u directed edge(s)%s. "
|
||||
"C8.13a executes far-map AreaTrigger portal edges with the normal Trinity handler.",
|
||||
sClientlessTravelGraphMgr->GetNodeCount(),
|
||||
sClientlessTravelGraphMgr->GetEdgeCount(),
|
||||
graphLoaded ? "" : " (no usable nodes)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessPortalInfo(ChatHandler* handler, char const* args)
|
||||
{
|
||||
std::istringstream input(args ? args : "");
|
||||
uint32 triggerId = 0;
|
||||
input >> triggerId;
|
||||
|
||||
if (!triggerId)
|
||||
{
|
||||
handler->SendSysMessage("Usage: .clientless portal info <AreaTriggerId>");
|
||||
return true;
|
||||
}
|
||||
|
||||
AreaTriggerEntry const* source = sAreaTriggerStore.LookupEntry(triggerId);
|
||||
if (!source)
|
||||
{
|
||||
handler->PSendSysMessage("AreaTrigger %u does not exist in AreaTrigger.dbc.", triggerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
QueryResult result = WorldDatabase.PQuery(
|
||||
"SELECT Name,target_map,target_position_x,target_position_y,target_position_z,target_orientation "
|
||||
"FROM areatrigger_teleport WHERE ID=%u LIMIT 1",
|
||||
triggerId);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"AreaTrigger %u: source map=%u pos=(%.2f %.2f %.2f), but it has no areatrigger_teleport row.",
|
||||
triggerId, uint32(source->ContinentID), source->Pos.X, source->Pos.Y, source->Pos.Z);
|
||||
return true;
|
||||
}
|
||||
|
||||
Field* fields = result->Fetch();
|
||||
std::string const name = fields[0].GetString();
|
||||
uint32 const targetMap = fields[1].GetUInt16();
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"AreaTrigger %u '%s': source map=%u pos=(%.2f %.2f %.2f) radius=%.2f box=(%.2f %.2f %.2f yaw=%.2f)",
|
||||
triggerId, name.c_str(), uint32(source->ContinentID),
|
||||
source->Pos.X, source->Pos.Y, source->Pos.Z, source->Radius,
|
||||
source->Box_length, source->Box_width, source->Box_height, source->Box_yaw);
|
||||
|
||||
handler->PSendSysMessage(
|
||||
" target map=%u pos=(%.2f %.2f %.2f o=%.2f) keys=portal_at_%u_source -> portal_at_%u_target%s",
|
||||
targetMap, fields[2].GetFloat(), fields[3].GetFloat(), fields[4].GetFloat(), fields[5].GetFloat(),
|
||||
triggerId, triggerId,
|
||||
uint32(source->ContinentID) == targetMap ? " [same-map: not imported by C8.13a]" : "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
bool const loaded = sClientlessTravelGraphMgr->Reload();
|
||||
@@ -1960,6 +2169,10 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiImport, "" },
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
|
||||
} },
|
||||
{ "portal", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", {
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessPortalImport, "" },
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessPortalInfo, "" },
|
||||
} },
|
||||
{ "travelreload", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTravelReload, "" },
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user