implemented taxinodes
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-14 16:03:14 +00:00
1 parent 4c04cd88de
commit 24fb292cee
4 files changed
+547 -4

No files matched your search

@@ -95,6 +95,22 @@ namespace
float parsed = std::strtof(value.c_str(), &end);
return end && end != value.c_str() ? parsed : fallback;
}
bool ParseTaxiNodeKey(std::string const& key, uint32& taxiNodeId)
{
static std::string const Prefix = "taxi_";
if (key.rfind(Prefix, 0) != 0 || key.size() <= Prefix.size())
return false;
char* end = nullptr;
unsigned long const parsed = std::strtoul(key.c_str() + Prefix.size(), &end, 10);
if (!end || *end != '\0' || parsed == 0 || parsed > 0xFFFFFFFFul)
return false;
taxiNodeId = uint32(parsed);
return true;
}
}
ClientlessActionExecutorMgr* ClientlessActionExecutorMgr::instance()
@@ -480,6 +496,10 @@ bool ClientlessActionExecutorMgr::StartTravelToNode(
record.Route.clear();
record.RouteStep = 0;
record.EdgePath.clear();
record.TaxiActive = false;
record.TaxiPathId = 0;
record.TaxiSourceNodeId = 0;
record.TaxiDestinationNodeId = 0;
std::string error;
if (!sClientlessTravelGraphMgr->PlanRoute(
player, destinationNode, startRadius, record.Route, error))
@@ -611,6 +631,10 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
record.Route.clear();
record.RouteStep = 0;
record.EdgePath.clear();
record.TaxiActive = false;
record.TaxiPathId = 0;
record.TaxiSourceNodeId = 0;
record.TaxiDestinationNodeId = 0;
std::string anchorNode;
std::string error;
@@ -725,7 +749,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
ClientlessTravelRouteStep const& step = record.Route[record.RouteStep];
if (step.EdgeType != "start" && step.EdgeType != "ground")
if (step.EdgeType != "start" && step.EdgeType != "ground" && step.EdgeType != "taxi")
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
@@ -741,6 +765,103 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
return false;
}
if (step.EdgeType == "taxi")
{
if (record.RouteStep == 0)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "taxi edge has no source route step";
record.Active = false;
return false;
}
ClientlessTravelRouteStep const& sourceStep = record.Route[record.RouteStep - 1];
uint32 sourceTaxiNodeId = 0;
uint32 destinationTaxiNodeId = 0;
if (!ParseTaxiNodeKey(sourceStep.NodeKey, sourceTaxiNodeId) ||
!ParseTaxiNodeKey(step.NodeKey, destinationTaxiNodeId))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.Detail = "taxi edge endpoints are not taxi_<id> nodes";
record.Active = false;
return false;
}
uint32 taxiPathId = 0;
uint32 taxiCost = 0;
sObjectMgr->GetTaxiPath(
sourceTaxiNodeId,
destinationTaxiNodeId,
taxiPathId,
taxiCost);
if (!taxiPathId)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.Detail =
"Trinity has no taxi path " +
std::to_string(sourceTaxiNodeId) + " -> " +
std::to_string(destinationTaxiNodeId);
record.Active = false;
return false;
}
sClientlessTravelMgr->CancelTravel(characterGuid);
record.EdgePath.clear();
if (!player->ActivateTaxiPathTo(taxiPathId))
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.Detail =
"ActivateTaxiPathTo failed for path=" +
std::to_string(taxiPathId) +
" source=" + std::to_string(sourceTaxiNodeId) +
" destination=" + std::to_string(destinationTaxiNodeId);
record.Active = false;
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] taxi-start-failed guid=%u name='%s' path=%u source=%u destination=%u cost=%u",
characterGuid,
player->GetName().c_str(),
taxiPathId,
sourceTaxiNodeId,
destinationTaxiNodeId,
taxiCost);
return false;
}
record.TaxiActive = true;
record.TaxiPathId = taxiPathId;
record.TaxiSourceNodeId = sourceTaxiNodeId;
record.TaxiDestinationNodeId = destinationTaxiNodeId;
record.Status.State = ClientlessActionExecutionState::Moving;
record.Status.TargetName = "node:" + step.NodeKey;
record.Status.TargetGuid = 0;
record.Status.TargetEntry = 0;
record.Status.Detail =
"taxi path " + std::to_string(taxiPathId) +
" " + std::to_string(sourceTaxiNodeId) +
" -> " + std::to_string(destinationTaxiNodeId);
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] taxi-started guid=%u name='%s' routeStep=%u/%u path=%u source=%u destination=%u cost=%u",
characterGuid,
player->GetName().c_str(),
uint32(record.RouteStep + 1),
uint32(record.Route.size()),
taxiPathId,
sourceTaxiNodeId,
destinationTaxiNodeId,
taxiCost);
return true;
}
if (step.MapId != player->GetMapId())
{
record.Status.State = ClientlessActionExecutionState::Failed;
@@ -886,6 +1007,82 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
uint32 characterGuid,
Record& record)
{
if (record.TaxiActive)
{
if (!player || !player->IsInWorld())
return;
if (player->IsInFlight())
return;
if (record.RouteStep >= record.Route.size())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "taxi route step index became invalid";
record.Active = false;
record.TaxiActive = false;
return;
}
ClientlessTravelRouteStep const& destination = record.Route[record.RouteStep];
if (player->GetMapId() != destination.MapId)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail =
"taxi ended on unexpected map; expected=" +
std::to_string(destination.MapId) +
" actual=" + std::to_string(player->GetMapId());
record.Active = false;
record.TaxiActive = false;
return;
}
float const dx = player->GetPositionX() - destination.X;
float const dy = player->GetPositionY() - destination.Y;
float const dz = player->GetPositionZ() - destination.Z;
float const remaining = std::sqrt(dx * dx + dy * dy + dz * dz);
constexpr float TaxiArrivalSanityRadius = 120.0f;
if (remaining > TaxiArrivalSanityRadius)
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail =
"taxi stopped away from destination node; remaining=" +
std::to_string(remaining);
record.Active = false;
record.TaxiActive = false;
TC_LOG_WARN("server.adventurebot",
"[AdventureBot] taxi-ended-away-from-node guid=%u name='%s' path=%u source=%u destination=%u remaining=%.2f",
characterGuid,
player->GetName().c_str(),
record.TaxiPathId,
record.TaxiSourceNodeId,
record.TaxiDestinationNodeId,
remaining);
return;
}
TC_LOG_INFO("server.adventurebot",
"[AdventureBot] taxi-complete guid=%u name='%s' path=%u source=%u destination=%u remaining=%.2f",
characterGuid,
player->GetName().c_str(),
record.TaxiPathId,
record.TaxiSourceNodeId,
record.TaxiDestinationNodeId,
remaining);
record.TaxiActive = false;
record.TaxiPathId = 0;
record.TaxiSourceNodeId = 0;
record.TaxiDestinationNodeId = 0;
++record.RouteStep;
StartRouteStep(player, characterGuid, record);
return;
}
if (!record.EdgePath.empty())
{
ClientlessTravelEdgePoint const& destination = record.EdgePath.back();
@@ -54,6 +54,12 @@ private:
std::vector<ClientlessTravelEdgePoint> EdgePath;
// C8.10b: active server-side taxi edge.
bool TaxiActive = false;
uint32 TaxiPathId = 0;
uint32 TaxiSourceNodeId = 0;
uint32 TaxiDestinationNodeId = 0;
bool FinalLocalLegPending = false;
bool FinalLocalLegActive = false;
uint32 FinalMapId = 0;
@@ -18,6 +18,12 @@
class Player;
// Canonical Vcore travel-node types:
// ground, taxi, ship, zeppelin, tram, portal, elevator
//
// Node type describes what the place is. Edge type describes how travel occurs.
// Ground edges may connect any node type; specialized edge executors may impose
// stricter endpoint rules.
struct TC_GAME_API ClientlessTravelNode
{
uint32 DatabaseId = 0;
@@ -20,6 +20,7 @@
#include "ClientlessRouteRecorderMgr.h"
#include "ClientlessLuaEngine.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
#include "Creature.h"
#include "Group.h"
#include "ObjectAccessor.h"
@@ -33,10 +34,13 @@
#include "Unit.h"
#include "World.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <sstream>
#include <string>
#include <cctype>
#include <vector>
namespace VianoBotCommands
{
@@ -718,6 +722,18 @@ namespace VianoBotCommands
return handler && handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
}
static bool IsClientlessTravelNodeType(std::string const& type)
{
return type == "ground"
|| type == "taxi"
|| type == "ship"
|| type == "zeppelin"
|| type == "tram"
|| type == "portal"
|| type == "elevator"
|| type == "location"; // legacy C8.x node type
}
static bool HandleClientlessNodeAdd(ChatHandler* handler, char const* args)
{
Player* player = GetClientlessCommandPlayer(handler);
@@ -726,14 +742,23 @@ static bool HandleClientlessNodeAdd(ChatHandler* handler, char const* args)
std::istringstream input(args ? args : "");
std::string key;
std::string type = "location";
std::string type = "ground";
input >> key;
if (!(input >> type))
type = "location";
type = "ground";
if (key.empty())
{
handler->SendSysMessage("Usage: .clientless node add <key> [type]");
handler->SendSysMessage(
"Usage: .clientless node add <key> [ground|taxi|ship|zeppelin|tram|portal|elevator]");
return true;
}
if (!IsClientlessTravelNodeType(type))
{
handler->PSendSysMessage(
"Unknown travel node type '%s'. Allowed: ground, taxi, ship, zeppelin, tram, portal, elevator.",
type.c_str());
return true;
}
@@ -838,6 +863,105 @@ static bool HandleClientlessNodeList(ChatHandler* handler, char const* /*args*/)
return true;
}
static bool HandleClientlessNodeListNearby(ChatHandler* handler, char const* args)
{
Player* player = GetClientlessCommandPlayer(handler);
if (!player || !player->IsInWorld())
return true;
float radius = 300.0f;
std::istringstream input(args ? args : "");
if (input >> radius)
{
if (radius <= 0.0f)
{
handler->SendSysMessage("Usage: .clientless node list_nearby [radius>0]");
return true;
}
}
struct NearbyNode
{
std::string Key;
std::string Type;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
float Distance = 0.0f;
};
QueryResult result = WorldDatabase.PQuery(
"SELECT n.node_key,n.node_type,w.position_x,w.position_y,w.position_z "
"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 AND w.map=%u",
uint32(player->GetMapId()));
if (!result)
{
handler->PSendSysMessage(
"No travel nodes on map %u within %.1f yd.",
uint32(player->GetMapId()), radius);
return true;
}
std::vector<NearbyNode> nodes;
do
{
Field* f = result->Fetch();
NearbyNode node;
node.Key = f[0].GetString();
node.Type = f[1].GetString();
node.X = f[2].GetFloat();
node.Y = f[3].GetFloat();
node.Z = f[4].GetFloat();
float const dx = node.X - player->GetPositionX();
float const dy = node.Y - player->GetPositionY();
float const dz = node.Z - player->GetPositionZ();
node.Distance = std::sqrt(dx * dx + dy * dy + dz * dz);
if (node.Distance <= radius)
nodes.push_back(std::move(node));
}
while (result->NextRow());
std::sort(nodes.begin(), nodes.end(),
[](NearbyNode const& left, NearbyNode const& right)
{
return left.Distance < right.Distance;
});
if (nodes.empty())
{
handler->PSendSysMessage(
"No travel nodes within %.1f yd on map %u.",
radius, uint32(player->GetMapId()));
return true;
}
handler->PSendSysMessage(
"Travel nodes within %.1f yd on map %u: %u",
radius, uint32(player->GetMapId()), uint32(nodes.size()));
for (NearbyNode const& node : nodes)
{
handler->PSendSysMessage(
"[%.1f yd] %s type=%s x=%.2f y=%.2f z=%.2f",
node.Distance,
node.Key.c_str(),
node.Type.c_str(),
node.X,
node.Y,
node.Z);
}
return true;
}
static bool HandleClientlessRouteRemove(ChatHandler* handler, char const* args)
{
std::istringstream input(args ? args : "");
@@ -984,6 +1108,211 @@ static bool HandleClientlessRouteStatus(ChatHandler* handler, char const* /*args
return true;
}
static bool HandleClientlessTaxiImport(ChatHandler* handler, char const* /*args*/)
{
std::vector<uint32> importedTaxiNodes;
uint32 createdPoints = 0;
uint32 updatedPoints = 0;
uint32 createdNodes = 0;
uint32 updatedNodes = 0;
uint32 createdEdges = 0;
uint32 existingEdges = 0;
for (uint32 taxiNodeId = 1; taxiNodeId < sTaxiNodesStore.GetNumRows(); ++taxiNodeId)
{
TaxiNodesEntry const* taxiNode = sTaxiNodesStore.LookupEntry(taxiNodeId);
if (!taxiNode)
continue;
uint8 const field = uint8((taxiNodeId - 1) / 8);
uint32 const submask = 1u << ((taxiNodeId - 1) % 8);
if ((sTaxiNodesMask[field] & submask) == 0)
continue;
std::string const key = "taxi_" + std::to_string(taxiNodeId);
QueryResult pointExists = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_world_point WHERE point_key='%s' LIMIT 1",
key.c_str());
if (pointExists)
{
WorldDatabase.DirectPExecute(
"UPDATE vcore_clientless_world_point "
"SET category='travel_taxi',map=%u,position_x=%.6f,position_y=%.6f,position_z=%.6f,"
"orientation=0,enabled=1,comment='Auto-imported from TaxiNodes.dbc node %u' "
"WHERE point_key='%s'",
uint32(taxiNode->ContinentID),
taxiNode->Pos.X,
taxiNode->Pos.Y,
taxiNode->Pos.Z,
taxiNodeId,
key.c_str());
++updatedPoints;
}
else
{
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_world_point "
"(point_key,category,map,position_x,position_y,position_z,orientation,enabled,comment) "
"VALUES ('%s','travel_taxi',%u,%.6f,%.6f,%.6f,0,1,"
"'Auto-imported from TaxiNodes.dbc node %u')",
key.c_str(),
uint32(taxiNode->ContinentID),
taxiNode->Pos.X,
taxiNode->Pos.Y,
taxiNode->Pos.Z,
taxiNodeId);
++createdPoints;
}
QueryResult nodeExists = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_node WHERE node_key='%s' LIMIT 1",
key.c_str());
if (nodeExists)
{
WorldDatabase.DirectPExecute(
"UPDATE vcore_clientless_travel_node "
"SET node_type='taxi',world_point_key='%s',enabled=1,"
"comment='Auto-imported TaxiNodes.dbc node %u' "
"WHERE node_key='%s'",
key.c_str(), taxiNodeId, key.c_str());
++updatedNodes;
}
else
{
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_travel_node "
"(node_key,node_type,world_point_key,enabled,comment) "
"VALUES ('%s','taxi','%s',1,'Auto-imported TaxiNodes.dbc node %u')",
key.c_str(), key.c_str(), taxiNodeId);
++createdNodes;
}
importedTaxiNodes.push_back(taxiNodeId);
}
for (uint32 sourceId : importedTaxiNodes)
{
std::string const fromKey = "taxi_" + std::to_string(sourceId);
for (uint32 destinationId : importedTaxiNodes)
{
if (sourceId == destinationId)
continue;
uint32 pathId = 0;
uint32 pathCost = 0;
sObjectMgr->GetTaxiPath(sourceId, destinationId, pathId, pathCost);
if (!pathId)
continue;
std::string const toKey = "taxi_" + std::to_string(destinationId);
QueryResult edgeExists = WorldDatabase.PQuery(
"SELECT id FROM vcore_clientless_travel_edge "
"WHERE from_node_key='%s' AND to_node_key='%s' AND edge_type='taxi' LIMIT 1",
fromKey.c_str(), toKey.c_str());
if (edgeExists)
{
++existingEdges;
continue;
}
WorldDatabase.DirectPExecute(
"INSERT INTO vcore_clientless_travel_edge "
"(from_node_key,to_node_key,edge_type,cost,bidirectional,enabled,comment) "
"VALUES ('%s','%s','taxi',1,0,1,"
"'Auto-imported TaxiPath id=%u price=%u')",
fromKey.c_str(),
toKey.c_str(),
pathId,
pathCost);
++createdEdges;
}
}
bool const graphLoaded = sClientlessTravelGraphMgr->Reload();
sClientlessWorldPointMgr->Reload();
handler->PSendSysMessage(
"Taxi import complete: taxiNodes=%u points(created=%u updated=%u) "
"nodes(created=%u updated=%u) taxiEdges(created=%u existing=%u).",
uint32(importedTaxiNodes.size()),
createdPoints,
updatedPoints,
createdNodes,
updatedNodes,
createdEdges,
existingEdges);
handler->PSendSysMessage(
"Travel graph reloaded: %u node(s), %u directed edge(s)%s. "
"Taxi execution is intentionally not implemented yet.",
sClientlessTravelGraphMgr->GetNodeCount(),
sClientlessTravelGraphMgr->GetEdgeCount(),
graphLoaded ? "" : " (no usable nodes)");
return true;
}
static bool HandleClientlessTaxiInfo(ChatHandler* handler, char const* args)
{
std::istringstream input(args ? args : "");
uint32 taxiNodeId = 0;
input >> taxiNodeId;
if (!taxiNodeId)
{
handler->SendSysMessage("Usage: .clientless taxi info <TaxiNodeId>");
return true;
}
TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(taxiNodeId);
if (!node)
{
handler->PSendSysMessage("TaxiNode %u does not exist in TaxiNodes.dbc.", taxiNodeId);
return true;
}
handler->PSendSysMessage(
"TaxiNode %u: map=%u x=%.2f y=%.2f z=%.2f vcoreKey=taxi_%u",
taxiNodeId,
uint32(node->ContinentID),
node->Pos.X,
node->Pos.Y,
node->Pos.Z,
taxiNodeId);
uint32 outgoing = 0;
for (uint32 destinationId = 1; destinationId < sTaxiNodesStore.GetNumRows(); ++destinationId)
{
if (destinationId == taxiNodeId || !sTaxiNodesStore.LookupEntry(destinationId))
continue;
uint32 pathId = 0;
uint32 cost = 0;
sObjectMgr->GetTaxiPath(taxiNodeId, destinationId, pathId, cost);
if (!pathId)
continue;
handler->PSendSysMessage(
" -> taxi_%u path=%u cost=%u",
destinationId,
pathId,
cost);
++outgoing;
}
handler->PSendSysMessage("Outgoing taxi edges: %u.", outgoing);
return true;
}
static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*args*/)
{
bool const loaded = sClientlessTravelGraphMgr->Reload();
@@ -1478,6 +1807,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
{ "remove", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNodeRemove, "" },
{ "force-remove", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNodeForceRemove, "" },
{ "list", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNodeList, "" },
{ "list_nearby", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNodeListNearby, "" },
} },
{ "route", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", {
{ "remove", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessRouteRemove, "" },
@@ -1490,6 +1820,10 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
{ "cancel", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessRouteCancel, "" },
{ "status", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessRouteStatus, "" },
} },
{ "taxi", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", {
{ "import", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiImport, "" },
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
} },
{ "travelreload", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTravelReload, "" },
};