diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 73ccbb237f..c406ce9373 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -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_ 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(); diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index 8127d3f4be..ef818073bb 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -54,6 +54,12 @@ private: std::vector 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; diff --git a/src/server/game/World/ClientlessTravelGraphMgr.h b/src/server/game/World/ClientlessTravelGraphMgr.h index 9cf57e8c79..b9afe7d024 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.h +++ b/src/server/game/World/ClientlessTravelGraphMgr.h @@ -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; diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 81d3d9ee3b..97d6959057 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -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 +#include #include #include #include #include +#include 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 [type]"); + handler->SendSysMessage( + "Usage: .clientless node add [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 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 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 "); + 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, "" }, };