importet Areateleports as Nodes
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-16 09:36:33 +00:00
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);
+262 -1
View File
@@ -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; }
+309 -109
View File
@@ -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);
+13 -3
View File
@@ -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, "" },
};