Implemented travel graph
This commit is contained in:
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
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user