diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index c406ce9373..d2255235c4 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -812,7 +812,39 @@ bool ClientlessActionExecutorMgr::StartRouteStep( sClientlessTravelMgr->CancelTravel(characterGuid); record.EdgePath.clear(); - if (!player->ActivateTaxiPathTo(taxiPathId)) + // AdventureBots are performative world actors, not simulated + // participants in the player economy. Give the normal Trinity taxi + // code enough temporary money to pass its fare check, then restore the + // bot's original balance immediately after the flight has been started. + // + // Use a generous temporary balance rather than taxiCost directly: + // ActivateTaxiPathTo() may internally calculate a different total when + // a path contains multiple legs. + uint64 const originalMoney = player->GetMoney(); + constexpr uint64 TemporaryTaxiMoney = 1000000000ULL; + + if (originalMoney < TemporaryTaxiMoney) + player->SetMoney(TemporaryTaxiMoney); + + // Clientless taxi movement runs server-side, but observers may + // otherwise see the bot flying without a visible taxi mount. Resolve + // the proper faction/node-specific taxi mount and apply it explicitly + // before starting the normal Trinity taxi flight. + uint32 const taxiMountDisplayId = + sObjectMgr->GetTaxiMountDisplayId( + sourceTaxiNodeId, + player->GetTeam(), + true); + + if (taxiMountDisplayId && !player->IsMounted()) + player->Mount(taxiMountDisplayId); + + bool const taxiStarted = player->ActivateTaxiPathTo(taxiPathId); + + if (player->GetMoney() != originalMoney) + player->SetMoney(originalMoney); + + if (!taxiStarted) { record.Status.State = ClientlessActionExecutionState::Failed; record.Status.TargetName = "node:" + step.NodeKey; diff --git a/src/server/game/World/ClientlessTravelMgr.cpp b/src/server/game/World/ClientlessTravelMgr.cpp index a6e962fb75..8eeaaf4e2f 100644 --- a/src/server/game/World/ClientlessTravelMgr.cpp +++ b/src/server/game/World/ClientlessTravelMgr.cpp @@ -2,10 +2,14 @@ #include "Log.h" #include "MotionMaster.h" +#include "PathGenerator.h" #include "Player.h" #include "World.h" #include "WorldSession.h" +#include +#include + namespace { constexpr float ARRIVAL_DISTANCE = 0.5f; @@ -35,6 +39,98 @@ char const* ClientlessTravelMgr::StateName(ClientlessTravelState state) return "Unknown"; } +bool ClientlessTravelMgr::AnalyzePath( + Player* player, + Position const& target, + ClientlessPathDiagnostics& diagnostics) const +{ + diagnostics = {}; + + if (!player || !player->IsInWorld()) + { + diagnostics.Reason = "player-not-in-world"; + return false; + } + + float const sx = player->GetPositionX(); + float const sy = player->GetPositionY(); + float const sz = player->GetPositionZ(); + float const tx = target.GetPositionX(); + float const ty = target.GetPositionY(); + float const tz = target.GetPositionZ(); + + float const ddx = tx - sx; + float const ddy = ty - sy; + float const ddz = tz - sz; + diagnostics.DirectDistance = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz); + + PathGenerator path(player); + diagnostics.Calculated = path.CalculatePath(tx, ty, tz); + diagnostics.PathType = uint32(path.GetPathType()); + + auto const& points = path.GetPath(); + diagnostics.PointCount = uint32(points.size()); + + if (!points.empty()) + { + for (std::size_t i = 1; i < points.size(); ++i) + { + float const dx = points[i].x - points[i - 1].x; + float const dy = points[i].y - points[i - 1].y; + float const dz = points[i].z - points[i - 1].z; + float const segment = std::sqrt(dx * dx + dy * dy + dz * dz); + + diagnostics.PathLength += segment; + diagnostics.MaxSegmentLength = std::max(diagnostics.MaxSegmentLength, segment); + diagnostics.MaxZDelta = std::max(diagnostics.MaxZDelta, std::fabs(dz)); + } + + auto const& last = points.back(); + float const dx = last.x - tx; + float const dy = last.y - ty; + float const dz = last.z - tz; + diagnostics.FinalDistance = std::sqrt(dx * dx + dy * dy + dz * dz); + } + else + diagnostics.FinalDistance = diagnostics.DirectDistance; + + uint32 const pathType = diagnostics.PathType; + diagnostics.Complete = + diagnostics.Calculated && + (pathType & uint32(PATHFIND_NOPATH)) == 0 && + (pathType & uint32(PATHFIND_INCOMPLETE)) == 0; + + auto addReason = [&](char const* reason) + { + if (!diagnostics.Reason.empty()) + diagnostics.Reason += ","; + diagnostics.Reason += reason; + diagnostics.Suspicious = true; + }; + + if (!diagnostics.Calculated) + addReason("calculate-failed"); + if (pathType & uint32(PATHFIND_NOPATH)) + addReason("no-path"); + if (pathType & uint32(PATHFIND_INCOMPLETE)) + addReason("incomplete"); + if (diagnostics.PointCount == 0 && diagnostics.DirectDistance > ARRIVAL_DISTANCE) + addReason("empty-path"); + if (diagnostics.MaxSegmentLength > 25.0f) + addReason("large-segment"); + if (diagnostics.MaxZDelta > 8.0f) + addReason("large-z-delta"); + if (diagnostics.FinalDistance > 3.0f) + addReason("far-from-target"); + if (diagnostics.DirectDistance > 5.0f && diagnostics.PathLength > diagnostics.DirectDistance * 8.0f) + addReason("extreme-detour"); + + if (diagnostics.Reason.empty()) + diagnostics.Reason = "ok"; + + return diagnostics.Calculated; +} + void ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record) { if (!player) @@ -46,6 +142,49 @@ void ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record) player->GetMotionMaster()->Clear(); Position const& target = record.Status.Target; + + AnalyzePath(player, target, record.Status.Path); + ClientlessPathDiagnostics const& nav = record.Status.Path; + + if (nav.Suspicious) + { + TC_LOG_WARN("server.adventurebot", + "[AdventureBot][Nav] path-suspicious guid=%u name='%s' map=%u " + "start=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f) " + "type=0x%X calculated=%u complete=%u points=%u direct=%.2f path=%.2f " + "maxSegment=%.2f maxZDelta=%.2f finalDistance=%.2f repath=%u reason='%s'", + player->GetGUID().GetCounter(), player->GetName().c_str(), player->GetMapId(), + player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), + nav.PathType, nav.Calculated ? 1u : 0u, nav.Complete ? 1u : 0u, + nav.PointCount, nav.DirectDistance, nav.PathLength, + nav.MaxSegmentLength, nav.MaxZDelta, nav.FinalDistance, + record.Status.RepathCount, nav.Reason.c_str()); + } + else + { + TC_LOG_INFO("server.adventurebot", + "[AdventureBot][Nav] path guid=%u name='%s' map=%u " + "type=0x%X points=%u direct=%.2f path=%.2f maxSegment=%.2f " + "maxZDelta=%.2f finalDistance=%.2f repath=%u", + player->GetGUID().GetCounter(), player->GetName().c_str(), player->GetMapId(), + nav.PathType, nav.PointCount, nav.DirectDistance, nav.PathLength, + nav.MaxSegmentLength, nav.MaxZDelta, nav.FinalDistance, + record.Status.RepathCount); + } + + PathGenerator debugPath(player); + if (debugPath.CalculatePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ())) + { + auto const& debugPoints = debugPath.GetPath(); + for (std::size_t i = 0; i < debugPoints.size(); ++i) + TC_LOG_DEBUG("server.adventurebot", + "[AdventureBot][Nav] path-point guid=%u repath=%u index=%u/%u xyz=(%.3f %.3f %.3f)", + player->GetGUID().GetCounter(), record.Status.RepathCount, + uint32(i + 1), uint32(debugPoints.size()), + debugPoints[i].x, debugPoints[i].y, debugPoints[i].z); + } + player->GetMotionMaster()->MovePoint( 1, target.GetPositionX(), @@ -201,6 +340,12 @@ void ClientlessTravelMgr::Update(uint32 diff) TC_LOG_WARN("server.worldserver", "ClientlessTravel character GUID %u is stuck (remaining=%.2f, repaths=%u).", characterGuid, distance, record.Status.RepathCount); + TC_LOG_WARN("server.adventurebot", + "[AdventureBot][Nav] stuck guid=%u name='%s' map=%u remaining=%.2f " + "repaths=%u lastPathType=0x%X lastPathPoints=%u lastReason='%s'", + characterGuid, player->GetName().c_str(), player->GetMapId(), distance, + record.Status.RepathCount, record.Status.Path.PathType, + record.Status.Path.PointCount, record.Status.Path.Reason.c_str()); continue; } @@ -210,6 +355,12 @@ void ClientlessTravelMgr::Update(uint32 diff) "ClientlessTravel repathing character GUID %u (remaining=%.2f, repath=%u/%u).", characterGuid, distance, record.Status.RepathCount, MAX_REPATHS); + TC_LOG_WARN("server.adventurebot", + "[AdventureBot][Nav] repath guid=%u name='%s' map=%u remaining=%.2f " + "noProgressMs=%u repath=%u/%u reason='no-progress'", + characterGuid, player->GetName().c_str(), player->GetMapId(), distance, + record.Status.NoProgressMs, record.Status.RepathCount, MAX_REPATHS); + LaunchPath(player, record); } } diff --git a/src/server/game/World/ClientlessTravelMgr.h b/src/server/game/World/ClientlessTravelMgr.h index 066c022644..34cf521e4b 100644 --- a/src/server/game/World/ClientlessTravelMgr.h +++ b/src/server/game/World/ClientlessTravelMgr.h @@ -7,8 +7,24 @@ #include "Common.h" #include "Position.h" +#include #include +struct TC_GAME_API ClientlessPathDiagnostics +{ + bool Calculated = false; + bool Complete = false; + bool Suspicious = false; + uint32 PathType = 0; + uint32 PointCount = 0; + float DirectDistance = 0.0f; + float PathLength = 0.0f; + float MaxSegmentLength = 0.0f; + float MaxZDelta = 0.0f; + float FinalDistance = 0.0f; + std::string Reason; +}; + class Player; enum class ClientlessTravelState : uint8 @@ -28,6 +44,7 @@ struct TC_GAME_API ClientlessTravelStatus float RemainingDistance = 0.0f; uint32 RepathCount = 0; uint32 NoProgressMs = 0; + ClientlessPathDiagnostics Path; }; class TC_GAME_API ClientlessTravelMgr @@ -36,6 +53,7 @@ public: static ClientlessTravelMgr* instance(); bool StartTravel(Player* player, Position const& target); + bool AnalyzePath(Player* player, Position const& target, ClientlessPathDiagnostics& diagnostics) const; void CancelTravel(uint32 characterGuid); ClientlessTravelStatus const* GetStatus(uint32 characterGuid) const; void Update(uint32 diff); diff --git a/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua b/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua index 12c0989ca8..c10bd3db06 100644 --- a/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua +++ b/src/server/scripts/Custom/clientless/adventure/overrides/arthoran.lua @@ -10,7 +10,7 @@ intend("socialize", { city = "Ironforge", venue = "tavern", activity = "drink", -- approach_distance = 2 --}) queue_action("travel_to_location", { - id = "goldshire_tavern" + id = "kharanos_tavern" }) -- C8.6 incremental travel-graph example. -- Enable after the corresponding graph nodes/edges exist in cata_world. diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 97d6959057..ea7e01bd69 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -1446,6 +1446,139 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg return true; } + static void PrintClientlessPathDiagnostics( + ChatHandler* handler, + uint32 characterGuid, + ClientlessPathDiagnostics const& nav) + { + handler->PSendSysMessage( + "Nav GUID %u: calculated=%s complete=%s suspicious=%s type=0x%X reason='%s'", + characterGuid, + nav.Calculated ? "yes" : "no", + nav.Complete ? "yes" : "no", + nav.Suspicious ? "yes" : "no", + nav.PathType, + nav.Reason.c_str()); + + handler->PSendSysMessage( + " points=%u direct=%.2f path=%.2f maxSegment=%.2f maxZDelta=%.2f finalDistance=%.2f", + nav.PointCount, nav.DirectDistance, nav.PathLength, + nav.MaxSegmentLength, nav.MaxZDelta, nav.FinalDistance); + } + + static bool HandleClientlessNavInfo(ChatHandler* handler, char const* args) + { + uint32 characterGuid = 0; + if (!ParseCharacterGuid(handler, args, characterGuid)) + { + handler->SendSysMessage("Usage: .clientless navinfo "); + return false; + } + + ClientlessTravelStatus const* status = sClientlessTravelMgr->GetStatus(characterGuid); + if (!status) + { + handler->PSendSysMessage("Clientless GUID %u has no travel/navigation state.", characterGuid); + return true; + } + + PrintClientlessPathDiagnostics(handler, characterGuid, status->Path); + return true; + } + + static bool HandleClientlessNavTest(ChatHandler* handler, char const* args) + { + if (!handler || !handler->GetSession()) + return true; + + std::istringstream input(args ? args : ""); + std::string first; + input >> first; + + bool const locationMode = (first == "location"); + uint32 characterGuid = 0; + std::string locationKey; + + if (locationMode) + { + input >> characterGuid >> locationKey; + if (!characterGuid || locationKey.empty()) + { + handler->SendSysMessage("Usage: .clientless navtest location "); + return false; + } + } + else + { + char* end = nullptr; + unsigned long const parsed = std::strtoul(first.c_str(), &end, 10); + if (first.empty() || !end || *end != '\0' || parsed == 0 || parsed > 0xFFFFFFFFul) + { + handler->SendSysMessage("Usage: .clientless navtest OR .clientless navtest location "); + return false; + } + characterGuid = uint32(parsed); + } + + WorldSession* session = sWorld->FindClientlessSession(characterGuid); + Player* bot = session ? session->GetPlayer() : nullptr; + if (!bot || !bot->IsInWorld()) + { + handler->PSendSysMessage("Clientless GUID %u is not online in world.", characterGuid); + return true; + } + + Position target; + if (locationMode) + { + QueryResult result = WorldDatabase.PQuery( + "SELECT map,position_x,position_y,position_z,orientation " + "FROM vcore_clientless_world_point WHERE point_key='%s' AND enabled=1 LIMIT 1", + locationKey.c_str()); + + if (!result) + { + handler->PSendSysMessage("Unknown/enabled location '%s'.", locationKey.c_str()); + return true; + } + + Field* f = result->Fetch(); + uint32 const mapId = f[0].GetUInt32(); + if (mapId != bot->GetMapId()) + { + handler->PSendSysMessage("Navtest requires same map: bot=%u location=%u.", bot->GetMapId(), mapId); + return true; + } + + target.Relocate(f[1].GetFloat(), f[2].GetFloat(), f[3].GetFloat(), f[4].GetFloat()); + } + else + { + Player* gm = handler->GetSession()->GetPlayer(); + if (!gm || !gm->IsInWorld()) + return true; + if (gm->GetMapId() != bot->GetMapId()) + { + handler->PSendSysMessage("Navtest requires same map: bot=%u GM=%u.", bot->GetMapId(), gm->GetMapId()); + return true; + } + target.Relocate(gm->GetPosition()); + } + + ClientlessPathDiagnostics nav; + sClientlessTravelMgr->AnalyzePath(bot, target, nav); + + handler->PSendSysMessage( + "Navtest GUID %u map=%u start=(%.3f %.3f %.3f) target=(%.3f %.3f %.3f)", + characterGuid, bot->GetMapId(), + bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); + if (locationMode) + handler->PSendSysMessage(" location='%s'", locationKey.c_str()); + PrintClientlessPathDiagnostics(handler, characterGuid, nav); + return true; + } + static bool HandleClientlessTravelInfo(ChatHandler* handler, char const* args) { uint32 characterGuid = 0; @@ -1473,6 +1606,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg status->RemainingDistance, status->RepathCount, status->NoProgressMs); + PrintClientlessPathDiagnostics(handler, characterGuid, status->Path); return true; } @@ -1791,6 +1925,8 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg { "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessInfo, "" }, { "travelto", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTravelTo, "" }, { "travelinfo", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTravelInfo, "" }, + { "navtest", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNavTest, "" }, + { "navinfo", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessNavInfo, "" }, { "perceive", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessPerceive, "" }, { "actioninfo", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessActionInfo, "" }, { "pointreload", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessPointReload, "" },