Implemented travel graph
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-13 19:39:53 +00:00
1 parent 05fd748537
commit b14c864814
19 files changed
+3835

No files matched your search

@@ -0,0 +1,815 @@
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessPerceptionMgr.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessWorldPointMgr.h"
#include "Log.h"
#include "Player.h"
#include "MotionMaster.h"
#include <algorithm>
#include "Position.h"
#include "World.h"
#include "WorldSession.h"
#include <array>
#include <cmath>
#include <cstdlib>
#include <utility>
namespace
{
struct NamedCapability
{
char const* Name;
uint64 Flag;
};
constexpr std::array<NamedCapability, 14> CAPABILITIES =
{{
{ "vendor", CLIENTLESS_CAP_VENDOR },
{ "trainer", CLIENTLESS_CAP_TRAINER },
{ "flightmaster", CLIENTLESS_CAP_FLIGHTMASTER },
{ "banker", CLIENTLESS_CAP_BANKER },
{ "innkeeper", CLIENTLESS_CAP_INNKEEPER },
{ "auctioneer", CLIENTLESS_CAP_AUCTIONEER },
{ "questgiver", CLIENTLESS_CAP_QUESTGIVER },
{ "gossip", CLIENTLESS_CAP_GOSSIP },
{ "repair", CLIENTLESS_CAP_REPAIR },
{ "battlemaster", CLIENTLESS_CAP_BATTLEMASTER },
{ "petitioner", CLIENTLESS_CAP_PETITIONER },
{ "spirithealer", CLIENTLESS_CAP_SPIRITHEALER },
{ "spiritguide", CLIENTLESS_CAP_SPIRITGUIDE },
{ "critter", CLIENTLESS_CAP_CRITTER }
}};
bool TryCapability(std::string const& name, uint64& result)
{
for (NamedCapability const& capability : CAPABILITIES)
{
if (name == capability.Name)
{
result = capability.Flag;
return true;
}
}
return false;
}
bool TryRelation(std::string const& name, ClientlessEntityRelation& result)
{
static constexpr std::array<std::pair<char const*, ClientlessEntityRelation>, 3> relations =
{{
{ "friendly", ClientlessEntityRelation::Friendly },
{ "neutral", ClientlessEntityRelation::Neutral },
{ "hostile", ClientlessEntityRelation::Hostile }
}};
for (auto const& [text, relation] : relations)
{
if (name == text)
{
result = relation;
return true;
}
}
return false;
}
bool ParseBool(std::string const& value)
{
return value == "1" || value == "true" || value == "yes";
}
float ParseFloat(std::string const& value, float fallback)
{
if (value.empty())
return fallback;
char* end = nullptr;
float parsed = std::strtof(value.c_str(), &end);
return end && end != value.c_str() ? parsed : fallback;
}
}
ClientlessActionExecutorMgr* ClientlessActionExecutorMgr::instance()
{
static ClientlessActionExecutorMgr instance;
return &instance;
}
char const* ClientlessActionExecutorMgr::StateName(ClientlessActionExecutionState state)
{
switch (state)
{
case ClientlessActionExecutionState::Idle: return "Idle";
case ClientlessActionExecutionState::SelectingTarget: return "SelectingTarget";
case ClientlessActionExecutionState::Moving: return "Moving";
case ClientlessActionExecutionState::Completed: return "Completed";
case ClientlessActionExecutionState::Failed: return "Failed";
}
return "Unknown";
}
ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() ? &itr->second.Status : nullptr;
}
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
{
record.Status = {};
record.Status.ActionName = action.Name;
record.Status.State = ClientlessActionExecutionState::SelectingTarget;
if (action.Name == "approach")
return StartApproach(player, action, record);
if (action.Name == "move_to_point")
return StartMoveToPoint(player, action, record);
if (action.Name == "travel_to_node")
return StartTravelToNode(player, action, record);
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "unsupported action";
record.Active = false;
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] action-failed guid=%u name='%s' action='%s' reason='unsupported action'",
player->GetGUID().GetCounter(), player->GetName().c_str(), action.Name.c_str());
return false;
}
bool ClientlessActionExecutorMgr::StartApproach(
Player* player,
ClientlessAction const& action,
Record& record)
{
uint32 const characterGuid = player->GetGUID().GetCounter();
ClientlessPerceptionQuery query;
query.FilterType = true;
query.Type = ClientlessEntityType::Creature;
query.RequireAlive = true;
float approachDistance = 2.0f;
auto find = [&](char const* key) -> std::string const*
{
auto itr = action.Parameters.find(key);
return itr != action.Parameters.end() ? &itr->second : nullptr;
};
if (std::string const* type = find("type"))
{
if (*type == "player")
query.Type = ClientlessEntityType::Player;
else if (*type == "creature")
query.Type = ClientlessEntityType::Creature;
else
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "invalid type";
record.Active = false;
return false;
}
}
if (std::string const* capability = find("capability"))
{
uint64 flag = 0;
if (!TryCapability(*capability, flag))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "unknown capability: " + *capability;
record.Active = false;
return false;
}
query.RequiredCapabilities |= flag;
}
if (std::string const* relation = find("relation"))
{
ClientlessEntityRelation parsed = ClientlessEntityRelation::Unknown;
if (!TryRelation(*relation, parsed))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "invalid relation: " + *relation;
record.Active = false;
return false;
}
query.FilterRelation = true;
query.Relation = parsed;
}
if (std::string const* attackable = find("attackable"))
query.RequireAttackable = ParseBool(*attackable);
if (std::string const* los = find("line_of_sight"))
query.RequireLineOfSight = ParseBool(*los);
if (std::string const* maxDistance = find("max_distance"))
query.MaxDistance = ParseFloat(*maxDistance, 0.0f);
if (std::string const* distance = find("approach_distance"))
approachDistance = ParseFloat(*distance, 2.0f);
float const scanRadius = query.MaxDistance > 0.0f ? query.MaxDistance : 40.0f;
sClientlessPerceptionMgr->ScanNow(player, scanRadius);
ClientlessPerceivedEntity const* target =
sClientlessPerceptionMgr->FindNearest(characterGuid, query);
if (!target)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "no matching entity";
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] approach-failed guid=%u name='%s' reason='no matching entity'",
characterGuid, player->GetName().c_str());
return false;
}
record.Status.TargetName = target->Name;
record.Status.TargetGuid = target->Guid;
record.Status.TargetEntry = target->Entry;
if (target->MapId != player->GetMapId())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "target is on another map";
record.Active = false;
return false;
}
float targetX = target->PositionX;
float targetY = target->PositionY;
float targetZ = target->PositionZ;
float const dx = targetX - player->GetPositionX();
float const dy = targetY - player->GetPositionY();
float const planarDistance = std::sqrt(dx * dx + dy * dy);
if (planarDistance <= approachDistance)
{
record.Status.State = ClientlessActionExecutionState::Completed;
record.Status.Detail = "already in approach range";
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] approach-complete guid=%u name='%s' target='%s' targetGuid=%u entry=%u reason='already near'",
characterGuid, player->GetName().c_str(), target->Name.c_str(), target->Guid, target->Entry);
return true;
}
if (planarDistance > 0.001f && approachDistance > 0.0f)
{
float const scale = (planarDistance - approachDistance) / planarDistance;
targetX = player->GetPositionX() + dx * scale;
targetY = player->GetPositionY() + dy * scale;
}
Position destination;
destination.Relocate(targetX, targetY, targetZ, target->Orientation);
if (!sClientlessTravelMgr->StartTravel(player, destination))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "travel start failed";
record.Active = false;
return false;
}
record.Status.State = ClientlessActionExecutionState::Moving;
record.Status.Detail = "travelling to selected entity";
record.Active = true;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] approach-start guid=%u name='%s' target='%s' targetGuid=%u entry=%u distance=%.2f caps='%s'",
characterGuid, player->GetName().c_str(), target->Name.c_str(), target->Guid,
target->Entry, target->Distance,
ClientlessPerceptionMgr::CapabilitiesToString(target->Capabilities).c_str());
return true;
}
bool ClientlessActionExecutorMgr::StartMoveToPoint(
Player* player,
ClientlessAction const& action,
Record& record)
{
uint32 const characterGuid = player->GetGUID().GetCounter();
auto find = [&](char const* key) -> std::string const*
{
auto itr = action.Parameters.find(key);
return itr != action.Parameters.end() ? &itr->second : nullptr;
};
auto parseFloat = [](std::string const& value, float fallback) -> float
{
if (value.empty())
return fallback;
char* end = nullptr;
float parsed = std::strtof(value.c_str(), &end);
return end && end != value.c_str() ? parsed : fallback;
};
Position destination;
std::string const* xText = find("x");
std::string const* yText = find("y");
std::string const* zText = find("z");
if (xText && yText && zText)
{
uint32 mapId = player->GetMapId();
if (std::string const* mapText = find("map"))
mapId = uint32(std::strtoul(mapText->c_str(), nullptr, 10));
if (mapId != player->GetMapId())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "raw point is on another map";
record.Active = false;
return false;
}
float const x = parseFloat(*xText, player->GetPositionX());
float const y = parseFloat(*yText, player->GetPositionY());
float const z = parseFloat(*zText, player->GetPositionZ());
float o = player->GetOrientation();
if (std::string const* oText = find("o"))
o = parseFloat(*oText, o);
destination.Relocate(x, y, z, o);
record.Status.TargetName = "raw-world-point";
record.Status.TargetGuid = 0;
record.Status.TargetEntry = 0;
}
else
{
ClientlessWorldPointQuery query;
if (std::string const* id = find("id"))
query.Id = *id;
if (std::string const* category = find("category"))
query.Category = *category;
if (query.Id.empty() && query.Category.empty())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "move_to_point requires id/category or x/y/z";
record.Active = false;
return false;
}
if (std::string const* maxDistance = find("max_distance"))
query.MaxDistance = parseFloat(*maxDistance, 0.0f);
ClientlessWorldPoint const* point =
sClientlessWorldPointMgr->FindNearest(player, query);
if (!point)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "no local matching world point";
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] move-to-point-failed guid=%u name='%s' point='%s' category='%s' reason='no local match'",
characterGuid,
player->GetName().c_str(),
query.Id.c_str(),
query.Category.c_str());
return false;
}
destination.Relocate(point->X, point->Y, point->Z, point->O);
record.Status.TargetName = "point:" + point->Id;
record.Status.TargetGuid = 0;
record.Status.TargetEntry = point->DatabaseId;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] move-to-point-selected guid=%u name='%s' point='%s' category='%s' dbId=%u",
characterGuid,
player->GetName().c_str(),
point->Id.c_str(),
point->Category.c_str(),
point->DatabaseId);
}
if (!sClientlessTravelMgr->StartTravel(player, destination))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "travel start failed";
record.Active = false;
return false;
}
record.Status.State = ClientlessActionExecutionState::Moving;
record.Status.Detail = "travelling to world point";
record.Active = true;
return true;
}
bool ClientlessActionExecutorMgr::StartTravelToNode(
Player* player,
ClientlessAction const& action,
Record& record)
{
uint32 const characterGuid = player->GetGUID().GetCounter();
auto find = [&](char const* key) -> std::string const*
{
auto itr = action.Parameters.find(key);
return itr != action.Parameters.end() ? &itr->second : nullptr;
};
std::string destinationNode;
if (std::string const* node = find("node"))
destinationNode = *node;
else if (std::string const* destination = find("destination"))
destinationNode = *destination;
if (destinationNode.empty())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "travel_to_node requires node";
record.Active = false;
return false;
}
float startRadius = 500.0f;
if (std::string const* radius = find("start_radius"))
{
char* end = nullptr;
float parsed = std::strtof(radius->c_str(), &end);
if (end && end != radius->c_str() && parsed > 0.0f)
startRadius = parsed;
}
record.Route.clear();
record.RouteStep = 0;
record.EdgePath.clear();
std::string error;
if (!sClientlessTravelGraphMgr->PlanRoute(
player, destinationNode, startRadius, record.Route, error))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = error;
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;
}
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()));
record.Active = true;
return StartRouteStep(player, characterGuid, record);
}
bool ClientlessActionExecutorMgr::StartRouteStep(
Player* player,
uint32 characterGuid,
Record& record)
{
if (record.RouteStep >= record.Route.size())
{
record.Status.State = ClientlessActionExecutionState::Completed;
record.Status.Detail = "travel route completed";
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] travel-route-complete guid=%u name='%s'",
characterGuid, player->GetName().c_str());
return true;
}
ClientlessTravelRouteStep const& step = record.Route[record.RouteStep];
if (step.EdgeType != "start" && step.EdgeType != "ground")
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.Detail = "edge type not implemented: " + step.EdgeType;
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] travel-route-stopped guid=%u name='%s' node='%s' edgeType='%s' reason='executor not implemented'",
characterGuid,
player->GetName().c_str(),
step.NodeKey.c_str(),
step.EdgeType.c_str());
return false;
}
if (step.MapId != player->GetMapId())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.Detail = "ground route step is on another map";
record.Active = false;
return false;
}
record.EdgePath.clear();
if (step.EdgeType == "ground" && step.EdgeDatabaseId)
{
if (std::vector<ClientlessTravelEdgePoint> const* points =
sClientlessTravelGraphMgr->GetEdgePoints(step.EdgeDatabaseId))
{
record.EdgePath = *points;
if (step.EdgeReverse)
std::reverse(record.EdgePath.begin(), record.EdgePath.end());
}
}
record.Status.State = ClientlessActionExecutionState::Moving;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.TargetGuid = 0;
record.Status.TargetEntry = 0;
record.Status.Detail =
"route step " + std::to_string(record.RouteStep + 1) +
"/" + std::to_string(record.Route.size()) +
" via " + step.EdgeType;
if (!record.EdgePath.empty())
return StartRecordedEdgePath(player, characterGuid, record);
Position destination;
destination.Relocate(step.X, step.Y, step.Z, step.O);
if (!sClientlessTravelMgr->StartTravel(player, destination))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "travel start failed for route node " + step.NodeKey;
record.Active = false;
return false;
}
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] travel-route-step guid=%u name='%s' step=%u/%u node='%s' edgeType='%s'",
characterGuid,
player->GetName().c_str(),
uint32(record.RouteStep + 1),
uint32(record.Route.size()),
step.NodeKey.c_str(),
step.EdgeType.c_str());
return true;
}
bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
Player* player,
uint32 characterGuid,
Record& record)
{
if (!player || record.EdgePath.empty())
return false;
for (ClientlessTravelEdgePoint const& point : record.EdgePath)
{
if (point.MapId != player->GetMapId())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "recorded ground edge contains a point on another map";
record.Active = false;
return false;
}
}
std::vector<Position> path;
path.reserve(record.EdgePath.size() + 1);
Position current;
current.Relocate(
player->GetPositionX(),
player->GetPositionY(),
player->GetPositionZ(),
player->GetOrientation());
path.push_back(current);
float lastX = current.GetPositionX();
float lastY = current.GetPositionY();
float lastZ = current.GetPositionZ();
for (ClientlessTravelEdgePoint const& point : record.EdgePath)
{
float dx = point.X - lastX;
float dy = point.Y - lastY;
float dz = point.Z - lastZ;
if ((dx * dx + dy * dy + dz * dz) < 0.25f)
continue;
Position p;
p.Relocate(point.X, point.Y, point.Z, point.O);
path.push_back(p);
lastX = point.X;
lastY = point.Y;
lastZ = point.Z;
}
if (path.size() < 2)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "recorded ground edge has no usable movement path";
record.Active = false;
return false;
}
player->GetMotionMaster()->MoveSmoothPath(
1,
path.data(),
path.size(),
false);
record.Status.State = ClientlessActionExecutionState::Moving;
record.Status.Detail =
"recorded ground spline: " +
std::to_string(record.EdgePath.size()) +
" route point(s)";
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] recorded-ground-spline guid=%u name='%s' "
"routeStep=%u/%u points=%u",
characterGuid,
player->GetName().c_str(),
uint32(record.RouteStep + 1),
uint32(record.Route.size()),
uint32(record.EdgePath.size()));
return true;
}
void ClientlessActionExecutorMgr::UpdateTravelToNode(
Player* player,
uint32 characterGuid,
Record& record)
{
if (!record.EdgePath.empty())
{
ClientlessTravelEdgePoint const& destination = record.EdgePath.back();
if (!player || player->GetMapId() != destination.MapId)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "recorded ground spline changed/lost map";
record.Active = false;
return;
}
if (!player->IsStopped())
return;
float dx = player->GetPositionX() - destination.X;
float dy = player->GetPositionY() - destination.Y;
float dz = player->GetPositionZ() - destination.Z;
float remaining = std::sqrt(dx * dx + dy * dy + dz * dz);
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;
return;
}
record.EdgePath.clear();
++record.RouteStep;
StartRouteStep(player, characterGuid, record);
return;
}
ClientlessTravelStatus const* travel =
sClientlessTravelMgr->GetStatus(characterGuid);
if (!travel)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "route travel state disappeared";
record.Active = false;
return;
}
if (travel->State == ClientlessTravelState::Moving)
return;
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;
return;
}
++record.RouteStep;
StartRouteStep(player, characterGuid, record);
}
void ClientlessActionExecutorMgr::UpdateActive(
Player* player,
uint32 characterGuid,
Record& record)
{
if (record.Status.ActionName == "travel_to_node")
{
UpdateTravelToNode(player, characterGuid, record);
return;
}
ClientlessTravelStatus const* travel = sClientlessTravelMgr->GetStatus(characterGuid);
if (!travel)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "travel state disappeared";
record.Active = false;
return;
}
if (travel->State == ClientlessTravelState::Moving)
return;
if (travel->State == ClientlessTravelState::Arrived)
{
record.Status.State = ClientlessActionExecutionState::Completed;
record.Status.Detail = "arrived";
record.Active = false;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] approach-complete guid=%u name='%s' target='%s' targetGuid=%u entry=%u",
characterGuid, player->GetName().c_str(), record.Status.TargetName.c_str(),
record.Status.TargetGuid, record.Status.TargetEntry);
return;
}
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail =
std::string("travel ended in state ") + ClientlessTravelMgr::StateName(travel->State);
record.Active = false;
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] approach-failed guid=%u name='%s' target='%s' travelState=%s",
characterGuid, player->GetName().c_str(), record.Status.TargetName.c_str(),
ClientlessTravelMgr::StateName(travel->State));
}
void ClientlessActionExecutorMgr::Update(uint32 /*diff*/)
{
for (auto const& [characterGuid, session] : sWorld->GetAllClientlessSessions())
{
if (!session || !sClientlessPlayerMgr->IsManaged(characterGuid))
continue;
Player* player = session->GetPlayer();
if (!player || !player->IsInWorld())
continue;
Record& record = _records[characterGuid];
if (record.Active)
{
UpdateActive(player, characterGuid, record);
continue;
}
ClientlessAction action;
if (!sClientlessPlayerMgr->PopNextAction(characterGuid, action))
continue;
StartAction(player, action, record);
}
}
@@ -0,0 +1,71 @@
/*
* Vcore clientless semantic action executor.
* C8.4: Lua action -> perception query -> entity selection -> travel.
*/
#ifndef VCORE_CLIENTLESS_ACTION_EXECUTOR_MGR_H
#define VCORE_CLIENTLESS_ACTION_EXECUTOR_MGR_H
#include "Common.h"
#include "ClientlessTravelGraphMgr.h"
#include <string>
#include <unordered_map>
#include <vector>
class Player;
struct ClientlessAction;
enum class ClientlessActionExecutionState : uint8
{
Idle,
SelectingTarget,
Moving,
Completed,
Failed
};
struct TC_GAME_API ClientlessActionExecutionStatus
{
ClientlessActionExecutionState State = ClientlessActionExecutionState::Idle;
std::string ActionName;
std::string TargetName;
uint32 TargetGuid = 0;
uint32 TargetEntry = 0;
std::string Detail;
};
class TC_GAME_API ClientlessActionExecutorMgr
{
public:
static ClientlessActionExecutorMgr* instance();
void Update(uint32 diff);
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
static char const* StateName(ClientlessActionExecutionState state);
private:
struct Record
{
ClientlessActionExecutionStatus Status;
bool Active = false;
std::vector<ClientlessTravelRouteStep> Route;
std::size_t RouteStep = 0;
std::vector<ClientlessTravelEdgePoint> EdgePath;
};
bool StartAction(Player* player, ClientlessAction const& action, Record& record);
bool StartApproach(Player* player, ClientlessAction const& action, Record& record);
bool StartMoveToPoint(Player* player, ClientlessAction const& action, Record& record);
bool StartTravelToNode(Player* player, ClientlessAction const& action, Record& record);
bool StartRouteStep(Player* player, uint32 characterGuid, Record& record);
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
void UpdateTravelToNode(Player* player, uint32 characterGuid, Record& record);
void UpdateActive(Player* player, uint32 characterGuid, Record& record);
std::unordered_map<uint32, Record> _records;
};
#define sClientlessActionExecutorMgr ClientlessActionExecutorMgr::instance()
#endif
@@ -0,0 +1,301 @@
#include "ClientlessPerceptionMgr.h"
#include "CellImpl.h"
#include "ClientlessPlayerMgr.h"
#include "Creature.h"
#include "GridNotifiersImpl.h"
#include "Player.h"
#include "SharedDefines.h"
#include "World.h"
#include "WorldSession.h"
#include <algorithm>
#include <list>
#include <sstream>
namespace
{
constexpr float DEFAULT_SCAN_RADIUS = 40.0f;
constexpr uint32 SCAN_INTERVAL_MS = 1000;
ClientlessEntityRelation RelationFromReaction(ReputationRank reaction)
{
if (reaction <= REP_HOSTILE)
return ClientlessEntityRelation::Hostile;
if (reaction >= REP_FRIENDLY)
return ClientlessEntityRelation::Friendly;
return ClientlessEntityRelation::Neutral;
}
}
ClientlessPerceptionMgr* ClientlessPerceptionMgr::instance()
{
static ClientlessPerceptionMgr instance;
return &instance;
}
char const* ClientlessPerceptionMgr::TypeName(ClientlessEntityType type)
{
switch (type)
{
case ClientlessEntityType::Creature: return "Creature";
case ClientlessEntityType::Player: return "Player";
}
return "Unknown";
}
char const* ClientlessPerceptionMgr::RelationName(ClientlessEntityRelation relation)
{
switch (relation)
{
case ClientlessEntityRelation::Unknown: return "unknown";
case ClientlessEntityRelation::Friendly: return "friendly";
case ClientlessEntityRelation::Neutral: return "neutral";
case ClientlessEntityRelation::Hostile: return "hostile";
}
return "unknown";
}
std::string ClientlessPerceptionMgr::CapabilitiesToString(uint64 capabilities)
{
std::ostringstream out;
bool first = true;
auto add = [&](uint64 flag, char const* name)
{
if ((capabilities & flag) == 0)
return;
if (!first)
out << ',';
out << name;
first = false;
};
add(CLIENTLESS_CAP_VENDOR, "vendor");
add(CLIENTLESS_CAP_TRAINER, "trainer");
add(CLIENTLESS_CAP_FLIGHTMASTER, "flightmaster");
add(CLIENTLESS_CAP_BANKER, "banker");
add(CLIENTLESS_CAP_INNKEEPER, "innkeeper");
add(CLIENTLESS_CAP_AUCTIONEER, "auctioneer");
add(CLIENTLESS_CAP_QUESTGIVER, "questgiver");
add(CLIENTLESS_CAP_GOSSIP, "gossip");
add(CLIENTLESS_CAP_REPAIR, "repair");
add(CLIENTLESS_CAP_BATTLEMASTER, "battlemaster");
add(CLIENTLESS_CAP_PETITIONER, "petitioner");
add(CLIENTLESS_CAP_SPIRITHEALER, "spirithealer");
add(CLIENTLESS_CAP_SPIRITGUIDE, "spiritguide");
add(CLIENTLESS_CAP_CRITTER, "critter");
add(CLIENTLESS_CAP_CLIENTLESS, "clientless");
return first ? "-" : out.str();
}
ClientlessEntityRelation ClientlessPerceptionMgr::DetermineRelation(Player* observer, Player* target)
{
if (!observer || !target)
return ClientlessEntityRelation::Unknown;
return RelationFromReaction(observer->GetReactionTo(target));
}
ClientlessEntityRelation ClientlessPerceptionMgr::DetermineRelation(Player* observer, Creature* target)
{
if (!observer || !target)
return ClientlessEntityRelation::Unknown;
return RelationFromReaction(observer->GetReactionTo(target));
}
bool ClientlessPerceptionMgr::ScanNow(Player* player, float radius)
{
if (!player || !player->IsInWorld())
return false;
radius = std::max(1.0f, std::min(radius, 100.0f));
uint32 const characterGuid = player->GetGUID().GetCounter();
std::list<WorldObject*> objects;
Trinity::AllWorldObjectsInRange check(player, radius);
Trinity::WorldObjectListSearcher<Trinity::AllWorldObjectsInRange> searcher(player, objects, check);
Cell::VisitAllObjects(player, searcher, radius);
ClientlessPerceptionSnapshot snapshot;
snapshot.Radius = radius;
snapshot.ScanAgeMs = 0;
for (WorldObject* object : objects)
{
if (!object)
continue;
if (Creature* creature = object->ToCreature())
{
ClientlessPerceivedEntity entity;
entity.Type = ClientlessEntityType::Creature;
entity.Guid = creature->GetGUID().GetCounter();
entity.Entry = creature->GetEntry();
entity.Name = creature->GetName();
entity.Distance = player->GetDistance(creature);
entity.MapId = creature->GetMapId();
entity.PositionX = creature->GetPositionX();
entity.PositionY = creature->GetPositionY();
entity.PositionZ = creature->GetPositionZ();
entity.Orientation = creature->GetOrientation();
entity.Level = creature->getLevel();
entity.Health = creature->GetHealth();
entity.MaxHealth = creature->GetMaxHealth();
entity.Alive = creature->IsAlive();
entity.InLineOfSight = player->IsWithinLOSInMap(creature);
entity.Attackable = player->IsValidAttackTarget(creature);
entity.Relation = DetermineRelation(player, creature);
uint64 capabilities = CLIENTLESS_CAP_NONE;
if (creature->IsVendor()) capabilities |= CLIENTLESS_CAP_VENDOR;
if (creature->IsTrainer()) capabilities |= CLIENTLESS_CAP_TRAINER;
if (creature->IsTaxi()) capabilities |= CLIENTLESS_CAP_FLIGHTMASTER;
if (creature->IsBanker()) capabilities |= CLIENTLESS_CAP_BANKER;
if (creature->IsInnkeeper()) capabilities |= CLIENTLESS_CAP_INNKEEPER;
if (creature->IsAuctioner()) capabilities |= CLIENTLESS_CAP_AUCTIONEER;
if (creature->IsQuestGiver()) capabilities |= CLIENTLESS_CAP_QUESTGIVER;
if (creature->IsGossip()) capabilities |= CLIENTLESS_CAP_GOSSIP;
if (creature->IsArmorer()) capabilities |= CLIENTLESS_CAP_REPAIR;
if (creature->IsBattleMaster()) capabilities |= CLIENTLESS_CAP_BATTLEMASTER;
if (creature->IsGuildMaster()) capabilities |= CLIENTLESS_CAP_PETITIONER;
if (creature->IsSpiritHealer()) capabilities |= CLIENTLESS_CAP_SPIRITHEALER;
if (creature->IsSpiritGuide()) capabilities |= CLIENTLESS_CAP_SPIRITGUIDE;
if (creature->IsCritter()) capabilities |= CLIENTLESS_CAP_CRITTER;
entity.Capabilities = capabilities;
snapshot.Entities.push_back(std::move(entity));
continue;
}
if (Player* otherPlayer = object->ToPlayer())
{
if (otherPlayer == player)
continue;
ClientlessPerceivedEntity entity;
entity.Type = ClientlessEntityType::Player;
entity.Guid = otherPlayer->GetGUID().GetCounter();
entity.Name = otherPlayer->GetName();
entity.Distance = player->GetDistance(otherPlayer);
entity.MapId = otherPlayer->GetMapId();
entity.PositionX = otherPlayer->GetPositionX();
entity.PositionY = otherPlayer->GetPositionY();
entity.PositionZ = otherPlayer->GetPositionZ();
entity.Orientation = otherPlayer->GetOrientation();
entity.Level = otherPlayer->getLevel();
entity.Health = otherPlayer->GetHealth();
entity.MaxHealth = otherPlayer->GetMaxHealth();
entity.Alive = otherPlayer->IsAlive();
entity.InLineOfSight = player->IsWithinLOSInMap(otherPlayer);
entity.Attackable = player->IsValidAttackTarget(otherPlayer);
entity.Relation = DetermineRelation(player, otherPlayer);
if (sClientlessPlayerMgr->IsManaged(entity.Guid))
entity.Capabilities |= CLIENTLESS_CAP_CLIENTLESS;
snapshot.Entities.push_back(std::move(entity));
}
}
std::sort(snapshot.Entities.begin(), snapshot.Entities.end(),
[](ClientlessPerceivedEntity const& a, ClientlessPerceivedEntity const& b)
{
return a.Distance < b.Distance;
});
Record& record = _records[characterGuid];
record.Snapshot = std::move(snapshot);
record.ScanTimerMs = SCAN_INTERVAL_MS;
return true;
}
ClientlessPerceptionSnapshot const* ClientlessPerceptionMgr::GetSnapshot(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() ? &itr->second.Snapshot : nullptr;
}
bool ClientlessPerceptionMgr::Matches(
ClientlessPerceivedEntity const& entity,
ClientlessPerceptionQuery const& query)
{
if (query.FilterType && entity.Type != query.Type)
return false;
if (query.FilterRelation && entity.Relation != query.Relation)
return false;
if (query.RequiredCapabilities &&
(entity.Capabilities & query.RequiredCapabilities) != query.RequiredCapabilities)
return false;
if (query.ForbiddenCapabilities &&
(entity.Capabilities & query.ForbiddenCapabilities) != 0)
return false;
if (query.RequireAlive && !entity.Alive)
return false;
if (query.RequireAttackable && !entity.Attackable)
return false;
if (query.RequireLineOfSight && !entity.InLineOfSight)
return false;
if (query.MaxDistance > 0.0f && entity.Distance > query.MaxDistance)
return false;
return true;
}
ClientlessPerceivedEntity const* ClientlessPerceptionMgr::FindNearest(
uint32 characterGuid,
ClientlessPerceptionQuery const& query) const
{
ClientlessPerceptionSnapshot const* snapshot = GetSnapshot(characterGuid);
if (!snapshot)
return nullptr;
for (ClientlessPerceivedEntity const& entity : snapshot->Entities)
if (Matches(entity, query))
return &entity;
return nullptr;
}
void ClientlessPerceptionMgr::Update(uint32 diff)
{
for (auto const& [characterGuid, session] : sWorld->GetAllClientlessSessions())
{
if (!session || !sClientlessPlayerMgr->IsManaged(characterGuid))
continue;
Player* player = session->GetPlayer();
if (!player || !player->IsInWorld())
continue;
Record& record = _records[characterGuid];
record.Snapshot.ScanAgeMs += diff;
if (record.ScanTimerMs > diff)
{
record.ScanTimerMs -= diff;
continue;
}
ScanNow(
player,
record.Snapshot.Radius > 0.0f
? record.Snapshot.Radius
: DEFAULT_SCAN_RADIUS);
}
}
@@ -0,0 +1,147 @@
/*
* Vcore AdventureBot local perception.
*
* C8.3 final model:
* - generic perceived entities (Creature / Player)
* - data-driven capability bitmask derived from existing Core/DB-backed flags
* - explicit relation vs. attackability
* - generic FindNearest(query) API
*/
#ifndef VCORE_CLIENTLESS_PERCEPTION_MGR_H
#define VCORE_CLIENTLESS_PERCEPTION_MGR_H
#include "Common.h"
#include <string>
#include <unordered_map>
#include <vector>
class Player;
class Creature;
enum class ClientlessEntityType : uint8
{
Creature,
Player
};
enum class ClientlessEntityRelation : uint8
{
Unknown,
Friendly,
Neutral,
Hostile
};
enum ClientlessEntityCapability : uint64
{
CLIENTLESS_CAP_NONE = 0,
CLIENTLESS_CAP_VENDOR = 1ULL << 0,
CLIENTLESS_CAP_TRAINER = 1ULL << 1,
CLIENTLESS_CAP_FLIGHTMASTER = 1ULL << 2,
CLIENTLESS_CAP_BANKER = 1ULL << 3,
CLIENTLESS_CAP_INNKEEPER = 1ULL << 4,
CLIENTLESS_CAP_AUCTIONEER = 1ULL << 5,
CLIENTLESS_CAP_QUESTGIVER = 1ULL << 6,
CLIENTLESS_CAP_GOSSIP = 1ULL << 7,
CLIENTLESS_CAP_REPAIR = 1ULL << 8,
CLIENTLESS_CAP_BATTLEMASTER = 1ULL << 9,
CLIENTLESS_CAP_PETITIONER = 1ULL << 10,
CLIENTLESS_CAP_SPIRITHEALER = 1ULL << 11,
CLIENTLESS_CAP_SPIRITGUIDE = 1ULL << 12,
CLIENTLESS_CAP_CRITTER = 1ULL << 13,
CLIENTLESS_CAP_CLIENTLESS = 1ULL << 14
};
struct TC_GAME_API ClientlessPerceivedEntity
{
ClientlessEntityType Type = ClientlessEntityType::Creature;
uint32 Guid = 0;
uint32 Entry = 0;
std::string Name;
float Distance = 0.0f;
uint32 MapId = 0;
float PositionX = 0.0f;
float PositionY = 0.0f;
float PositionZ = 0.0f;
float Orientation = 0.0f;
uint8 Level = 0;
uint32 Health = 0;
uint32 MaxHealth = 0;
bool Alive = false;
bool InLineOfSight = false;
bool Attackable = false;
ClientlessEntityRelation Relation = ClientlessEntityRelation::Unknown;
uint64 Capabilities = CLIENTLESS_CAP_NONE;
bool HasCapability(uint64 capability) const
{
return (Capabilities & capability) == capability;
}
};
struct TC_GAME_API ClientlessPerceptionSnapshot
{
float Radius = 0.0f;
uint32 ScanAgeMs = 0;
std::vector<ClientlessPerceivedEntity> Entities;
};
struct TC_GAME_API ClientlessPerceptionQuery
{
bool FilterType = false;
ClientlessEntityType Type = ClientlessEntityType::Creature;
bool FilterRelation = false;
ClientlessEntityRelation Relation = ClientlessEntityRelation::Unknown;
uint64 RequiredCapabilities = CLIENTLESS_CAP_NONE;
uint64 ForbiddenCapabilities = CLIENTLESS_CAP_NONE;
bool RequireAlive = false;
bool RequireAttackable = false;
bool RequireLineOfSight = false;
// 0 = use the whole current snapshot.
float MaxDistance = 0.0f;
};
class TC_GAME_API ClientlessPerceptionMgr
{
public:
static ClientlessPerceptionMgr* instance();
void Update(uint32 diff);
bool ScanNow(Player* player, float radius = 40.0f);
ClientlessPerceptionSnapshot const* GetSnapshot(uint32 characterGuid) const;
ClientlessPerceivedEntity const* FindNearest(
uint32 characterGuid,
ClientlessPerceptionQuery const& query) const;
static char const* TypeName(ClientlessEntityType type);
static char const* RelationName(ClientlessEntityRelation relation);
static std::string CapabilitiesToString(uint64 capabilities);
private:
struct Record
{
ClientlessPerceptionSnapshot Snapshot;
uint32 ScanTimerMs = 0;
};
static ClientlessEntityRelation DetermineRelation(Player* observer, Player* target);
static ClientlessEntityRelation DetermineRelation(Player* observer, Creature* target);
static bool Matches(ClientlessPerceivedEntity const& entity, ClientlessPerceptionQuery const& query);
std::unordered_map<uint32, Record> _records;
};
#define sClientlessPerceptionMgr ClientlessPerceptionMgr::instance()
#endif
@@ -1,4 +1,7 @@
#include "ClientlessPlayerMgr.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessPerceptionMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "Log.h"
#include "Player.h"
#include "World.h"
@@ -139,6 +142,13 @@ std::deque<ClientlessAction> const* ClientlessPlayerMgr::GetQueuedActions(uint32
void ClientlessPlayerMgr::Update(uint32 diff)
{
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
sClientlessTravelMgr->Update(diff);
// CLIENTLESS_PLAYER_CORE_C8_3_LOCAL_PERCEPTION
sClientlessPerceptionMgr->Update(diff);
// CLIENTLESS_PLAYER_CORE_C8_4_ACTION_EXECUTOR
sClientlessActionExecutorMgr->Update(diff);
for (auto itr = _records.begin(); itr != _records.end(); )
{
uint32 characterGuid = itr->first;
@@ -0,0 +1,698 @@
#include "ClientlessRouteRecorderMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "DatabaseEnv.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include <algorithm>
#include <cmath>
ClientlessRouteRecorderMgr* ClientlessRouteRecorderMgr::instance()
{
static ClientlessRouteRecorderMgr instance;
return &instance;
}
bool ClientlessRouteRecorderMgr::IsSafeKey(std::string const& value)
{
if (value.empty() || value.size() > 100)
return false;
for (char c : value)
if (!(c >= 'a' && c <= 'z') &&
!(c >= 'A' && c <= 'Z') &&
!(c >= '0' && c <= '9') &&
c != '_' && c != '-' && c != '.')
return false;
return true;
}
char const* ClientlessRouteRecorderMgr::StateName(ClientlessRouteRecorderState state)
{
switch (state)
{
case ClientlessRouteRecorderState::Inactive: return "inactive";
case ClientlessRouteRecorderState::Recording: return "recording";
case ClientlessRouteRecorderState::Paused: return "paused";
}
return "unknown";
}
bool ClientlessRouteRecorderMgr::LoadNode(std::string const& key, NodeLocation& node) const
{
QueryResult result = WorldDatabase.PQuery(
"SELECT w.map, w.position_x, w.position_y, w.position_z, w.orientation "
"FROM vcore_clientless_travel_node n "
"INNER JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key "
"WHERE n.node_key='%s' AND n.enabled=1 AND w.enabled=1 LIMIT 1",
key.c_str());
if (!result)
return false;
Field* f = result->Fetch();
node.MapId = f[0].GetUInt16();
node.X = f[1].GetFloat();
node.Y = f[2].GetFloat();
node.Z = f[3].GetFloat();
node.O = f[4].GetFloat();
return true;
}
float ClientlessRouteRecorderMgr::Distance(
ClientlessRecordedRoutePoint const& a,
ClientlessRecordedRoutePoint const& b) const
{
float dx = a.X - b.X;
float dy = a.Y - b.Y;
float dz = a.Z - b.Z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
float ClientlessRouteRecorderMgr::Distance(Player const* player, NodeLocation const& node) const
{
float dx = player->GetPositionX() - node.X;
float dy = player->GetPositionY() - node.Y;
float dz = player->GetPositionZ() - node.Z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
void ClientlessRouteRecorderMgr::RecalculateLength()
{
_length = 0.0f;
for (std::size_t i = 1; i < _points.size(); ++i)
_length += Distance(_points[i - 1], _points[i]);
}
bool ClientlessRouteRecorderMgr::AppendCurrent(Player* player, bool force)
{
if (!player || !player->IsInWorld())
return false;
ClientlessRecordedRoutePoint point;
point.MapId = player->GetMapId();
point.X = player->GetPositionX();
point.Y = player->GetPositionY();
point.Z = player->GetPositionZ();
point.O = player->GetOrientation();
if (!_points.empty())
{
if (_points.back().MapId != point.MapId)
return false;
float distance = Distance(_points.back(), point);
if (!force && distance < RECORD_INTERVAL_YARDS)
return false;
_length += distance;
}
_points.push_back(point);
return true;
}
bool ClientlessRouteRecorderMgr::AddNode(
Player* player, std::string const& key, std::string const& type, std::string& error)
{
if (!player || !player->IsInWorld())
{
error = "player is not in world";
return false;
}
if (!IsSafeKey(key) || !IsSafeKey(type))
{
error = "key/type may contain only A-Z a-z 0-9 _ - .";
return false;
}
if (WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
key.c_str()))
{
error = "node already exists; use node move to change its position";
return false;
}
if (WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_world_point WHERE point_key='%s' LIMIT 1",
key.c_str()))
{
error = "world point with the same key already exists";
return false;
}
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_world_point "
"(point_key,category,map,position_x,position_y,position_z,orientation,enabled,comment) "
"VALUES ('%s','travel',%u,%.9f,%.9f,%.9f,%.9f,1,'Travel node recorded in game')",
key.c_str(),
player->GetMapId(),
player->GetPositionX(),
player->GetPositionY(),
player->GetPositionZ(),
player->GetOrientation());
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_travel_node "
"(node_key,node_type,world_point_key,enabled,comment) "
"VALUES ('%s','%s','%s',1,'Recorded in game')",
key.c_str(), type.c_str(), key.c_str());
sClientlessTravelGraphMgr->Reload();
return true;
}
bool ClientlessRouteRecorderMgr::MoveNode(
Player* player, std::string const& key, std::string& error)
{
if (!player || !player->IsInWorld())
{
error = "player is not in world";
return false;
}
if (!IsSafeKey(key))
{
error = "invalid node key";
return false;
}
QueryResult result = WorldDatabase.PQuery(
"SELECT world_point_key FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
key.c_str());
if (!result)
{
error = "unknown node";
return false;
}
std::string worldPointKey = result->Fetch()[0].GetString();
if (!IsSafeKey(worldPointKey))
{
error = "node references unsupported world point key";
return false;
}
WorldDatabase.DirectPExecute(
"UPDATE vcore_clientless_world_point "
"SET map=%u,position_x=%.9f,position_y=%.9f,position_z=%.9f,orientation=%.9f "
"WHERE point_key='%s'",
player->GetMapId(),
player->GetPositionX(),
player->GetPositionY(),
player->GetPositionZ(),
player->GetOrientation(),
worldPointKey.c_str());
sClientlessTravelGraphMgr->Reload();
return true;
}
bool ClientlessRouteRecorderMgr::RemoveNode(std::string const& key, std::string& error)
{
if (!IsSafeKey(key))
{
error = "invalid node key";
return false;
}
QueryResult nodeResult = WorldDatabase.PQuery(
"SELECT world_point_key FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
key.c_str());
if (!nodeResult)
{
error = "unknown node";
return false;
}
QueryResult edgeResult = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_edge "
"WHERE from_node_key='%s' OR to_node_key='%s' LIMIT 1",
key.c_str(), key.c_str());
if (edgeResult)
{
error = "node still has travel edges; remove/rework those first";
return false;
}
std::string worldPointKey = nodeResult->Fetch()[0].GetString();
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_node WHERE node_key='%s'",
key.c_str());
if (IsSafeKey(worldPointKey))
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_world_point WHERE point_key='%s'",
worldPointKey.c_str());
sClientlessTravelGraphMgr->Reload();
return true;
}
bool ClientlessRouteRecorderMgr::ForceRemoveNode(
std::string const& key, uint32& removedEdges, std::string& error)
{
removedEdges = 0;
if (!IsSafeKey(key))
{
error = "invalid node key";
return false;
}
QueryResult nodeResult = WorldDatabase.PQuery(
"SELECT world_point_key FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
key.c_str());
if (!nodeResult)
{
error = "unknown node";
return false;
}
std::string worldPointKey = nodeResult->Fetch()[0].GetString();
if (QueryResult countResult = WorldDatabase.PQuery(
"SELECT COUNT(*) FROM vcore_clientless_travel_edge "
"WHERE from_node_key='%s' OR to_node_key='%s'",
key.c_str(), key.c_str()))
removedEdges = countResult->Fetch()[0].GetUInt32();
WorldDatabase.DirectPExecute(
"DELETE ep FROM vcore_clientless_travel_edge_point ep "
"INNER JOIN vcore_clientless_travel_edge e ON e.id=ep.edge_id "
"WHERE e.from_node_key='%s' OR e.to_node_key='%s'",
key.c_str(), key.c_str());
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_edge "
"WHERE from_node_key='%s' OR to_node_key='%s'",
key.c_str(), key.c_str());
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_node WHERE node_key='%s'",
key.c_str());
if (IsSafeKey(worldPointKey))
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_world_point WHERE point_key='%s'",
worldPointKey.c_str());
sClientlessTravelGraphMgr->Reload();
return true;
}
bool ClientlessRouteRecorderMgr::RemoveRoute(
std::string const& fromNode,
std::string const& toNode,
uint32& removedEdges,
std::string& error)
{
removedEdges = 0;
if (!IsSafeKey(fromNode) || !IsSafeKey(toNode) || fromNode == toNode)
{
error = "invalid route endpoints";
return false;
}
QueryResult edgeResult = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_edge "
"WHERE ((from_node_key='%s' AND to_node_key='%s') OR "
"(from_node_key='%s' AND to_node_key='%s')) "
"ORDER BY id",
fromNode.c_str(), toNode.c_str(), toNode.c_str(), fromNode.c_str());
if (!edgeResult)
{
error = "no route exists between those nodes";
return false;
}
std::vector<uint32> edgeIds;
do
edgeIds.push_back(edgeResult->Fetch()[0].GetUInt32());
while (edgeResult->NextRow());
for (uint32 edgeId : edgeIds)
{
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_edge_point WHERE edge_id=%u",
edgeId);
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_edge WHERE id=%u",
edgeId);
}
removedEdges = uint32(edgeIds.size());
sClientlessTravelGraphMgr->Reload();
return true;
}
bool ClientlessRouteRecorderMgr::Start(
Player* player, std::string const& fromNode, std::string const& toNode, std::string& error)
{
if (_state != ClientlessRouteRecorderState::Inactive)
{
error = "a route recording is already active";
return false;
}
if (!player || !player->IsInWorld())
{
error = "player is not in world";
return false;
}
if (!IsSafeKey(fromNode) || !IsSafeKey(toNode) || fromNode == toNode)
{
error = "invalid route endpoints";
return false;
}
if (!LoadNode(fromNode, _fromLocation))
{
error = "unknown from-node: " + fromNode;
return false;
}
if (!LoadNode(toNode, _toLocation))
{
error = "unknown to-node: " + toNode;
return false;
}
if (_fromLocation.MapId != _toLocation.MapId)
{
error = "ground route endpoints are on different maps";
return false;
}
if (player->GetMapId() != _fromLocation.MapId ||
Distance(player, _fromLocation) > NODE_ENDPOINT_RADIUS_YARDS)
{
error = "stand within 20 yd of the from-node before recording";
return false;
}
_recorderGuid = player->GetGUID();
_fromNode = fromNode;
_toNode = toNode;
_points.clear();
_length = 0.0f;
_sampleTimer = 0;
ClientlessRecordedRoutePoint first;
first.MapId = _fromLocation.MapId;
first.X = _fromLocation.X;
first.Y = _fromLocation.Y;
first.Z = _fromLocation.Z;
first.O = _fromLocation.O;
_points.push_back(first);
_state = ClientlessRouteRecorderState::Recording;
return true;
}
bool ClientlessRouteRecorderMgr::Pause(Player* player, std::string& error)
{
if (_state != ClientlessRouteRecorderState::Recording)
{
error = "route recorder is not recording";
return false;
}
if (!player || player->GetGUID() != _recorderGuid)
{
error = "this player is not the active route recorder";
return false;
}
_state = ClientlessRouteRecorderState::Paused;
return true;
}
bool ClientlessRouteRecorderMgr::Resume(Player* player, std::string& error)
{
if (_state != ClientlessRouteRecorderState::Paused)
{
error = "route recorder is not paused";
return false;
}
if (!player || player->GetGUID() != _recorderGuid)
{
error = "this player is not the active route recorder";
return false;
}
if (player->GetMapId() != _fromLocation.MapId)
{
error = "recorder is on a different map";
return false;
}
AppendCurrent(player, true);
_state = ClientlessRouteRecorderState::Recording;
_sampleTimer = 0;
return true;
}
bool ClientlessRouteRecorderMgr::Cut(
Player* player, uint32& keptPoints, float& nearestDistance, std::string& error)
{
keptPoints = 0;
nearestDistance = 0.0f;
if (_state != ClientlessRouteRecorderState::Paused)
{
error = "pause the route before using cut";
return false;
}
if (!player || player->GetGUID() != _recorderGuid || _points.empty())
{
error = "invalid recorder player/state";
return false;
}
if (player->GetMapId() != _fromLocation.MapId)
{
error = "recorder is on a different map";
return false;
}
std::size_t best = 0;
float bestDistance = 999999.0f;
ClientlessRecordedRoutePoint current;
current.MapId = player->GetMapId();
current.X = player->GetPositionX();
current.Y = player->GetPositionY();
current.Z = player->GetPositionZ();
current.O = player->GetOrientation();
for (std::size_t i = 0; i < _points.size(); ++i)
{
float d = Distance(_points[i], current);
if (d < bestDistance)
{
bestDistance = d;
best = i;
}
}
_points.resize(best + 1);
if (bestDistance > 1.0f)
_points.push_back(current);
RecalculateLength();
keptPoints = uint32(_points.size());
nearestDistance = bestDistance;
return true;
}
bool ClientlessRouteRecorderMgr::Mark(Player* player, std::string& error)
{
if (_state == ClientlessRouteRecorderState::Inactive)
{
error = "no active route recording";
return false;
}
if (!player || player->GetGUID() != _recorderGuid)
{
error = "this player is not the active route recorder";
return false;
}
if (!AppendCurrent(player, true))
{
error = "could not append route point";
return false;
}
return true;
}
bool ClientlessRouteRecorderMgr::Stop(
Player* player, uint32& savedPoints, std::string& error)
{
savedPoints = 0;
if (_state == ClientlessRouteRecorderState::Inactive)
{
error = "no active route recording";
return false;
}
if (!player || player->GetGUID() != _recorderGuid)
{
error = "this player is not the active route recorder";
return false;
}
if (player->GetMapId() != _toLocation.MapId ||
Distance(player, _toLocation) > NODE_ENDPOINT_RADIUS_YARDS)
{
error = "stand within 20 yd of the to-node before stopping";
return false;
}
ClientlessRecordedRoutePoint last;
last.MapId = _toLocation.MapId;
last.X = _toLocation.X;
last.Y = _toLocation.Y;
last.Z = _toLocation.Z;
last.O = _toLocation.O;
if (_points.empty() || Distance(_points.back(), last) > 0.25f)
_points.push_back(last);
RecalculateLength();
QueryResult oldEdge = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_edge "
"WHERE edge_type='ground' AND "
"((from_node_key='%s' AND to_node_key='%s') OR "
"(from_node_key='%s' AND to_node_key='%s')) "
"ORDER BY id LIMIT 1",
_fromNode.c_str(), _toNode.c_str(), _toNode.c_str(), _fromNode.c_str());
if (oldEdge)
{
uint32 oldId = oldEdge->Fetch()[0].GetUInt32();
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_edge_point WHERE edge_id=%u",
oldId);
WorldDatabase.DirectPExecute(
"DELETE FROM vcore_clientless_travel_edge WHERE id=%u",
oldId);
}
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_travel_edge "
"(from_node_key,to_node_key,edge_type,cost,bidirectional,enabled,comment) "
"VALUES ('%s','%s','ground',%.3f,1,1,'Recorded in game')",
_fromNode.c_str(), _toNode.c_str(),
std::max(1.0f, _length));
QueryResult newEdge = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_edge "
"WHERE from_node_key='%s' AND to_node_key='%s' AND edge_type='ground' "
"ORDER BY id DESC LIMIT 1",
_fromNode.c_str(), _toNode.c_str());
if (!newEdge)
{
error = "edge insert succeeded but edge id could not be resolved";
return false;
}
uint32 edgeId = newEdge->Fetch()[0].GetUInt32();
for (uint32 i = 0; i < _points.size(); ++i)
{
ClientlessRecordedRoutePoint const& p = _points[i];
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_travel_edge_point "
"(edge_id,point_index,map,position_x,position_y,position_z,orientation) "
"VALUES (%u,%u,%u,%.9f,%.9f,%.9f,%.9f)",
edgeId, i, p.MapId, p.X, p.Y, p.Z, p.O);
}
savedPoints = uint32(_points.size());
_state = ClientlessRouteRecorderState::Inactive;
_recorderGuid.Clear();
_fromNode.clear();
_toNode.clear();
_points.clear();
_length = 0.0f;
_sampleTimer = 0;
sClientlessTravelGraphMgr->Reload();
return true;
}
void ClientlessRouteRecorderMgr::Cancel()
{
_state = ClientlessRouteRecorderState::Inactive;
_recorderGuid.Clear();
_fromNode.clear();
_toNode.clear();
_points.clear();
_length = 0.0f;
_sampleTimer = 0;
}
ClientlessRouteRecorderStatus ClientlessRouteRecorderMgr::GetStatus() const
{
ClientlessRouteRecorderStatus status;
status.State = _state;
status.FromNode = _fromNode;
status.ToNode = _toNode;
status.PointCount = uint32(_points.size());
status.Length = _length;
return status;
}
void ClientlessRouteRecorderMgr::Update(uint32 diff)
{
if (_state != ClientlessRouteRecorderState::Recording)
return;
if (_sampleTimer > diff)
{
_sampleTimer -= diff;
return;
}
_sampleTimer = SAMPLE_INTERVAL_MS;
Player* player = ObjectAccessor::FindConnectedPlayer(_recorderGuid);
if (!player || !player->IsInWorld())
{
_state = ClientlessRouteRecorderState::Paused;
return;
}
if (player->GetMapId() != _fromLocation.MapId)
{
_state = ClientlessRouteRecorderState::Paused;
return;
}
AppendCurrent(player, false);
}
@@ -0,0 +1,102 @@
/*
* Vcore AdventureBot route recorder.
*
* Nodes are independent graph objects. Ground routes are recorded later
* between two existing nodes and are bidirectional by default.
*/
#ifndef VCORE_CLIENTLESS_ROUTE_RECORDER_MGR_H
#define VCORE_CLIENTLESS_ROUTE_RECORDER_MGR_H
#include "Common.h"
#include "ObjectGuid.h"
#include <string>
#include <vector>
class Player;
struct TC_GAME_API ClientlessRecordedRoutePoint
{
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
enum class ClientlessRouteRecorderState : uint8
{
Inactive,
Recording,
Paused
};
struct TC_GAME_API ClientlessRouteRecorderStatus
{
ClientlessRouteRecorderState State = ClientlessRouteRecorderState::Inactive;
std::string FromNode;
std::string ToNode;
uint32 PointCount = 0;
float Length = 0.0f;
};
class TC_GAME_API ClientlessRouteRecorderMgr
{
public:
static ClientlessRouteRecorderMgr* instance();
void Update(uint32 diff);
bool AddNode(Player* player, std::string const& key, std::string const& type, std::string& error);
bool MoveNode(Player* player, std::string const& key, std::string& error);
bool RemoveNode(std::string const& key, std::string& error);
bool ForceRemoveNode(std::string const& key, uint32& removedEdges, std::string& error);
bool RemoveRoute(std::string const& fromNode, std::string const& toNode, uint32& removedEdges, std::string& error);
bool Start(Player* player, std::string const& fromNode, std::string const& toNode, std::string& error);
bool Pause(Player* player, std::string& error);
bool Resume(Player* player, std::string& error);
bool Cut(Player* player, uint32& keptPoints, float& nearestDistance, std::string& error);
bool Mark(Player* player, std::string& error);
bool Stop(Player* player, uint32& savedPoints, std::string& error);
void Cancel();
ClientlessRouteRecorderStatus GetStatus() const;
static bool IsSafeKey(std::string const& value);
static char const* StateName(ClientlessRouteRecorderState state);
private:
struct NodeLocation
{
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
bool LoadNode(std::string const& key, NodeLocation& node) const;
bool AppendCurrent(Player* player, bool force);
float Distance(ClientlessRecordedRoutePoint const& a, ClientlessRecordedRoutePoint const& b) const;
float Distance(Player const* player, NodeLocation const& node) const;
void RecalculateLength();
ClientlessRouteRecorderState _state = ClientlessRouteRecorderState::Inactive;
ObjectGuid _recorderGuid;
std::string _fromNode;
std::string _toNode;
NodeLocation _fromLocation;
NodeLocation _toLocation;
std::vector<ClientlessRecordedRoutePoint> _points;
float _length = 0.0f;
uint32 _sampleTimer = 0;
static constexpr float RECORD_INTERVAL_YARDS = 6.0f;
static constexpr float NODE_ENDPOINT_RADIUS_YARDS = 20.0f;
static constexpr uint32 SAMPLE_INTERVAL_MS = 250;
};
#define sClientlessRouteRecorderMgr ClientlessRouteRecorderMgr::instance()
#endif
@@ -0,0 +1,337 @@
#include "ClientlessTravelGraphMgr.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "Player.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <queue>
#include <utility>
ClientlessTravelGraphMgr* ClientlessTravelGraphMgr::instance()
{
static ClientlessTravelGraphMgr instance;
return &instance;
}
bool ClientlessTravelGraphMgr::EnsureLoaded()
{
if (_loaded)
return true;
return Reload();
}
bool ClientlessTravelGraphMgr::Reload()
{
_nodes.clear();
_edges.clear();
_edgePoints.clear();
_edgeCount = 0;
QueryResult nodeResult = WorldDatabase.Query(
"SELECT n.id, n.node_key, n.node_type, n.world_point_key, "
"w.map, w.position_x, w.position_y, w.position_z, w.orientation "
"FROM vcore_clientless_travel_node n "
"INNER JOIN vcore_clientless_world_point w "
"ON w.point_key = n.world_point_key "
"WHERE n.enabled = 1 AND w.enabled = 1 "
"ORDER BY n.id");
if (nodeResult)
{
do
{
Field* fields = nodeResult->Fetch();
ClientlessTravelNode node;
node.DatabaseId = fields[0].GetUInt32();
node.Key = fields[1].GetString();
node.Type = fields[2].GetString();
node.WorldPointKey = fields[3].GetString();
node.MapId = fields[4].GetUInt16();
node.X = fields[5].GetFloat();
node.Y = fields[6].GetFloat();
node.Z = fields[7].GetFloat();
node.O = fields[8].GetFloat();
if (!node.Key.empty())
_nodes[node.Key] = std::move(node);
}
while (nodeResult->NextRow());
}
QueryResult edgeResult = WorldDatabase.Query(
"SELECT id, from_node_key, to_node_key, edge_type, cost, bidirectional "
"FROM vcore_clientless_travel_edge "
"WHERE enabled = 1 "
"ORDER BY id");
if (edgeResult)
{
do
{
Field* fields = edgeResult->Fetch();
ClientlessTravelEdge edge;
edge.DatabaseId = fields[0].GetUInt32();
edge.FromNode = fields[1].GetString();
edge.ToNode = fields[2].GetString();
edge.Type = fields[3].GetString();
edge.Cost = fields[4].GetFloat();
bool const bidirectional = fields[5].GetBool();
if (_nodes.find(edge.FromNode) == _nodes.end() ||
_nodes.find(edge.ToNode) == _nodes.end())
{
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] ignoring travel edge id=%u from='%s' to='%s': node missing",
edge.DatabaseId, edge.FromNode.c_str(), edge.ToNode.c_str());
continue;
}
if (edge.Type.empty())
edge.Type = "ground";
if (edge.Cost <= 0.0f)
edge.Cost = 1.0f;
_edges[edge.FromNode].push_back(edge);
++_edgeCount;
if (bidirectional)
{
ClientlessTravelEdge reverse = edge;
reverse.FromNode = edge.ToNode;
reverse.ToNode = edge.FromNode;
reverse.Reverse = true;
_edges[reverse.FromNode].push_back(std::move(reverse));
++_edgeCount;
}
}
while (edgeResult->NextRow());
}
QueryResult pointResult = WorldDatabase.Query(
"SELECT edge_id, point_index, map, position_x, position_y, position_z, orientation "
"FROM vcore_clientless_travel_edge_point "
"ORDER BY edge_id, point_index");
if (pointResult)
{
do
{
Field* fields = pointResult->Fetch();
ClientlessTravelEdgePoint point;
uint32 edgeId = fields[0].GetUInt32();
point.MapId = fields[2].GetUInt16();
point.X = fields[3].GetFloat();
point.Y = fields[4].GetFloat();
point.Z = fields[5].GetFloat();
point.O = fields[6].GetFloat();
_edgePoints[edgeId].push_back(point);
}
while (pointResult->NextRow());
}
_loaded = true;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] loaded travel graph: %u node(s), %u directed edge(s).",
uint32(_nodes.size()), _edgeCount);
return !_nodes.empty();
}
std::string ClientlessTravelGraphMgr::FindNearestStartNode(
Player const* player,
float maxDistance) const
{
if (!player || !player->IsInWorld())
return {};
std::string bestKey;
float bestDistance = std::numeric_limits<float>::max();
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 distance = std::sqrt(dx * dx + dy * dy + dz * dz);
if (distance > maxDistance || distance >= bestDistance)
continue;
bestDistance = distance;
bestKey = pair.first;
}
return bestKey;
}
bool ClientlessTravelGraphMgr::PlanRoute(
Player const* player,
std::string const& destinationNode,
float startRadius,
std::vector<ClientlessTravelRouteStep>& route,
std::string& error)
{
route.clear();
error.clear();
EnsureLoaded();
if (!player || !player->IsInWorld())
{
error = "player is not in world";
return false;
}
if (_nodes.find(destinationNode) == _nodes.end())
{
error = "unknown destination node: " + destinationNode;
return false;
}
if (startRadius <= 0.0f)
startRadius = 500.0f;
std::string const startNode = FindNearestStartNode(player, startRadius);
if (startNode.empty())
{
error = "no travel node near current position";
return false;
}
struct QueueItem
{
float Cost;
std::string Node;
bool operator>(QueueItem const& other) const
{
return Cost > other.Cost;
}
};
struct Previous
{
std::string Node;
std::string EdgeType;
uint32 EdgeDatabaseId = 0;
bool EdgeReverse = false;
};
std::priority_queue<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> open;
std::unordered_map<std::string, float> distance;
std::unordered_map<std::string, Previous> previous;
for (auto const& pair : _nodes)
distance[pair.first] = std::numeric_limits<float>::infinity();
distance[startNode] = 0.0f;
open.push({ 0.0f, startNode });
while (!open.empty())
{
QueueItem current = open.top();
open.pop();
if (current.Cost != distance[current.Node])
continue;
if (current.Node == destinationNode)
break;
auto edgeItr = _edges.find(current.Node);
if (edgeItr == _edges.end())
continue;
for (ClientlessTravelEdge const& edge : edgeItr->second)
{
float const candidate = current.Cost + edge.Cost;
if (candidate >= distance[edge.ToNode])
continue;
distance[edge.ToNode] = candidate;
previous[edge.ToNode] = {
current.Node, edge.Type, edge.DatabaseId, edge.Reverse
};
open.push({ candidate, edge.ToNode });
}
}
if (!std::isfinite(distance[destinationNode]))
{
error = "no known route from " + startNode + " to " + destinationNode;
return false;
}
std::vector<std::string> nodeKeys;
std::string cursor = destinationNode;
nodeKeys.push_back(cursor);
while (cursor != startNode)
{
auto previousItr = previous.find(cursor);
if (previousItr == previous.end())
{
error = "route reconstruction failed";
return false;
}
cursor = previousItr->second.Node;
nodeKeys.push_back(cursor);
}
std::reverse(nodeKeys.begin(), nodeKeys.end());
route.reserve(nodeKeys.size());
for (std::size_t i = 0; i < nodeKeys.size(); ++i)
{
ClientlessTravelNode const& node = _nodes.at(nodeKeys[i]);
ClientlessTravelRouteStep step;
step.NodeKey = node.Key;
step.NodeType = node.Type;
step.WorldPointKey = node.WorldPointKey;
step.MapId = node.MapId;
step.X = node.X;
step.Y = node.Y;
step.Z = node.Z;
step.O = node.O;
if (i == 0)
step.EdgeType = "start";
else
{
Previous const& prev = previous.at(nodeKeys[i]);
step.EdgeType = prev.EdgeType;
step.EdgeDatabaseId = prev.EdgeDatabaseId;
step.EdgeReverse = prev.EdgeReverse;
}
route.push_back(std::move(step));
}
return true;
}
std::vector<ClientlessTravelEdgePoint> const*
ClientlessTravelGraphMgr::GetEdgePoints(uint32 edgeDatabaseId) const
{
auto itr = _edgePoints.find(edgeDatabaseId);
return itr != _edgePoints.end() ? &itr->second : nullptr;
}
@@ -0,0 +1,103 @@
/*
* Vcore AdventureBot travel graph.
*
* The graph is deliberately data driven and may be incomplete. Missing routes
* fail cleanly. New nodes and edges can be added without rebuilding the core.
*
* C8.6 executes only "ground" edges. Other edge types may already be stored
* and planned, but execution stops cleanly until their executor exists.
*/
#ifndef VCORE_CLIENTLESS_TRAVEL_GRAPH_MGR_H
#define VCORE_CLIENTLESS_TRAVEL_GRAPH_MGR_H
#include "Common.h"
#include <string>
#include <unordered_map>
#include <vector>
class Player;
struct TC_GAME_API ClientlessTravelNode
{
uint32 DatabaseId = 0;
std::string Key;
std::string Type;
std::string WorldPointKey;
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
struct TC_GAME_API ClientlessTravelEdge
{
uint32 DatabaseId = 0;
std::string FromNode;
std::string ToNode;
std::string Type;
float Cost = 1.0f;
bool Reverse = false;
};
struct TC_GAME_API ClientlessTravelEdgePoint
{
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
struct TC_GAME_API ClientlessTravelRouteStep
{
std::string NodeKey;
std::string NodeType;
std::string WorldPointKey;
std::string EdgeType;
uint32 EdgeDatabaseId = 0;
bool EdgeReverse = false;
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
class TC_GAME_API ClientlessTravelGraphMgr
{
public:
static ClientlessTravelGraphMgr* instance();
bool Reload();
bool EnsureLoaded();
bool PlanRoute(
Player const* player,
std::string const& destinationNode,
float startRadius,
std::vector<ClientlessTravelRouteStep>& route,
std::string& error);
uint32 GetNodeCount() const { return uint32(_nodes.size()); }
uint32 GetEdgeCount() const { return _edgeCount; }
std::vector<ClientlessTravelEdgePoint> const* GetEdgePoints(uint32 edgeDatabaseId) const;
private:
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
bool _loaded = false;
uint32 _edgeCount = 0;
std::unordered_map<std::string, ClientlessTravelNode> _nodes;
std::unordered_map<std::string, std::vector<ClientlessTravelEdge>> _edges;
std::unordered_map<uint32, std::vector<ClientlessTravelEdgePoint>> _edgePoints;
};
#define sClientlessTravelGraphMgr ClientlessTravelGraphMgr::instance()
#endif
@@ -0,0 +1,215 @@
#include "ClientlessTravelMgr.h"
#include "Log.h"
#include "MotionMaster.h"
#include "Player.h"
#include "World.h"
#include "WorldSession.h"
namespace
{
constexpr float ARRIVAL_DISTANCE = 0.5f;
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;
}
ClientlessTravelMgr* ClientlessTravelMgr::instance()
{
static ClientlessTravelMgr instance;
return &instance;
}
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";
}
return "Unknown";
}
void ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record)
{
if (!player)
return;
player->AttackStop();
player->CastStop();
player->StopMoving();
player->GetMotionMaster()->Clear();
Position const& target = record.Status.Target;
player->GetMotionMaster()->MovePoint(
1,
target.GetPositionX(),
target.GetPositionY(),
target.GetPositionZ(),
true);
record.LastDistance = player->GetDistance(
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;
}
bool ClientlessTravelMgr::StartTravel(Player* player, Position const& target)
{
if (!player || !player->IsInWorld())
return false;
uint32 const characterGuid = player->GetGUID().GetCounter();
TravelRecord& record = _records[characterGuid];
record.Status = {};
record.Status.State = ClientlessTravelState::Moving;
record.Status.MapId = player->GetMapId();
record.Status.Target = target;
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);
return true;
}
void ClientlessTravelMgr::CancelTravel(uint32 characterGuid)
{
_records.erase(characterGuid);
}
ClientlessTravelStatus const* ClientlessTravelMgr::GetStatus(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() ? &itr->second.Status : nullptr;
}
void ClientlessTravelMgr::Update(uint32 diff)
{
for (auto& [characterGuid, record] : _records)
{
if (record.Status.State != ClientlessTravelState::Moving)
continue;
if (record.CheckTimerMs > diff)
{
record.CheckTimerMs -= diff;
continue;
}
uint32 const elapsed = record.CheckTimerMs + diff;
record.CheckTimerMs = PROGRESS_CHECK_INTERVAL_MS;
WorldSession* session = sWorld->FindClientlessSession(characterGuid);
Player* player = session ? session->GetPlayer() : nullptr;
if (!player || !player->IsInWorld())
continue;
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());
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(
record.LastObservedPosition.GetPositionX(),
record.LastObservedPosition.GetPositionY(),
record.LastObservedPosition.GetPositionZ());
if (observedJump >= 15.0f)
{
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] movement-recovery-jump guid=%u name='%s' map=%u jump=%.2f "
"from=(%.3f %.3f %.3f) to=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f) repaths=%u",
characterGuid, player->GetName().c_str(), player->GetMapId(), observedJump,
record.LastObservedPosition.GetPositionX(),
record.LastObservedPosition.GetPositionY(),
record.LastObservedPosition.GetPositionZ(),
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(),
record.Status.Target.GetPositionX(),
record.Status.Target.GetPositionY(),
record.Status.Target.GetPositionZ(),
record.Status.RepathCount);
}
}
record.LastObservedPosition.Relocate(player->GetPosition());
record.HasLastObservedPosition = true;
Position const& target = record.Status.Target;
float const distance = player->GetDistance(
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())
{
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);
continue;
}
float const progress = record.LastDistance - distance;
if (progress >= MIN_PROGRESS_DISTANCE)
record.Status.NoProgressMs = 0;
else
record.Status.NoProgressMs += elapsed;
record.LastDistance = distance;
if (record.Status.NoProgressMs < REPATH_AFTER_NO_PROGRESS_MS)
continue;
if (record.Status.RepathCount >= MAX_REPATHS)
{
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);
continue;
}
++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);
LaunchPath(player, record);
}
}
@@ -0,0 +1,62 @@
/*
* Vcore clientless point-travel robustness layer.
* C8.2: arrival detection, progress monitoring, repathing, stuck state.
*/
#ifndef VCORE_CLIENTLESS_TRAVEL_MGR_H
#define VCORE_CLIENTLESS_TRAVEL_MGR_H
#include "Common.h"
#include "Position.h"
#include <unordered_map>
class Player;
enum class ClientlessTravelState : uint8
{
Idle,
Moving,
Arrived,
Stuck,
InvalidMap
};
struct TC_GAME_API ClientlessTravelStatus
{
ClientlessTravelState State = ClientlessTravelState::Idle;
uint32 MapId = 0;
Position Target;
float RemainingDistance = 0.0f;
uint32 RepathCount = 0;
uint32 NoProgressMs = 0;
};
class TC_GAME_API ClientlessTravelMgr
{
public:
static ClientlessTravelMgr* instance();
bool StartTravel(Player* player, Position const& target);
void CancelTravel(uint32 characterGuid);
ClientlessTravelStatus const* GetStatus(uint32 characterGuid) const;
void Update(uint32 diff);
static char const* StateName(ClientlessTravelState state);
private:
struct TravelRecord
{
ClientlessTravelStatus Status;
float LastDistance = 0.0f;
Position LastObservedPosition;
bool HasLastObservedPosition = false;
uint32 CheckTimerMs = 0;
};
void LaunchPath(Player* player, TravelRecord& record);
std::unordered_map<uint32, TravelRecord> _records;
};
#define sClientlessTravelMgr ClientlessTravelMgr::instance()
#endif
@@ -0,0 +1,108 @@
#include "ClientlessWorldPointMgr.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "Player.h"
#include <cmath>
#include <limits>
ClientlessWorldPointMgr* ClientlessWorldPointMgr::instance()
{
static ClientlessWorldPointMgr instance;
return &instance;
}
bool ClientlessWorldPointMgr::EnsureLoaded()
{
if (_loaded)
return true;
return Reload();
}
bool ClientlessWorldPointMgr::Reload()
{
_points.clear();
QueryResult result = WorldDatabase.Query(
"SELECT id, point_key, category, map, position_x, position_y, position_z, orientation "
"FROM vcore_clientless_world_point WHERE enabled = 1 ORDER BY id");
if (!result)
{
_loaded = true;
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] loaded 0 clientless world points.");
return false;
}
do
{
Field* fields = result->Fetch();
ClientlessWorldPoint point;
point.DatabaseId = fields[0].GetUInt32();
point.Id = fields[1].GetString();
point.Category = fields[2].GetString();
point.MapId = fields[3].GetUInt16();
point.X = fields[4].GetFloat();
point.Y = fields[5].GetFloat();
point.Z = fields[6].GetFloat();
point.O = fields[7].GetFloat();
if (point.Id.empty() || point.Category.empty())
continue;
_points.push_back(std::move(point));
}
while (result->NextRow());
_loaded = true;
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] loaded %u clientless world points.",
uint32(_points.size()));
return true;
}
ClientlessWorldPoint const* ClientlessWorldPointMgr::FindNearest(
Player const* player,
ClientlessWorldPointQuery const& query)
{
if (!player || !player->IsInWorld())
return nullptr;
EnsureLoaded();
float const maxDistance = query.MaxDistance > 0.0f
? query.MaxDistance
: DefaultSearchRadius;
ClientlessWorldPoint const* best = nullptr;
float bestDistance = std::numeric_limits<float>::max();
for (ClientlessWorldPoint const& point : _points)
{
if (point.MapId != player->GetMapId())
continue;
if (!query.Id.empty() && point.Id != query.Id)
continue;
if (!query.Category.empty() && point.Category != query.Category)
continue;
float const dx = point.X - player->GetPositionX();
float const dy = point.Y - player->GetPositionY();
float const dz = point.Z - player->GetPositionZ();
float const distance = std::sqrt(dx * dx + dy * dy + dz * dz);
if (distance > maxDistance || distance >= bestDistance)
continue;
best = &point;
bestDistance = distance;
}
return best;
}
@@ -0,0 +1,55 @@
/*
* Vcore AdventureBot local semantic world points.
*/
#ifndef VCORE_CLIENTLESS_WORLD_POINT_MGR_H
#define VCORE_CLIENTLESS_WORLD_POINT_MGR_H
#include "Common.h"
#include <string>
#include <vector>
class Player;
struct TC_GAME_API ClientlessWorldPoint
{
uint32 DatabaseId = 0;
std::string Id;
std::string Category;
uint32 MapId = 0;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float O = 0.0f;
};
struct TC_GAME_API ClientlessWorldPointQuery
{
std::string Id;
std::string Category;
float MaxDistance = 0.0f;
};
class TC_GAME_API ClientlessWorldPointMgr
{
public:
static ClientlessWorldPointMgr* instance();
bool Reload();
bool EnsureLoaded();
ClientlessWorldPoint const* FindNearest(
Player const* player,
ClientlessWorldPointQuery const& query);
uint32 GetPointCount() const { return uint32(_points.size()); }
float GetDefaultSearchRadius() const { return DefaultSearchRadius; }
private:
static constexpr float DefaultSearchRadius = 400.0f;
bool _loaded = false;
std::vector<ClientlessWorldPoint> _points;
};
#define sClientlessWorldPointMgr ClientlessWorldPointMgr::instance()
#endif
+2
View File
@@ -21,6 +21,7 @@
#include "World.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "AccountMgr.h"
#include "AchievementMgr.h"
#include "AddonMgr.h"
@@ -3393,6 +3394,7 @@ void World::UpdateSessions(uint32 diff)
// High-level clientless controllers observe state before the low-level
// lifecycle loop may destroy a Removing session.
sClientlessPlayerMgr->Update(diff);
sClientlessRouteRecorderMgr->Update(diff);
// Clientless sessions share normal WorldSession callbacks/update machinery,
// but are intentionally outside the network session registry and queue.