Implement clientless transports, Lua reload and same-map portals
Add native transport recording and execution with grid lifecycle handling, clientless teleport acknowledgements, and regression tests. Support targeted same-map portal imports and preserve disabled taxi nodes. Document live transport and portal verification and the remaining portal inventory.
This commit is contained in:
1 parent
01a9ba4b0b
commit
1164a72ea3
40 files changed
+4533
-163
No files matched your search
@@ -1,4 +1,5 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
@@ -149,6 +150,12 @@ ClientlessActionExecutorMgr* ClientlessActionExecutorMgr::instance()
|
||||
return &instance;
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::Clear(uint32 characterGuid)
|
||||
{
|
||||
_records.erase(characterGuid);
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
}
|
||||
|
||||
char const* ClientlessActionExecutorMgr::StateName(ClientlessActionExecutionState state)
|
||||
{
|
||||
switch (state)
|
||||
@@ -171,8 +178,16 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
record.TransportActive = false;
|
||||
record.Status = {};
|
||||
record.Status.ActionName = action.Name;
|
||||
if (player->GetTransport())
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "player is already aboard a transport; wait for a safe recorded stop";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
record.Status.State = ClientlessActionExecutionState::SelectingTarget;
|
||||
record.FinalLocalLegPending = false;
|
||||
record.FinalLocalLegActive = false;
|
||||
@@ -722,6 +737,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation(
|
||||
std::string anchorNode;
|
||||
std::string error;
|
||||
float proxyDistance = std::numeric_limits<float>::infinity();
|
||||
sClientlessTravelGraphMgr->EnsureLoaded();
|
||||
if (!sClientlessTravelGraphMgr->FindNearestNodeToPosition(
|
||||
point->MapId, point->X, point->Y, point->Z, anchorNode, proxyDistance))
|
||||
{
|
||||
@@ -796,15 +812,11 @@ bool ClientlessActionExecutorMgr::StartFinalLocalLeg(
|
||||
record.FinalZ,
|
||||
record.FinalO);
|
||||
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "final-local-leg");
|
||||
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination, false))
|
||||
// A graph boundary does not require dismounting. Keep the normal ground
|
||||
// mount policy on the final MMAP leg, including subsequent local segments.
|
||||
if (!sClientlessTravelMgr->StartTravel(player, destination, true))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "location final local travel start failed";
|
||||
record.Active = false;
|
||||
record.FinalLocalLegPending = false;
|
||||
return false;
|
||||
return RecoverFinalLocalLeg(player, characterGuid, record, "final-local-start-failed");
|
||||
}
|
||||
|
||||
record.FinalLocalLegPending = false;
|
||||
@@ -816,11 +828,11 @@ bool ClientlessActionExecutorMgr::StartFinalLocalLeg(
|
||||
record.Status.Detail = "local navigation to final location";
|
||||
record.Active = true;
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot] travel-location-local-leg guid=%u name='%s' location='%s'",
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"[AdventureBot] travel-location-local-leg guid=%u name='%s' location='%s' mounted=%u",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
record.FinalLocationId.c_str());
|
||||
record.FinalLocationId.c_str(), player->IsMounted() ? 1u : 0u);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -878,6 +890,22 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
step.NodeKey.c_str(), step.NodeType.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, step.EdgeReverse ? 1u : 0u, record.RecoveryCount);
|
||||
|
||||
if (ClientlessTransportMgr::IsTransportEdge(step.EdgeType))
|
||||
{
|
||||
std::string error;
|
||||
if (!record.RouteStep || !sClientlessTransportMgr->Start(player,
|
||||
record.Route[record.RouteStep - 1].NodeKey, step.NodeKey, error))
|
||||
{
|
||||
return RecoverTravel(player, characterGuid, record, step.EdgeDatabaseId,
|
||||
"transport-start-failed: " + error);
|
||||
}
|
||||
record.TransportActive = true;
|
||||
record.Active = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.Detail = "waiting for native transport";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (step.EdgeType != "start" && step.EdgeType != "ground" &&
|
||||
step.EdgeType != "taxi" && step.EdgeType != "portal" &&
|
||||
step.EdgeType != "local_bridge")
|
||||
@@ -990,18 +1018,6 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
return false;
|
||||
}
|
||||
|
||||
// C8.13a only imports far map transitions. Keeping this invariant here
|
||||
// prevents a clientless Player from getting stuck waiting for the near
|
||||
// MOVE_TELEPORT_ACK path, which is intentionally not implemented yet.
|
||||
if (uint32(atEntry->ContinentID) == teleport->target_mapId)
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "same-map portal transition is not supported by C8.13a";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player->GetMapId() != uint32(atEntry->ContinentID) ||
|
||||
!player->IsInAreaTriggerRadius(atEntry))
|
||||
{
|
||||
@@ -1045,7 +1061,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
areaTriggerPacket << triggerId;
|
||||
player->GetSession()->HandleAreaTriggerOpcode(areaTriggerPacket);
|
||||
|
||||
if (player->IsBeingTeleportedFar())
|
||||
if (player->IsBeingTeleported())
|
||||
{
|
||||
if (!player->GetSession()->IsClientless())
|
||||
{
|
||||
@@ -1055,13 +1071,11 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
return false;
|
||||
}
|
||||
|
||||
// A real client would answer the far transfer with CMSG_MOVE_WORLDPORT_ACK.
|
||||
// Clientless sessions have no socket, so finish the exact same server-side
|
||||
// transfer path explicitly after the normal AreaTrigger handler accepted it.
|
||||
player->GetSession()->HandleMoveWorldportAck();
|
||||
// Finish the normal near/far ACK after the real AreaTrigger handler.
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
}
|
||||
|
||||
if (!player->IsInWorld() || player->GetMapId() != teleport->target_mapId)
|
||||
if (!player->IsInWorld() || player->IsBeingTeleported() || player->GetMapId() != teleport->target_mapId)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u name='%s' kind='portal-transfer-not-completed' edgeDbId=%u "
|
||||
@@ -1083,6 +1097,18 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
float const dy = player->GetPositionY() - teleport->target_Y;
|
||||
float const dz = player->GetPositionZ() - teleport->target_Z;
|
||||
float const arrivalError = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
// On a same-map transition the map ID alone also matches a denied
|
||||
// teleport. Require arrival at the actual destination before advancing.
|
||||
if (arrivalError > 5.0f)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"EDGE_FAILED guid=%u kind='portal-destination-not-reached' trigger=%u arrivalError=%.2f",
|
||||
characterGuid, triggerId, arrivalError);
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "AreaTrigger destination not reached";
|
||||
record.Active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
@@ -1649,6 +1675,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
|
||||
{
|
||||
std::string anchorNode;
|
||||
float proxyDistance = std::numeric_limits<float>::infinity();
|
||||
sClientlessTravelGraphMgr->EnsureLoaded();
|
||||
if (sClientlessTravelGraphMgr->FindNearestNodeToPosition(
|
||||
record.FinalMapId, record.FinalX, record.FinalY, record.FinalZ,
|
||||
anchorNode, proxyDistance))
|
||||
@@ -1738,6 +1765,36 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
uint32 characterGuid,
|
||||
Record& record)
|
||||
{
|
||||
if (record.TransportActive)
|
||||
{
|
||||
ClientlessTransportStatus const* transport = sClientlessTransportMgr->GetStatus(characterGuid);
|
||||
if (transport && transport->Phase != ClientlessTransportPhase::Failed &&
|
||||
transport->Phase != ClientlessTransportPhase::Arrived)
|
||||
{
|
||||
record.Status.Detail = transport->Detail;
|
||||
return;
|
||||
}
|
||||
record.TransportActive = false;
|
||||
if (transport && transport->Phase == ClientlessTransportPhase::Arrived)
|
||||
{
|
||||
++record.RouteStep;
|
||||
StartRouteStep(player, characterGuid, record);
|
||||
}
|
||||
else if (player->GetTransport() || (transport && transport->Cancelled))
|
||||
{
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = transport ? transport->Detail : "transport state lost aboard";
|
||||
record.Active = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32 edge = record.RouteStep < record.Route.size() ? record.Route[record.RouteStep].EdgeDatabaseId : 0;
|
||||
std::string reason = transport ? transport->Detail : "transport state lost";
|
||||
RecoverTravel(player, characterGuid, record, edge, "transport-failed: " + reason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.RecoveryWanderActive)
|
||||
{
|
||||
ClientlessTravelStatus const* wander =
|
||||
@@ -2065,11 +2122,11 @@ void ClientlessActionExecutorMgr::UpdateTravelToLocation(
|
||||
record.Active = false;
|
||||
record.FinalLocalLegActive = false;
|
||||
|
||||
TC_LOG_INFO("server.adventurebot",
|
||||
"[AdventureBot] travel-location-complete guid=%u name='%s' location='%s'",
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"[AdventureBot] travel-location-complete guid=%u name='%s' location='%s' mounted=%u",
|
||||
characterGuid,
|
||||
player->GetName().c_str(),
|
||||
record.FinalLocationId.c_str());
|
||||
record.FinalLocationId.c_str(), player->IsMounted() ? 1u : 0u);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2083,6 +2140,12 @@ void ClientlessActionExecutorMgr::UpdateTravelToLocation(
|
||||
ClientlessTravelMgr::StateName(travel->State),
|
||||
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
|
||||
|
||||
RecoverFinalLocalLeg(player, characterGuid, record, reason);
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::RecoverFinalLocalLeg(
|
||||
Player* player, uint32 characterGuid, Record& record, std::string const& reason)
|
||||
{
|
||||
record.FinalLocalLegActive = false;
|
||||
record.FinalLocalLegPending = true;
|
||||
if (!record.Route.empty())
|
||||
@@ -2097,8 +2160,23 @@ void ClientlessActionExecutorMgr::UpdateTravelToLocation(
|
||||
"C820_EXIT_BLOCKED edgeDbId=%u endIndex=%d reason='%s'",
|
||||
last.EdgeDatabaseId, last.GroundEndIndex, reason.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
record.FailedBridgeKeys.insert("finish:" + last.NodeKey);
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"C821_NODE_FINISH_BLOCKED guid=%u node='%s' reason='%s'",
|
||||
characterGuid, last.NodeKey.c_str(), reason.c_str());
|
||||
}
|
||||
}
|
||||
RecoverTravel(player, characterGuid, record, 0, reason);
|
||||
else
|
||||
{
|
||||
// A direct local attempt has no graph endpoint to blacklist. Ensure
|
||||
// recovery can select the graph and then reject failed endpoints.
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"C821_DIRECT_LOCAL_FAILED guid=%u location='%s' reason='%s'",
|
||||
characterGuid, record.FinalLocationId.c_str(), reason.c_str());
|
||||
}
|
||||
return RecoverTravel(player, characterGuid, record, 0, reason);
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::UpdateActive(
|
||||
@@ -2158,7 +2236,8 @@ void ClientlessActionExecutorMgr::Update(uint32 /*diff*/)
|
||||
{
|
||||
for (auto const& [characterGuid, session] : sWorld->GetAllClientlessSessions())
|
||||
{
|
||||
if (!session || !sClientlessPlayerMgr->IsManaged(characterGuid))
|
||||
if (!session || !sClientlessPlayerMgr->IsManaged(characterGuid) ||
|
||||
sWorld->GetClientlessPlayerState(characterGuid) != ClientlessPlayerState::Online)
|
||||
continue;
|
||||
|
||||
Player* player = session->GetPlayer();
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
static ClientlessActionExecutorMgr* instance();
|
||||
|
||||
void Update(uint32 diff);
|
||||
void Clear(uint32 characterGuid);
|
||||
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
|
||||
|
||||
static char const* StateName(ClientlessActionExecutionState state);
|
||||
@@ -58,6 +59,7 @@ private:
|
||||
std::vector<ClientlessTravelEdgePoint> EdgePath;
|
||||
|
||||
// C8.10b: active server-side taxi edge.
|
||||
bool TransportActive = false;
|
||||
bool TaxiActive = false;
|
||||
uint32 TaxiPathId = 0;
|
||||
uint32 TaxiSourceNodeId = 0;
|
||||
@@ -98,6 +100,7 @@ private:
|
||||
bool StartTravelToNode(Player* player, ClientlessAction const& action, Record& record);
|
||||
bool StartTravelToLocation(Player* player, ClientlessAction const& action, Record& record);
|
||||
bool StartFinalLocalLeg(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverFinalLocalLeg(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
bool StartRouteStep(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverTravel(Player* player, uint32 characterGuid, Record& record, uint32 failedEdgeDatabaseId, std::string const& reason);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "Config.h"
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include "Random.h"
|
||||
@@ -14,6 +17,15 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
// World-thread only. Script effects are committed only after a successful reload.
|
||||
struct ReloadEffects
|
||||
{
|
||||
std::vector<ClientlessAction> Actions;
|
||||
ClientlessIntent Intent;
|
||||
bool IntentChanged = false;
|
||||
};
|
||||
ReloadEffects* pendingReload = nullptr;
|
||||
|
||||
uint32 BoundGuid(lua_State* L) { return uint32(lua_tointeger(L, lua_upvalueindex(1))); }
|
||||
|
||||
std::unordered_map<std::string, std::string> ReadMap(lua_State* L, int index)
|
||||
@@ -48,12 +60,30 @@ int LuaIntend(lua_State* L)
|
||||
ClientlessIntent intent;
|
||||
intent.Name = luaL_checkstring(L, 1);
|
||||
if (lua_gettop(L) >= 2) intent.Parameters = ReadMap(L, 2);
|
||||
if (pendingReload)
|
||||
{
|
||||
bool valid = !intent.Empty();
|
||||
if (valid)
|
||||
{
|
||||
pendingReload->Intent = std::move(intent);
|
||||
pendingReload->IntentChanged = true;
|
||||
}
|
||||
lua_pushboolean(L, valid);
|
||||
return 1;
|
||||
}
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->SetIntent(BoundGuid(L), std::move(intent)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaClearIntent(lua_State* L)
|
||||
{
|
||||
if (pendingReload)
|
||||
{
|
||||
pendingReload->Intent = ClientlessIntent();
|
||||
pendingReload->IntentChanged = true;
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->ClearIntent(BoundGuid(L)));
|
||||
return 1;
|
||||
}
|
||||
@@ -63,6 +93,13 @@ int LuaQueueAction(lua_State* L)
|
||||
ClientlessAction action;
|
||||
action.Name = luaL_checkstring(L, 1);
|
||||
if (lua_gettop(L) >= 2) action.Parameters = ReadMap(L, 2);
|
||||
if (pendingReload)
|
||||
{
|
||||
bool valid = !action.Name.empty();
|
||||
if (valid) pendingReload->Actions.push_back(std::move(action));
|
||||
lua_pushboolean(L, valid);
|
||||
return 1;
|
||||
}
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->QueueAction(BoundGuid(L), std::move(action)));
|
||||
return 1;
|
||||
}
|
||||
@@ -184,7 +221,10 @@ ClientlessLuaEngine* ClientlessLuaEngine::instance()
|
||||
|
||||
ClientlessLuaEngine::ClientlessLuaEngine()
|
||||
{
|
||||
_scriptRoot = std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") ? std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") : "scripts/clientless";
|
||||
_scriptRoot = sConfigMgr->GetStringDefault("Clientless.ScriptRoot", "scripts/clientless");
|
||||
if (char const* overrideRoot = std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR"))
|
||||
if (*overrideRoot) _scriptRoot = overrideRoot;
|
||||
_scriptRoot = std::filesystem::absolute(_scriptRoot).lexically_normal().string();
|
||||
_state = luaL_newstate();
|
||||
if (!_state)
|
||||
{
|
||||
@@ -445,6 +485,61 @@ bool ClientlessLuaEngine::ExecuteAdventureScript(Player* player)
|
||||
return ExecuteScript(player, (root / "adventure" / "generic.lua").string(), "generic adventure");
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ReloadAdventureScript(Player* player, std::string& detail)
|
||||
{
|
||||
if (!player || !player->IsInWorld() || !sClientlessPlayerMgr->IsManaged(player->GetGUID().GetCounter()))
|
||||
{
|
||||
detail = "Reload requires an online managed clientless player.";
|
||||
return false;
|
||||
}
|
||||
uint32 guid = player->GetGUID().GetCounter();
|
||||
auto action = sClientlessActionExecutorMgr->GetStatus(guid);
|
||||
auto travel = sClientlessTravelMgr->GetStatus(guid);
|
||||
if (pendingReload || sClientlessPlayerMgr->GetQueuedActionCount(guid) ||
|
||||
(action && (action->State == ClientlessActionExecutionState::SelectingTarget ||
|
||||
action->State == ClientlessActionExecutionState::Moving)) ||
|
||||
(travel && travel->State == ClientlessTravelState::Moving) ||
|
||||
player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported())
|
||||
{
|
||||
detail = "Reload refused: bot has queued actions, is travelling or is aboard a transport. Wait until idle.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto previous = _profiles.find(guid);
|
||||
bool hadProfile = previous != _profiles.end();
|
||||
ClientlessAdventureProfile backup = hadProfile ? previous->second : ClientlessAdventureProfile();
|
||||
ReloadEffects effects;
|
||||
bool success;
|
||||
{
|
||||
struct Capture
|
||||
{
|
||||
explicit Capture(ReloadEffects& effects) { pendingReload = &effects; }
|
||||
~Capture() { pendingReload = nullptr; }
|
||||
} capture(effects);
|
||||
success = LoadAdventureProfile(player) && ExecuteAdventureScript(player);
|
||||
}
|
||||
if (!success)
|
||||
{
|
||||
if (hadProfile) _profiles[guid] = std::move(backup);
|
||||
else _profiles.erase(guid);
|
||||
detail = "Lua reload failed. Previous profile retained; no new actions or intent applied. See server log.";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (effects.IntentChanged)
|
||||
{
|
||||
if (effects.Intent.Empty()) sClientlessPlayerMgr->ClearIntent(guid);
|
||||
else sClientlessPlayerMgr->SetIntent(guid, std::move(effects.Intent));
|
||||
}
|
||||
for (auto& queued : effects.Actions)
|
||||
sClientlessPlayerMgr->QueueAction(guid, std::move(queued));
|
||||
detail = "Profile and adventure script reloaded; queued actions=" + std::to_string(effects.Actions.size()) + ".";
|
||||
}
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "LUA_RELOAD guid=%u success=%u root='%s' detail='%s'",
|
||||
guid, success ? 1u : 0u, _scriptRoot.c_str(), detail.c_str());
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ExecuteTestScript(Player* player, std::string const& scriptName)
|
||||
{
|
||||
if (!player || scriptName.empty() || scriptName.find("..") != std::string::npos || scriptName.find('/') != std::string::npos || scriptName.find('\\') != std::string::npos)
|
||||
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
|
||||
bool ExecuteAdventureScript(Player* player);
|
||||
bool ExecuteTestScript(Player* player, std::string const& scriptName);
|
||||
bool ReloadAdventureScript(Player* player, std::string& detail);
|
||||
|
||||
bool LoadAdventureProfile(Player* player);
|
||||
ClientlessAdventureProfile const* GetAdventureProfile(uint32 characterGuid) const;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessPerceptionMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "Log.h"
|
||||
@@ -147,6 +148,7 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
// CLIENTLESS_PLAYER_CORE_C8_3_LOCAL_PERCEPTION
|
||||
sClientlessPerceptionMgr->Update(diff);
|
||||
// CLIENTLESS_PLAYER_CORE_C8_4_ACTION_EXECUTOR
|
||||
sClientlessTransportMgr->Update(diff);
|
||||
sClientlessActionExecutorMgr->Update(diff);
|
||||
|
||||
for (auto itr = _records.begin(); itr != _records.end(); )
|
||||
@@ -157,6 +159,7 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
|
||||
if (state == ClientlessPlayerState::Offline)
|
||||
{
|
||||
sClientlessActionExecutorMgr->Clear(characterGuid);
|
||||
itr = _records.erase(itr);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessRouteRecorderMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "GameObject.h"
|
||||
#include "GameTime.h"
|
||||
#include "Opcodes.h"
|
||||
#include "WorldPacket.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "Transport.h"
|
||||
#include "TransportMgr.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
void PublishPassengerState(Player* player)
|
||||
{
|
||||
player->m_movementInfo.guid = player->GetGUID();
|
||||
player->m_movementInfo.pos.Relocate(player->GetPosition());
|
||||
player->m_movementInfo.time = GameTime::GetGameTimeMS();
|
||||
WorldPacket update(SMSG_MOVE_UPDATE);
|
||||
player->WriteMovementInfo(update);
|
||||
player->SendMessageToSet(&update, true);
|
||||
}
|
||||
char const* PhaseName(ClientlessTransportPhase phase)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ClientlessTransportPhase::Waiting: return "Waiting";
|
||||
case ClientlessTransportPhase::Boarding: return "Boarding";
|
||||
case ClientlessTransportPhase::Riding: return "Riding";
|
||||
case ClientlessTransportPhase::Leaving: return "Leaving";
|
||||
case ClientlessTransportPhase::Arrived: return "Arrived";
|
||||
case ClientlessTransportPhase::Failed: return "Failed";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
bool Safe(std::string const& s) { return ClientlessRouteRecorderMgr::IsSafeKey(s); }
|
||||
float Distance(Position const& a, Position const& b) { return a.GetExactDist(&b); }
|
||||
uint32 Spawn(GameObject const* go) { return go->ToTransport() ? go->GetGUID().GetCounter() : go->GetSpawnId(); }
|
||||
bool SameService(ClientlessTransportStop const& a, ClientlessTransportStop const& b)
|
||||
{
|
||||
return a.Entry == b.Entry && a.Spawn == b.Spawn;
|
||||
}
|
||||
Position WorldPoint(GameObject* object, ClientlessTransportPoint const& point)
|
||||
{
|
||||
Position result = point.Pos;
|
||||
if (point.OnTransport)
|
||||
{
|
||||
float x, y, z, o;
|
||||
result.GetPosition(x, y, z, o);
|
||||
object->ToTransportBase()->CalculatePassengerPosition(x, y, z, &o);
|
||||
result.Relocate(x, y, z, o);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
GameObject* Resolve(Player* player, uint32 spawn)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return nullptr;
|
||||
GameObject* go = player->GetMap()->GetGameObjectBySpawnId(spawn);
|
||||
return go ? go : player->GetMap()->GetTransport(ObjectGuid(HighGuid::Mo_Transport, spawn));
|
||||
}
|
||||
bool LoadNode(std::string const& key, uint32& map, Position& pos)
|
||||
{
|
||||
if (!Safe(key))
|
||||
return false;
|
||||
QueryResult r = WorldDatabase.PQuery("SELECT w.map,w.position_x,w.position_y,w.position_z,w.orientation "
|
||||
"FROM vcore_clientless_travel_node n 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", key.c_str());
|
||||
if (!r)
|
||||
return false;
|
||||
Field* f = r->Fetch();
|
||||
// vcore_clientless_world_point.map is SMALLINT, not INT. Field access
|
||||
// must match its width; GetUInt32 returns zero in a debug build.
|
||||
map = f[0].GetUInt16();
|
||||
pos.Relocate(f[1].GetFloat(), f[2].GetFloat(), f[3].GetFloat(), f[4].GetFloat());
|
||||
return true;
|
||||
}
|
||||
float PathLength(ClientlessTransportStop const& stop)
|
||||
{
|
||||
float sum = 0;
|
||||
for (std::size_t i = 1; i < stop.Points.size(); ++i)
|
||||
{
|
||||
auto world = [&](ClientlessTransportPoint const& p)
|
||||
{
|
||||
Position pos = p.Pos;
|
||||
if (p.OnTransport)
|
||||
{
|
||||
float x, y, z, o;
|
||||
pos.GetPosition(x, y, z, o);
|
||||
TransportBase::CalculatePassengerPosition(x, y, z, &o,
|
||||
stop.Dock.GetPositionX(), stop.Dock.GetPositionY(), stop.Dock.GetPositionZ(), stop.Dock.GetOrientation());
|
||||
pos.Relocate(x, y, z, o);
|
||||
}
|
||||
return pos;
|
||||
};
|
||||
sum += Distance(world(stop.Points[i - 1]), world(stop.Points[i]));
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
ClientlessTransportMgr* ClientlessTransportMgr::instance()
|
||||
{
|
||||
static ClientlessTransportMgr mgr;
|
||||
return &mgr;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::IsTransportEdge(std::string const& type)
|
||||
{
|
||||
return type == "ship" || type == "zeppelin" || type == "tram" || type == "elevator";
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::EnsureLoaded()
|
||||
{
|
||||
return _loaded ? _available : Reload();
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::Reload()
|
||||
{
|
||||
_stops.clear();
|
||||
_loaded = true;
|
||||
_available = false;
|
||||
// Keep existing CPC installations usable before the explicit migration.
|
||||
if (!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_transport_stop'") ||
|
||||
!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_transport_point'") ||
|
||||
!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_transport_stand'"))
|
||||
return false;
|
||||
_available = true;
|
||||
QueryResult r = WorldDatabase.Query("SELECT node_key,transport_entry,transport_spawn,map,dock_x,dock_y,dock_z,dock_o,period_ms,stand_name FROM vcore_clientless_transport_stop");
|
||||
if (r)
|
||||
do
|
||||
{
|
||||
Field* f = r->Fetch();
|
||||
ClientlessTransportStop stop;
|
||||
stop.Node = f[0].GetString(); stop.Entry = f[1].GetUInt32(); stop.Spawn = f[2].GetUInt32(); stop.Map = f[3].GetUInt32();
|
||||
stop.Dock.Relocate(f[4].GetFloat(), f[5].GetFloat(), f[6].GetFloat(), f[7].GetFloat());
|
||||
stop.Period = f[8].GetUInt32(); stop.Stand = f[9].GetString();
|
||||
if (Safe(stop.Node) && stop.Entry && stop.Spawn && stop.Period)
|
||||
_stops.emplace(stop.Node, std::move(stop));
|
||||
} while (r->NextRow());
|
||||
r = WorldDatabase.Query("SELECT node_key,point_index,on_transport,x,y,z,o FROM vcore_clientless_transport_point ORDER BY node_key,point_index");
|
||||
if (r)
|
||||
do
|
||||
{
|
||||
Field* f = r->Fetch();
|
||||
auto itr = _stops.find(f[0].GetString());
|
||||
if (itr == _stops.end())
|
||||
continue;
|
||||
if (f[1].GetUInt32() != itr->second.Points.size())
|
||||
{
|
||||
itr->second.Period = 0;
|
||||
continue;
|
||||
}
|
||||
ClientlessTransportPoint p;
|
||||
p.OnTransport = f[2].GetBool();
|
||||
p.Pos.Relocate(f[3].GetFloat(), f[4].GetFloat(), f[5].GetFloat(), f[6].GetFloat());
|
||||
itr->second.Points.push_back(p);
|
||||
} while (r->NextRow());
|
||||
for (auto itr = _stops.begin(); itr != _stops.end(); )
|
||||
{
|
||||
auto const& stop = itr->second;
|
||||
uint32 map = 0; Position node;
|
||||
bool valid = stop.Period && stop.Points.size() >= 2 && stop.Points.size() <= 512 &&
|
||||
!stop.Points.front().OnTransport && stop.Points.back().OnTransport &&
|
||||
LoadNode(stop.Node, map, node) && map == stop.Map && Distance(node, stop.Points.front().Pos) <= 3.0f;
|
||||
bool boarded = false;
|
||||
for (auto const& p : stop.Points)
|
||||
{
|
||||
if (!Trinity::IsValidMapCoord(p.Pos.GetPositionX(), p.Pos.GetPositionY(), p.Pos.GetPositionZ(), p.Pos.GetOrientation()) ||
|
||||
(boarded && !p.OnTransport) ||
|
||||
(p.OnTransport && (std::fabs(p.Pos.GetPositionX()) > 100 ||
|
||||
std::fabs(p.Pos.GetPositionY()) > 100 || std::fabs(p.Pos.GetPositionZ()) > 100))) valid = false;
|
||||
boarded |= p.OnTransport;
|
||||
}
|
||||
if (!valid || !std::isfinite(PathLength(stop)) || PathLength(stop) > 60.0f)
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot.navigation", "TRANSPORT_STOP_INVALID node='%s'", stop.Node.c_str());
|
||||
itr = _stops.erase(itr);
|
||||
}
|
||||
else ++itr;
|
||||
}
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "TRANSPORT_STOPS_LOADED count=%u", uint32(_stops.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::HasConnection(std::string const& from, std::string const& to)
|
||||
{
|
||||
EnsureLoaded();
|
||||
auto a = _stops.find(from), b = _stops.find(to);
|
||||
return a != _stops.end() && b != _stops.end() && SameService(a->second, b->second) &&
|
||||
(a->second.Map != b->second.Map || Distance(a->second.Dock, b->second.Dock) > 3.0f) &&
|
||||
Distance(a->second.Points.back().Pos, b->second.Points.back().Pos) <= 3.0f;
|
||||
}
|
||||
|
||||
GameObject* ClientlessTransportMgr::FindObject(Player* player, ClientlessTransportStop const& stop) const
|
||||
{
|
||||
if (!player || !player->IsInWorld()) return nullptr;
|
||||
GameObjectTemplate const* info = sObjectMgr->GetGameObjectTemplate(stop.Entry);
|
||||
GameObject* object = info && info->type == GAMEOBJECT_TYPE_MO_TRANSPORT
|
||||
? player->GetMap()->GetTransport(ObjectGuid(HighGuid::Mo_Transport, stop.Spawn))
|
||||
: player->GetMap()->GetGameObjectBySpawnId(stop.Spawn);
|
||||
return object && object->GetEntry() == stop.Entry && object->IsInWorld() &&
|
||||
object->ToTransportBase() && object->isSpawned() && player->IsInPhase(object) ? object : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::AtDock(Player* player, GameObject* object, ClientlessTransportStop const& stop, uint32 requiredPause) const
|
||||
{
|
||||
TransportDockState state;
|
||||
return object && player->GetMapId() == stop.Map && object->GetMapId() == stop.Map &&
|
||||
Distance(*object, stop.Dock) <= 1.0f &&
|
||||
std::fabs(std::remainder(object->GetOrientation() - stop.Dock.GetOrientation(), float(2 * M_PI))) < 0.10f &&
|
||||
object->GetTransportDockState(state) && state.Stopped && state.RemainingPause >= requiredPause;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::Sample(Player* player, Recording& recording, bool force)
|
||||
{
|
||||
if (!recording.Error.empty())
|
||||
return false;
|
||||
auto fail = [&](char const* message) { recording.Error = message; return false; };
|
||||
if (!player->IsAlive() || player->IsFlying() || player->IsInFlight())
|
||||
return fail("Record with a living character on foot, not flying or on a taxi.");
|
||||
GameObject* object = FindObject(player, recording.Stop);
|
||||
if (!object || player->GetMapId() != recording.Stop.Map || object->GetGUID() != recording.Object ||
|
||||
!AtDock(player, object, recording.Stop, 0))
|
||||
return fail("Transport left its recorded dock; cancel and record during a complete stop.");
|
||||
bool aboard = player->GetTransport() == object->ToTransportBase();
|
||||
if ((player->GetTransport() && !aboard) || (recording.Attached && !aboard))
|
||||
return fail("Recorder changed or left the transport. Record land -> deck only.");
|
||||
ClientlessTransportPoint point;
|
||||
point.OnTransport = aboard;
|
||||
point.Pos.Relocate(player->GetPosition());
|
||||
if (aboard)
|
||||
{
|
||||
float x, y, z, o;
|
||||
point.Pos.GetPosition(x, y, z, o);
|
||||
object->ToTransportBase()->CalculatePassengerOffset(x, y, z, &o);
|
||||
if (std::fabs(x) > 100 || std::fabs(y) > 100 || std::fabs(z) > 100)
|
||||
return fail("Deck point is outside the supported transport bounds.");
|
||||
point.Pos.Relocate(x, y, z, o);
|
||||
}
|
||||
if (!recording.Stop.Points.empty())
|
||||
{
|
||||
auto const& previous = recording.Stop.Points.back();
|
||||
float delta = Distance(WorldPoint(object, previous), WorldPoint(object, point));
|
||||
if (delta > 8.0f)
|
||||
return fail("Position jump over 8 yards; do not teleport or fly while recording.");
|
||||
if (!force && previous.OnTransport == point.OnTransport && delta < 0.75f)
|
||||
return true;
|
||||
}
|
||||
if (recording.Stop.Points.size() >= 512)
|
||||
return fail("Recording exceeds 512 points.");
|
||||
recording.Stop.Points.push_back(point);
|
||||
recording.Attached |= aboard;
|
||||
if (PathLength(recording.Stop) > 60.0f)
|
||||
return fail("Dock transition exceeds 60 yards; move the shared node closer to the ramp.");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::Save(Player* player, std::string const& stand, std::string& error)
|
||||
{
|
||||
auto itr = _recordings.find(player->GetGUID().GetCounter());
|
||||
if (itr == _recordings.end()) { error = "No active recording."; return false; }
|
||||
Recording& recording = itr->second;
|
||||
if (!Safe(stand) || !Sample(player, recording, true) || !recording.Attached || recording.Stop.Points.size() < 2 || player->IsFalling())
|
||||
{
|
||||
error = recording.Error.empty() ? "Finish aboard the recorded transport and specify a valid stand name." : recording.Error;
|
||||
return false;
|
||||
}
|
||||
auto& stop = recording.Stop;
|
||||
if (WorldDatabase.PQuery("SELECT 1 FROM vcore_clientless_transport_stop WHERE node_key='%s'", stop.Node.c_str()))
|
||||
{ error = "This dock has already been saved; explicitly forget it before replacing a recording."; return false; }
|
||||
uint32 map = 0; Position node;
|
||||
if (!LoadNode(stop.Node, map, node) || map != stop.Map || Distance(node, stop.Points.front().Pos) > 0.25f)
|
||||
{ error = "Shared node changed during recording; cancel and start again."; return false; }
|
||||
auto const& pos = stop.Points.back().Pos;
|
||||
QueryResult existing = WorldDatabase.PQuery("SELECT x,y,z FROM vcore_clientless_transport_stand WHERE transport_entry=%u AND name='%s'", stop.Entry, stand.c_str());
|
||||
if (existing)
|
||||
{
|
||||
Field* f = existing->Fetch(); Position target;
|
||||
target.Relocate(f[0].GetFloat(), f[1].GetFloat(), f[2].GetFloat());
|
||||
if (Distance(pos, target) > 1.5f) { error = "Stand name already exists at another deck point; stand there or use another name."; return false; }
|
||||
}
|
||||
WorldDatabaseTransaction trans = WorldDatabase.BeginTransaction();
|
||||
trans->PAppend("DELETE FROM vcore_clientless_transport_point WHERE node_key='%s'", stop.Node.c_str());
|
||||
trans->PAppend("INSERT INTO vcore_clientless_transport_stop VALUES ('%s',%u,%u,%u,%.9f,%.9f,%.9f,%.9f,%u,'%s')",
|
||||
stop.Node.c_str(), stop.Entry, stop.Spawn, stop.Map, stop.Dock.GetPositionX(), stop.Dock.GetPositionY(), stop.Dock.GetPositionZ(), stop.Dock.GetOrientation(), stop.Period, stand.c_str());
|
||||
uint32 index = 0;
|
||||
for (auto const& p : stop.Points)
|
||||
trans->PAppend("INSERT INTO vcore_clientless_transport_point VALUES ('%s',%u,%u,%.9f,%.9f,%.9f,%.9f)",
|
||||
stop.Node.c_str(), index++, p.OnTransport ? 1u : 0u, p.Pos.GetPositionX(), p.Pos.GetPositionY(), p.Pos.GetPositionZ(), p.Pos.GetOrientation());
|
||||
trans->PAppend("INSERT IGNORE INTO vcore_clientless_transport_stand VALUES (%u,'%s',%.9f,%.9f,%.9f,%.9f)", stop.Entry, stand.c_str(),
|
||||
pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation());
|
||||
WorldDatabase.DirectCommitTransaction(trans);
|
||||
std::string nodeKey = stop.Node;
|
||||
std::size_t pointCount = stop.Points.size();
|
||||
if (!Reload() || !_stops.count(nodeKey) || _stops.at(nodeKey).Points.size() != pointCount)
|
||||
{ error = "Recorded stop could not be reloaded; inspect database errors. Unsaved recording retained."; return false; }
|
||||
_recordings.erase(itr);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std::string const& to, std::string& error)
|
||||
{
|
||||
if (!player || !player->IsInWorld() || player->GetTransport() || !HasConnection(from, to))
|
||||
{ error = "Transport stops are not recorded/compatible, or player is already aboard."; return false; }
|
||||
auto const& source = _stops.at(from);
|
||||
if (source.Map != player->GetMapId() || Distance(*player, source.Points.front().Pos) > 3.0f)
|
||||
{ error = "Player has not reached the shared departure node."; return false; }
|
||||
Ride ride;
|
||||
ride.From = source; ride.To = _stops.at(to);
|
||||
ride.RequiredPause = uint32(PathLength(source) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000.0f) + uint32(source.Points.size()) * 100 + 1500;
|
||||
ride.Status.Phase = ClientlessTransportPhase::Waiting;
|
||||
ride.Status.Detail = "waiting for recorded transport at departure dock";
|
||||
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
|
||||
player->StopMoving();
|
||||
_rides[player->GetGUID().GetCounter()] = std::move(ride);
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "TRANSPORT_WAIT guid=%u from='%s' to='%s' spawn=%u boardingMs=%u",
|
||||
player->GetGUID().GetCounter(), from.c_str(), to.c_str(), source.Spawn, _rides.at(player->GetGUID().GetCounter()).RequiredPause);
|
||||
return true;
|
||||
}
|
||||
|
||||
ClientlessTransportStatus const* ClientlessTransportMgr::GetStatus(uint32 guid) const
|
||||
{
|
||||
auto itr = _rides.find(guid);
|
||||
return itr == _rides.end() ? nullptr : &itr->second.Status;
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail)
|
||||
{
|
||||
ride.Status = { phase, detail, ride.Abort }; ride.PhaseElapsed = 0; ride.Moving = false;
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "TRANSPORT_STATE guid=%u phase='%s' from='%s' to='%s' passenger=%u detail='%s'",
|
||||
player->GetGUID().GetCounter(), PhaseName(phase), ride.From.Node.c_str(), ride.To.Node.c_str(), player->GetTransport() ? 1u : 0u, detail);
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::Cancel(uint32 guid)
|
||||
{
|
||||
auto itr = _rides.find(guid);
|
||||
if (itr != _rides.end()) itr->second.Abort = true;
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
{
|
||||
using Phase = ClientlessTransportPhase;
|
||||
if (ride.Status.Phase == Phase::Arrived || ride.Status.Phase == Phase::Failed) return;
|
||||
ride.Elapsed += diff; ride.PhaseElapsed += diff;
|
||||
if (player->IsBeingTeleported())
|
||||
{
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
return;
|
||||
}
|
||||
GameObject* object = FindObject(player, ride.From);
|
||||
uint32 limit = uint32(std::min<uint64>(1800000, std::max<uint64>(180000, uint64(ride.From.Period) * 3 + 30000)));
|
||||
auto fail = [&](char const* detail)
|
||||
{
|
||||
player->StopMoving();
|
||||
SetPhase(player, ride, Phase::Failed, detail);
|
||||
// Never detach an actual passenger in midair or in the sea.
|
||||
};
|
||||
if (ride.Elapsed > limit) { fail("transport timeout; any passenger attachment retained"); return; }
|
||||
if (ride.Status.Phase == Phase::Waiting)
|
||||
{
|
||||
if (ride.Abort) { fail("transport cancelled on land"); return; }
|
||||
if (!AtDock(player, object, ride.From, ride.RequiredPause)) return;
|
||||
if (Distance(*player, ride.From.Points.front().Pos) > 3.0f)
|
||||
{ fail("player moved away from departure node while waiting"); return; }
|
||||
ride.Point = 0;
|
||||
SetPhase(player, ride, Phase::Boarding, "boarding recorded land-to-deck transition");
|
||||
}
|
||||
if (ride.Status.Phase == Phase::Boarding && ride.Abort)
|
||||
{
|
||||
player->StopMoving();
|
||||
if (!player->GetTransport()) { fail("transport cancelled on land"); return; }
|
||||
SetPhase(player, ride, Phase::Riding, "boarding cancelled; waiting for safe dock");
|
||||
}
|
||||
if (ride.Status.Phase == Phase::Riding)
|
||||
{
|
||||
if (!object || player->GetTransport() != object->ToTransportBase())
|
||||
{ fail("transport passenger identity lost"); return; }
|
||||
ClientlessTransportStop const* arrival = nullptr;
|
||||
if (AtDock(player, object, ride.To, uint32(PathLength(ride.To) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000) + uint32(ride.To.Points.size()) * 100 + 1500)) arrival = &ride.To;
|
||||
else if (ride.Abort && AtDock(player, object, ride.From, ride.RequiredPause)) arrival = &ride.From;
|
||||
if (!arrival) return;
|
||||
if (arrival == &ride.From) ride.To = ride.From;
|
||||
ride.Point = ride.To.Points.size();
|
||||
SetPhase(player, ride, Phase::Leaving, "leaving via reversed recorded transition");
|
||||
}
|
||||
if (ride.Status.Phase != Phase::Boarding && ride.Status.Phase != Phase::Leaving) return;
|
||||
bool boarding = ride.Status.Phase == Phase::Boarding;
|
||||
auto const& stop = boarding ? ride.From : ride.To;
|
||||
if (!object || !AtDock(player, object, stop, 0))
|
||||
{
|
||||
if (player->GetTransport())
|
||||
{
|
||||
player->StopMoving(); ride.Abort = true;
|
||||
SetPhase(player, ride, Phase::Riding, "departure during transition; retaining passenger until safe dock");
|
||||
}
|
||||
else fail("transport departed during land transition");
|
||||
return;
|
||||
}
|
||||
if (ride.Moving)
|
||||
{
|
||||
if (player->IsSplineEnabled())
|
||||
{
|
||||
if (ride.PhaseElapsed > ride.PointTimeout) fail("recorded transition spline timeout");
|
||||
return;
|
||||
}
|
||||
auto const& previous = stop.Points[boarding ? ride.Point - 1 : ride.Point];
|
||||
if (Distance(*player, WorldPoint(object, previous)) > 1.5f)
|
||||
{ fail("recorded transition stopped before target point"); return; }
|
||||
if (!previous.OnTransport && player->GetTransport() == object->ToTransportBase())
|
||||
{
|
||||
object->ToTransportBase()->RemovePassenger(player);
|
||||
PublishPassengerState(player);
|
||||
}
|
||||
ride.Moving = false;
|
||||
}
|
||||
if ((boarding && ride.Point >= stop.Points.size()) || (!boarding && ride.Point == 0))
|
||||
{
|
||||
if (boarding)
|
||||
{
|
||||
if (player->GetTransport() != object->ToTransportBase()) { fail("boarding did not attach passenger"); return; }
|
||||
SetPhase(player, ride, Phase::Riding, "aboard; waiting for destination dock");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (player->GetTransport() || Distance(*player, stop.Points.front().Pos) > 2.0f)
|
||||
{ fail("shared arrival node not reached on land"); return; }
|
||||
SetPhase(player, ride, ride.Abort ? Phase::Failed : Phase::Arrived, ride.Abort ? "cancelled at safe dock" : "arrived at shared destination node");
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto const& point = stop.Points[boarding ? ride.Point : ride.Point - 1];
|
||||
Position target = WorldPoint(object, point);
|
||||
if (Distance(*player, target) > 8.0f) { fail("recorded transition has an unsafe gap over 8 yards"); return; }
|
||||
bool attached = player->GetTransport() == object->ToTransportBase();
|
||||
if (point.OnTransport && !attached)
|
||||
{
|
||||
if (player->GetTransport()) { fail("another passenger owner already attached"); return; }
|
||||
float x, y, z, o;
|
||||
player->GetPosition(x, y, z, o);
|
||||
object->ToTransportBase()->CalculatePassengerOffset(x, y, z, &o);
|
||||
if (std::fabs(x) > 100 || std::fabs(y) > 100 || std::fabs(z) > 100)
|
||||
{ fail("passenger attachment outside supported local bounds"); return; }
|
||||
player->m_movementInfo.transport.Reset();
|
||||
player->m_movementInfo.transport.pos.Relocate(x, y, z, o);
|
||||
TransportDockState state; object->GetTransportDockState(state);
|
||||
player->m_movementInfo.transport.time = state.Progress;
|
||||
object->ToTransportBase()->AddPassenger(player);
|
||||
if (player->GetTransport() != object->ToTransportBase())
|
||||
{ fail("native passenger attachment failed"); return; }
|
||||
PublishPassengerState(player);
|
||||
}
|
||||
// Keep ownership until the first land point is physically reached. If
|
||||
// departure interrupts disembarkation, the bot remains a real passenger.
|
||||
if (!point.OnTransport && attached && Distance(*player, target) <= 0.1f)
|
||||
{
|
||||
object->ToTransportBase()->RemovePassenger(player);
|
||||
PublishPassengerState(player);
|
||||
}
|
||||
Position path[2]; path[0].Relocate(player->GetPosition()); path[1] = target;
|
||||
if (Distance(path[0], path[1]) > 0.1f)
|
||||
player->GetMotionMaster()->MoveSmoothPath(1, path, 2, false);
|
||||
ride.Moving = Distance(path[0], path[1]) > 0.1f; ride.PhaseElapsed = 0;
|
||||
ride.PointTimeout = 10000;
|
||||
if (boarding) ++ride.Point; else --ride.Point;
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::Update(uint32 diff)
|
||||
{
|
||||
for (auto itr = _recordings.begin(); itr != _recordings.end(); )
|
||||
{
|
||||
Player* player = ObjectAccessor::FindConnectedPlayer(ObjectGuid(HighGuid::Player, itr->first));
|
||||
if (!player || !player->IsInWorld()) { itr = _recordings.erase(itr); continue; }
|
||||
Recording& recording = itr->second;
|
||||
recording.Timer += diff;
|
||||
if (recording.Timer >= 100)
|
||||
{
|
||||
recording.Timer = 0;
|
||||
if (recording.Error.empty() && !Sample(player, recording, false) && !recording.Error.empty())
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "TRANSPORT_RECORD_FAILED recorder=%u detail='%s'", itr->first, recording.Error.c_str());
|
||||
// Keep the buffer and diagnostic; no further automatic samples.
|
||||
recording.Timer = 0;
|
||||
}
|
||||
}
|
||||
++itr;
|
||||
}
|
||||
for (auto itr = _rides.begin(); itr != _rides.end(); )
|
||||
{
|
||||
WorldSession* session = sWorld->FindClientlessSession(itr->first);
|
||||
if (!session || sWorld->GetClientlessPlayerState(itr->first) != ClientlessPlayerState::Online)
|
||||
{ itr = _rides.erase(itr); continue; }
|
||||
Player* player = session->GetPlayer();
|
||||
if (player) UpdateRide(player, itr->second, diff);
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::Command(ChatHandler* handler, char const* args)
|
||||
{
|
||||
std::istringstream input(args ? args : "");
|
||||
std::string command; input >> command;
|
||||
Player* gm = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
|
||||
if (command == "reload") Reload();
|
||||
if (!EnsureLoaded())
|
||||
{
|
||||
handler->SendSysMessage("Transport schema unavailable. Apply sql/custom/world/2026_10_25_00_clientless_transport.sql to the test world DB.");
|
||||
return true;
|
||||
}
|
||||
auto say = [&](std::string const& text) { handler->SendSysMessage(text.c_str()); };
|
||||
if (command == "services")
|
||||
{
|
||||
uint32 requested = 0;
|
||||
input >> std::ws;
|
||||
if (input.peek() != std::char_traits<char>::eof())
|
||||
{
|
||||
std::string extra;
|
||||
if (!(input >> requested) || !requested || (input >> extra))
|
||||
{ say("Usage: .clientless transport services [spawnId]"); return true; }
|
||||
}
|
||||
QueryResult rows = requested
|
||||
? WorldDatabase.PQuery("SELECT guid,entry FROM transports WHERE guid=%u", requested)
|
||||
: WorldDatabase.Query("SELECT guid,entry FROM transports ORDER BY guid");
|
||||
if (!rows) { say("No configured native transport service found."); return true; }
|
||||
do
|
||||
{
|
||||
Field* fields = rows->Fetch();
|
||||
uint32 spawn = fields[0].GetUInt32(), entry = fields[1].GetUInt32();
|
||||
auto info = sObjectMgr->GetGameObjectTemplate(entry);
|
||||
auto route = sTransportMgr->GetTransportTemplate(entry);
|
||||
if (!info || !route) continue;
|
||||
uint32 stops = 0;
|
||||
for (auto const& leg : route->PathLegs)
|
||||
for (auto const& segment : leg.Segments)
|
||||
if (segment.Delay) ++stops;
|
||||
handler->PSendSysMessage("Service spawn=%u entry=%u name='%s' periodMs=%u stops=%u legs=%u instanced=%u",
|
||||
spawn, entry, info->name.c_str(), route->TotalPathTime, stops, uint32(route->PathLegs.size()), route->InInstance ? 1u : 0u);
|
||||
if (!requested) continue;
|
||||
uint32 dock = 0;
|
||||
for (auto const& leg : route->PathLegs)
|
||||
for (auto const& segment : leg.Segments)
|
||||
{
|
||||
if (!segment.Delay) continue;
|
||||
TransportMovementState state;
|
||||
size_t index;
|
||||
auto position = route->ComputePosition(segment.SegmentEndArrivalTimestamp, &state, &index);
|
||||
if (!position || state != TransportMovementState::WaitingOnPauseWaypoint || index >= route->PathLegs.size())
|
||||
continue;
|
||||
handler->PSendSysMessage(" Dock %u: map=%u pose=(%.3f %.3f %.3f %.3f) arrivalMs=%u pauseMs=%u",
|
||||
++dock, route->PathLegs[index].MapId, position->GetPositionX(), position->GetPositionY(),
|
||||
position->GetPositionZ(), position->GetOrientation(), segment.SegmentEndArrivalTimestamp, segment.Delay);
|
||||
}
|
||||
if (!dock) say(" No scheduled boarding pauses. Do not create a regular passenger edge without a verified stop.");
|
||||
} while (rows->NextRow());
|
||||
return true;
|
||||
}
|
||||
if (command == "reload")
|
||||
{
|
||||
Reload(); sClientlessTravelGraphMgr->Reload();
|
||||
handler->PSendSysMessage("Transport data reloaded: %u stops. Active rides retain their captured definitions.", uint32(_stops.size()));
|
||||
return true;
|
||||
}
|
||||
if (command == "node")
|
||||
{
|
||||
std::string node; input >> node;
|
||||
uint32 map = 0; Position position;
|
||||
if (!LoadNode(node, map, position))
|
||||
say("Transport node lookup failed: specify an existing enabled node.");
|
||||
else handler->PSendSysMessage("Transport node '%s': map=%u position=(%.3f %.3f %.3f %.3f)",
|
||||
node.c_str(), map, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), position.GetOrientation());
|
||||
return true;
|
||||
}
|
||||
if (command == "status" || command == "cancel" || command == "travel")
|
||||
{
|
||||
uint32 guid = 0; input >> guid;
|
||||
if (!guid) { say("Usage: .clientless transport status|cancel <guid> OR travel <guid> <destinationNode>"); return true; }
|
||||
if (command == "cancel")
|
||||
{
|
||||
auto const* status = GetStatus(guid);
|
||||
if (!status || status->Phase == ClientlessTransportPhase::Arrived || status->Phase == ClientlessTransportPhase::Failed)
|
||||
say("No active transport ride to cancel.");
|
||||
else { Cancel(guid); say("Cancellation requested; a passenger remains attached until a safe recorded stop."); }
|
||||
return true;
|
||||
}
|
||||
if (command == "travel")
|
||||
{
|
||||
std::string node; input >> node;
|
||||
if (!Safe(node)) { say("Specify a valid destination node."); return true; }
|
||||
ClientlessAction action; action.Name = "travel_to_node";
|
||||
action.Parameters["node"] = node; action.Parameters["hard_destination"] = "1";
|
||||
say(sClientlessPlayerMgr->QueueAction(guid, std::move(action)) ? "Travel action queued through the regular graph executor." : "Character is not CPC managed.");
|
||||
return true;
|
||||
}
|
||||
auto itr = _rides.find(guid);
|
||||
if (itr == _rides.end()) say("No transport ride state for this character.");
|
||||
else handler->PSendSysMessage("Transport GUID %u: phase=%s from='%s' to='%s' spawn=%u point=%u elapsedMs=%u abort=%u detail='%s'",
|
||||
guid, PhaseName(itr->second.Status.Phase), itr->second.From.Node.c_str(), itr->second.To.Node.c_str(), itr->second.From.Spawn,
|
||||
uint32(itr->second.Point), itr->second.Elapsed, itr->second.Abort ? 1u : 0u, itr->second.Status.Detail.c_str());
|
||||
return true;
|
||||
}
|
||||
if (command == "stops")
|
||||
{
|
||||
for (auto const& pair : _stops)
|
||||
{
|
||||
auto const& s = pair.second;
|
||||
handler->PSendSysMessage("Stop '%s': entry=%u spawn=%u map=%u points=%u length=%.2f stand='%s' period=%ums dock=(%.3f %.3f %.3f)",
|
||||
s.Node.c_str(), s.Entry, s.Spawn, s.Map, uint32(s.Points.size()), PathLength(s), s.Stand.c_str(), s.Period,
|
||||
s.Dock.GetPositionX(), s.Dock.GetPositionY(), s.Dock.GetPositionZ());
|
||||
}
|
||||
handler->PSendSysMessage("Recorded transport stops: %u", uint32(_stops.size()));
|
||||
return true;
|
||||
}
|
||||
if (command == "forget")
|
||||
{
|
||||
std::string node; input >> node;
|
||||
if (!Safe(node)) { say("Usage: .clientless transport forget <node> (removes only the recorded dock transition)"); return true; }
|
||||
WorldDatabaseTransaction trans = WorldDatabase.BeginTransaction();
|
||||
trans->PAppend("DELETE FROM vcore_clientless_transport_point WHERE node_key='%s'", node.c_str());
|
||||
trans->PAppend("DELETE FROM vcore_clientless_transport_stop WHERE node_key='%s'", node.c_str());
|
||||
WorldDatabase.DirectCommitTransaction(trans); Reload(); sClientlessTravelGraphMgr->Reload();
|
||||
say("Dock recording removed. Shared node, ground routes and deck stands retained; incomplete transport connections are not planned.");
|
||||
return true;
|
||||
}
|
||||
if (command == "link")
|
||||
{
|
||||
std::string from, to, type, both; float cost = 0;
|
||||
input >> from >> to >> type >> cost >> both;
|
||||
if (!Safe(from) || !Safe(to) || !IsTransportEdge(type) || !std::isfinite(cost) || cost <= 0 || cost > 100000000 ||
|
||||
(!both.empty() && both != "both") || !HasConnection(from, to))
|
||||
{ say("Usage: .clientless transport link <fromNode> <toNode> <ship|zeppelin|tram|elevator> <graphCost> [both]. Both stops must use the same spawn and deck stand."); return true; }
|
||||
auto const& a = _stops.at(from);
|
||||
GameObjectTemplate const* info = sObjectMgr->GetGameObjectTemplate(a.Entry);
|
||||
if (!info || ((type == "tram" || type == "elevator") ? info->type != GAMEOBJECT_TYPE_TRANSPORT : info->type != GAMEOBJECT_TYPE_MO_TRANSPORT))
|
||||
{ say("Edge type does not match the recorded GameObject transport implementation."); return true; }
|
||||
if (WorldDatabase.PQuery("SELECT id FROM vcore_clientless_travel_edge WHERE from_node_key='%s' AND to_node_key='%s' AND edge_type='%s'", from.c_str(), to.c_str(), type.c_str()))
|
||||
{ say("This directed transport edge already exists; it was not overwritten."); return true; }
|
||||
WorldDatabase.DirectPExecute("INSERT INTO vcore_clientless_travel_edge (from_node_key,to_node_key,edge_type,cost,bidirectional,enabled,comment) "
|
||||
"VALUES ('%s','%s','%s',%.6f,%u,1,'CPC recorded native transport')", from.c_str(), to.c_str(), type.c_str(), cost, both == "both" ? 1u : 0u);
|
||||
sClientlessTravelGraphMgr->Reload(); say("Transport edge created. The shared stop nodes are used in both directions.");
|
||||
return true;
|
||||
}
|
||||
if (command == "stand")
|
||||
{
|
||||
std::string operation, name; input >> operation;
|
||||
if (operation == "list")
|
||||
{
|
||||
uint32 entry = 0; input >> entry;
|
||||
if (!entry && gm && gm->GetTransport())
|
||||
if (GameObject* go = gm->GetMap()->GetGameObject(gm->GetTransport()->GetTransportGUID())) entry = go->GetEntry();
|
||||
if (!entry) { say("Usage: .clientless transport stand list <transportEntry>"); return true; }
|
||||
QueryResult r = WorldDatabase.PQuery("SELECT name,x,y,z,o FROM vcore_clientless_transport_stand WHERE transport_entry=%u ORDER BY name", entry);
|
||||
if (r) do { Field* f = r->Fetch(); handler->PSendSysMessage("Stand '%s' entry=%u local=(%.3f %.3f %.3f %.3f)", f[0].GetString().c_str(), entry, f[1].GetFloat(), f[2].GetFloat(), f[3].GetFloat(), f[4].GetFloat()); } while (r->NextRow());
|
||||
else say("No stands recorded for this entry.");
|
||||
return true;
|
||||
}
|
||||
if (operation == "remove")
|
||||
{
|
||||
uint32 entry = 0; input >> entry >> name;
|
||||
if (!entry || !Safe(name)) { say("Usage: .clientless transport stand remove <entry> <name>"); return true; }
|
||||
if (WorldDatabase.PQuery("SELECT node_key FROM vcore_clientless_transport_stop WHERE transport_entry=%u AND stand_name='%s' LIMIT 1", entry, name.c_str()))
|
||||
{ say("Stand is used by a stop. Forget/re-record those transitions before removing it."); return true; }
|
||||
WorldDatabase.DirectPExecute("DELETE FROM vcore_clientless_transport_stand WHERE transport_entry=%u AND name='%s'", entry, name.c_str());
|
||||
say("Unused deck stand removed."); return true;
|
||||
}
|
||||
input >> name;
|
||||
if (operation != "add" || !Safe(name) || !gm || !gm->GetTransport() || gm->IsFlying() || gm->IsFalling())
|
||||
{ say("Stand aboard the intended transport, then use .clientless transport stand add <name>."); return true; }
|
||||
GameObject* go = gm->GetMap()->GetGameObject(gm->GetTransport()->GetTransportGUID());
|
||||
if (!go || !go->ToTransportBase()) { say("Transport GameObject not found."); return true; }
|
||||
float x, y, z, o; gm->GetPosition(x, y, z, o); go->ToTransportBase()->CalculatePassengerOffset(x, y, z, &o);
|
||||
if (std::fabs(x) > 100 || std::fabs(y) > 100 || std::fabs(z) > 100) { say("Stand outside supported deck bounds."); return true; }
|
||||
if (WorldDatabase.PQuery("SELECT 1 FROM vcore_clientless_transport_stand WHERE transport_entry=%u AND name='%s'", go->GetEntry(), name.c_str()))
|
||||
{ say("Stand name already recorded; use a new name to preserve the existing point."); return true; }
|
||||
WorldDatabase.DirectPExecute("INSERT INTO vcore_clientless_transport_stand VALUES (%u,'%s',%.9f,%.9f,%.9f,%.9f)", go->GetEntry(), name.c_str(), x, y, z, o);
|
||||
handler->PSendSysMessage("Stand '%s' saved for entry=%u at local (%.3f %.3f %.3f %.3f).", name.c_str(), go->GetEntry(), x, y, z, o);
|
||||
return true;
|
||||
}
|
||||
if (command == "inspect" || command == "near")
|
||||
{
|
||||
uint32 spawn = 0, guid = 0;
|
||||
float radius = 50;
|
||||
if (command == "inspect") input >> spawn >> guid;
|
||||
else input >> radius >> guid;
|
||||
Player* observer = gm;
|
||||
if (!observer && guid)
|
||||
if (WorldSession* session = sWorld->FindClientlessSession(guid)) observer = session->GetPlayer();
|
||||
if (!observer || !observer->IsInWorld()) { say("In console use: transport inspect <spawnId> <botGuid> OR transport near <radius> <botGuid>."); return true; }
|
||||
auto print = [&](GameObject* go)
|
||||
{
|
||||
if (!go || !go->IsTransport() || !go->IsInWorld() || !observer->IsInPhase(go)) return;
|
||||
TransportDockState state; go->GetTransportDockState(state);
|
||||
handler->PSendSysMessage("Transport '%s': entry=%u spawn=%u guid='%s' map=%u pos=(%.3f %.3f %.3f %.3f) distance=%.2f stopped=%u remainingPause=%u period=%u progress=%u",
|
||||
go->GetName().c_str(), go->GetEntry(), Spawn(go), go->GetGUID().ToString().c_str(), go->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ(), go->GetOrientation(),
|
||||
observer->GetExactDist(go), state.Stopped ? 1u : 0u, state.RemainingPause, state.Period, state.Progress);
|
||||
};
|
||||
if (command == "inspect")
|
||||
{
|
||||
GameObject* go = Resolve(observer, spawn);
|
||||
if (go) print(go); else say("Spawn is not loaded on the observer's current map.");
|
||||
}
|
||||
else
|
||||
{
|
||||
radius = std::isfinite(radius) ? std::max(1.0f, std::min(200.0f, radius)) : 50;
|
||||
for (auto const& pair : observer->GetMap()->GetGameObjectBySpawnIdStore())
|
||||
if (observer->GetExactDist(pair.second) <= radius) print(pair.second);
|
||||
QueryResult r = WorldDatabase.Query("SELECT guid FROM transports");
|
||||
if (r) do { GameObject* go = observer->GetMap()->GetTransport(ObjectGuid(HighGuid::Mo_Transport, r->Fetch()[0].GetUInt32())); if (go && observer->GetExactDist(go) <= radius) print(go); } while (r->NextRow());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (command == "record")
|
||||
{
|
||||
if (!gm || !gm->IsInWorld()) { say("Dock recording needs an in-game GM player."); return true; }
|
||||
uint32 guid = gm->GetGUID().GetCounter();
|
||||
std::string op; input >> op;
|
||||
if (op == "cancel") { _recordings.erase(guid); say("Your unsaved recording was cancelled."); return true; }
|
||||
if (op == "info")
|
||||
{
|
||||
auto itr = _recordings.find(guid);
|
||||
if (itr == _recordings.end()) say("No active recording.");
|
||||
else handler->PSendSysMessage("Recording node='%s' spawn=%u points=%u length=%.2f aboard=%u error='%s'", itr->second.Stop.Node.c_str(), itr->second.Stop.Spawn,
|
||||
uint32(itr->second.Stop.Points.size()), PathLength(itr->second.Stop), itr->second.Attached ? 1u : 0u, itr->second.Error.c_str());
|
||||
return true;
|
||||
}
|
||||
if (op == "start")
|
||||
{
|
||||
std::string node; uint32 spawn = 0; input >> node >> spawn;
|
||||
if (_recordings.count(guid)) { say("Finish or cancel your existing recording first."); return true; }
|
||||
Position position; uint32 map = 0;
|
||||
if (!Safe(node) || !spawn)
|
||||
{ say("Usage: .clientless transport record start <sharedNode> <spawnId>"); return true; }
|
||||
if (!LoadNode(node, map, position))
|
||||
{ handler->PSendSysMessage("Recording not started: node '%s' does not exist or is disabled.", node.c_str()); return true; }
|
||||
if (map != gm->GetMapId())
|
||||
{ handler->PSendSysMessage("Recording not started: node map=%u, recorder map=%u.", map, gm->GetMapId()); return true; }
|
||||
float const nodeDistance = Distance(*gm, position);
|
||||
if (nodeDistance > 2.0f)
|
||||
{ handler->PSendSysMessage("Recording not started: distance to node '%s' is %.3f yd (maximum 2.000); node=(%.3f %.3f %.3f), recorder=(%.3f %.3f %.3f).",
|
||||
node.c_str(), nodeDistance, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), gm->GetPositionX(), gm->GetPositionY(), gm->GetPositionZ()); return true; }
|
||||
if (gm->GetTransport())
|
||||
{ handler->PSendSysMessage("Recording not started: recorder is already a passenger of '%s'. Start on the platform, before boarding.",
|
||||
gm->GetTransport()->GetTransportGUID().ToString().c_str()); return true; }
|
||||
GameObject* go = Resolve(gm, spawn);
|
||||
if (!go)
|
||||
{ handler->PSendSysMessage("Recording not started: spawn %u is not loaded on map %u.", spawn, gm->GetMapId()); return true; }
|
||||
if (!go->ToTransportBase())
|
||||
{ handler->PSendSysMessage("Recording not started: spawn %u entry %u is not a supported transport.", spawn, go->GetEntry()); return true; }
|
||||
if (!gm->IsInPhase(go))
|
||||
{ say("Recording not started: recorder and transport are in different phases."); return true; }
|
||||
float const transportDistance = gm->GetExactDist(go);
|
||||
if (transportDistance > 100.0f)
|
||||
{ handler->PSendSysMessage("Recording not started: spawn %u is %.3f yd away (maximum 100.000); transport=(%.3f %.3f %.3f).",
|
||||
spawn, transportDistance, go->GetPositionX(), go->GetPositionY(), go->GetPositionZ()); return true; }
|
||||
TransportDockState state;
|
||||
if (!go->GetTransportDockState(state))
|
||||
{ handler->PSendSysMessage("Recording not started: no server dock state available for spawn %u entry %u.", spawn, go->GetEntry()); return true; }
|
||||
if (!state.Stopped)
|
||||
{ handler->PSendSysMessage("Recording not started: spawn %u is moving according to its animation; progress=%u/%u ms, remainingPause=%u ms, nodeDistance=%.3f yd, transportDistance=%.3f yd.",
|
||||
spawn, state.Progress, state.Period, state.RemainingPause, nodeDistance, transportDistance); return true; }
|
||||
if (WorldDatabase.PQuery("SELECT 1 FROM vcore_clientless_transport_stop WHERE node_key='%s'", node.c_str())) { say("Dock already recorded; use transport forget <node> explicitly before replacing it."); return true; }
|
||||
Recording recording;
|
||||
recording.Stop.Node = node; recording.Stop.Entry = go->GetEntry(); recording.Stop.Spawn = Spawn(go);
|
||||
recording.Stop.Map = map; recording.Stop.Period = state.Period; recording.Stop.Dock.Relocate(go->GetPosition());
|
||||
recording.Object = go->GetGUID(); recording.NodePosition = position;
|
||||
recording.Stop.Points.push_back({ false, position });
|
||||
_recordings[guid] = recording;
|
||||
say("Recording started. Walk normally onto this transport and to a deck stand during this stop. Then: transport record stop <standName>.");
|
||||
return true;
|
||||
}
|
||||
auto itr = _recordings.find(guid);
|
||||
if (itr == _recordings.end()) { say("No active recording."); return true; }
|
||||
if (op == "mark") { bool ok = Sample(gm, itr->second, true); say(ok ? "Transition point marked." : itr->second.Error); return true; }
|
||||
if (op == "stop")
|
||||
{
|
||||
std::string stand, error; input >> stand;
|
||||
if (Save(gm, stand, error)) { sClientlessTravelGraphMgr->Reload(); say("Dock transition and local deck stand saved. Record the other stop on the same physical transport, ending at the same stand."); }
|
||||
else say(error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
say("Transport tools: services [spawn], node <key>, near [radius], inspect <spawn> [botGuid], stops, stand add|list, record start|mark|info|stop|cancel, link <from> <to> <type> <cost> [both], forget <node>, reload, status|cancel <guid>, travel <guid> <node>.");
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* Vcore: recorded dock transitions and native clientless transport travel. */
|
||||
#ifndef VCORE_CLIENTLESS_TRANSPORT_MGR_H
|
||||
#define VCORE_CLIENTLESS_TRANSPORT_MGR_H
|
||||
|
||||
#include "Common.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "Position.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class Player;
|
||||
class GameObject;
|
||||
class ChatHandler;
|
||||
|
||||
struct ClientlessTransportPoint
|
||||
{
|
||||
bool OnTransport = false;
|
||||
Position Pos;
|
||||
};
|
||||
|
||||
struct ClientlessTransportStop
|
||||
{
|
||||
std::string Node;
|
||||
uint32 Entry = 0;
|
||||
uint32 Spawn = 0;
|
||||
uint32 Map = 0;
|
||||
uint32 Period = 0;
|
||||
Position Dock;
|
||||
std::string Stand;
|
||||
std::vector<ClientlessTransportPoint> Points;
|
||||
};
|
||||
|
||||
enum class ClientlessTransportPhase : uint8
|
||||
{
|
||||
Waiting, Boarding, Riding, Leaving, Arrived, Failed
|
||||
};
|
||||
|
||||
struct ClientlessTransportStatus
|
||||
{
|
||||
ClientlessTransportPhase Phase = ClientlessTransportPhase::Failed;
|
||||
std::string Detail;
|
||||
bool Cancelled = false;
|
||||
};
|
||||
|
||||
class TC_GAME_API ClientlessTransportMgr
|
||||
{
|
||||
public:
|
||||
static ClientlessTransportMgr* instance();
|
||||
bool Reload();
|
||||
bool HasConnection(std::string const& from, std::string const& to);
|
||||
bool Start(Player* player, std::string const& from, std::string const& to, std::string& error);
|
||||
ClientlessTransportStatus const* GetStatus(uint32 guid) const;
|
||||
void Cancel(uint32 guid);
|
||||
void Update(uint32 diff);
|
||||
bool Command(ChatHandler* handler, char const* args);
|
||||
static bool IsTransportEdge(std::string const& type);
|
||||
|
||||
private:
|
||||
struct Recording
|
||||
{
|
||||
ClientlessTransportStop Stop;
|
||||
ObjectGuid Object;
|
||||
Position NodePosition;
|
||||
bool Attached = false;
|
||||
std::string Error;
|
||||
uint32 Timer = 0;
|
||||
};
|
||||
struct Ride
|
||||
{
|
||||
ClientlessTransportStatus Status;
|
||||
ClientlessTransportStop From, To;
|
||||
std::size_t Point = 0;
|
||||
uint32 Elapsed = 0;
|
||||
uint32 PhaseElapsed = 0;
|
||||
uint32 RequiredPause = 0;
|
||||
uint32 PointTimeout = 0;
|
||||
bool Moving = false;
|
||||
bool Abort = false;
|
||||
};
|
||||
bool EnsureLoaded();
|
||||
bool Sample(Player* player, Recording& recording, bool force);
|
||||
bool Save(Player* player, std::string const& stand, std::string& error);
|
||||
void UpdateRide(Player* player, Ride& ride, uint32 diff);
|
||||
void SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail);
|
||||
GameObject* FindObject(Player* player, ClientlessTransportStop const& stop) const;
|
||||
bool AtDock(Player* player, GameObject* object, ClientlessTransportStop const& stop, uint32 requiredPause) const;
|
||||
|
||||
bool _loaded = false;
|
||||
bool _available = false;
|
||||
std::unordered_map<std::string, ClientlessTransportStop> _stops;
|
||||
std::unordered_map<uint32, Recording> _recordings;
|
||||
std::unordered_map<uint32, Ride> _rides;
|
||||
};
|
||||
|
||||
#define sClientlessTransportMgr ClientlessTransportMgr::instance()
|
||||
#endif
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Log.h"
|
||||
@@ -100,6 +101,13 @@ bool ClientlessTravelGraphMgr::Reload()
|
||||
if (edge.Cost <= 0.0f)
|
||||
edge.Cost = 1.0f;
|
||||
|
||||
if (ClientlessTransportMgr::IsTransportEdge(edge.Type) &&
|
||||
!sClientlessTransportMgr->HasConnection(edge.FromNode, edge.ToNode))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "TRANSPORT_EDGE_UNAVAILABLE edge=%u from='%s' to='%s'",
|
||||
edge.DatabaseId, edge.FromNode.c_str(), edge.ToNode.c_str());
|
||||
continue;
|
||||
}
|
||||
_edges[edge.FromNode].push_back(edge);
|
||||
++_edgeCount;
|
||||
|
||||
@@ -428,11 +436,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
{
|
||||
std::string Node;
|
||||
bool LastWasBridge = false;
|
||||
// Preserve each validated start entry until same-edge exits are scored.
|
||||
std::string EntryKey;
|
||||
};
|
||||
|
||||
auto stateKey = [](State const& state)
|
||||
{
|
||||
return state.Node + (state.LastWasBridge ? "\x1f" "B" : "\x1f" "R");
|
||||
return state.Node + (state.LastWasBridge ? "\x1f" "B" : "\x1f" "R") + state.EntryKey;
|
||||
};
|
||||
|
||||
struct QueueItem
|
||||
@@ -473,6 +483,32 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
};
|
||||
std::unordered_map<std::string, VirtualEntry> virtualRoots;
|
||||
|
||||
// Exact road-section cost from recorded arclength; handles reverse
|
||||
// travel as well as virtual entry + exit within the SAME edge.
|
||||
auto roadSectionCost = [&](uint32 edgeId, bool reverse, uint32 begin,
|
||||
uint32 end, float fullCost) -> float
|
||||
{
|
||||
auto itr = _edgePoints.find(edgeId);
|
||||
if (itr == _edgePoints.end() || end >= itr->second.size() || begin > end)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
std::vector<ClientlessTravelEdgePoint> ordered = itr->second;
|
||||
if (reverse)
|
||||
std::reverse(ordered.begin(), ordered.end());
|
||||
float total = 0.0f, section = 0.0f;
|
||||
for (uint32 i = 1; i < ordered.size(); ++i)
|
||||
{
|
||||
float dx = ordered[i].X - ordered[i - 1].X;
|
||||
float dy = ordered[i].Y - ordered[i - 1].Y;
|
||||
float dz = ordered[i].Z - ordered[i - 1].Z;
|
||||
float const segment = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
total += segment;
|
||||
if (i > begin && i <= end)
|
||||
section += segment;
|
||||
}
|
||||
return total > 0.1f ? fullCost * section / total
|
||||
: std::numeric_limits<float>::infinity();
|
||||
};
|
||||
|
||||
uint32 startCount = 0;
|
||||
for (auto const& pair : _nodes)
|
||||
{
|
||||
@@ -628,14 +664,17 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
continue;
|
||||
}
|
||||
|
||||
State state{ candidate.Edge.ToNode, false };
|
||||
State state{ candidate.Edge.ToNode, false,
|
||||
":entry:" + std::to_string(candidate.Edge.DatabaseId) +
|
||||
(candidate.Edge.Reverse ? "R:" : "F:") + std::to_string(candidate.Index) };
|
||||
std::string key = stateKey(state);
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + candidate.Edge.ToNode))
|
||||
continue;
|
||||
float remainingFraction = candidate.Edge.Reverse
|
||||
? float(candidate.Index) / float(candidate.Count - 1)
|
||||
: float(candidate.Count - candidate.Index - 1) / float(candidate.Count - 1);
|
||||
float newCost = (alreadyThere ? 0.0f : nav.PathLength) + candidate.Edge.Cost * remainingFraction;
|
||||
uint32 const startIndex = candidate.Edge.Reverse
|
||||
? candidate.Count - 1 - candidate.Index : candidate.Index;
|
||||
float newCost = (alreadyThere ? 0.0f : nav.PathLength) +
|
||||
roadSectionCost(candidate.Edge.DatabaseId, candidate.Edge.Reverse,
|
||||
startIndex, candidate.Count - 1, candidate.Edge.Cost);
|
||||
auto old = distance.find(key);
|
||||
if (old != distance.end() && old->second <= newCost)
|
||||
continue;
|
||||
@@ -690,10 +729,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
player->GetInstanceId(), a, b, NAV_GROUND);
|
||||
};
|
||||
|
||||
// VCORE_C820_VIRTUAL_EXIT_NODES: preflight a bounded, geographical
|
||||
// shortlist of directed recorded-road exits. Candidate generation does
|
||||
// not mutate nodes, edges or stored path points. Costs use travelled
|
||||
// arclength, not a flat per-bridge penalty or waypoint index fraction.
|
||||
// Select the closest eligible waypoint per directed road geometrically.
|
||||
// Future destination tiles need not be loaded: local access is estimated
|
||||
// here and validated by the executor only after reaching the exit.
|
||||
struct ExitCandidate
|
||||
{
|
||||
std::string FromNode;
|
||||
@@ -705,12 +743,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float DistanceSq = 0.0f;
|
||||
ClientlessTravelEdgePoint Point;
|
||||
};
|
||||
float const locationAccessRadius = locationGoal
|
||||
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
|
||||
std::vector<ExitCandidate> rawExitCandidates;
|
||||
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
|
||||
uint32 exitTests = 0, exitAccepted = 0;
|
||||
if (locationGoal)
|
||||
{
|
||||
float const radius = std::min(300.0f, locationGoal->AnchorRadius);
|
||||
float const radius = locationAccessRadius;
|
||||
std::unordered_set<std::string> scannedDirected;
|
||||
for (auto const& edgePair : _edges)
|
||||
{
|
||||
@@ -740,6 +779,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (totalLength < 0.1f)
|
||||
continue;
|
||||
float travelled = 0.0f;
|
||||
ExitCandidate nearest;
|
||||
nearest.DistanceSq = std::numeric_limits<float>::infinity();
|
||||
for (uint32 i = 1; i + 1 < ordered.size(); ++i)
|
||||
{
|
||||
float dx = ordered[i].X - ordered[i - 1].X;
|
||||
@@ -753,7 +794,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
dy = point.Y - locationGoal->Y;
|
||||
dz = point.Z - locationGoal->Z;
|
||||
float const dsq = dx * dx + dy * dy + dz * dz;
|
||||
if (dsq > radius * radius)
|
||||
if (dsq > radius * radius || dsq >= nearest.DistanceSq)
|
||||
continue;
|
||||
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
|
||||
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
|
||||
@@ -767,74 +808,30 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
candidate.RoadCost = edge.Cost * (travelled / totalLength);
|
||||
candidate.DistanceSq = dsq;
|
||||
candidate.Point = point;
|
||||
rawExitCandidates.push_back(std::move(candidate));
|
||||
candidate.LocalCost = std::sqrt(dsq);
|
||||
nearest = std::move(candidate);
|
||||
}
|
||||
if (std::isfinite(nearest.DistanceSq))
|
||||
rawExitCandidates.push_back(std::move(nearest));
|
||||
}
|
||||
}
|
||||
std::sort(rawExitCandidates.begin(), rawExitCandidates.end(),
|
||||
[](ExitCandidate const& a, ExitCandidate const& b)
|
||||
{ return a.DistanceSq < b.DistanceSq; });
|
||||
// Bound Detour preflight work, not the number of ground edges.
|
||||
constexpr uint32 MaxExitPreflights = 96;
|
||||
for (ExitCandidate candidate : rawExitCandidates)
|
||||
{
|
||||
if (exitTests >= MaxExitPreflights)
|
||||
break;
|
||||
++exitTests;
|
||||
Position fromPosition, goalPosition;
|
||||
fromPosition.Relocate(candidate.Point.X, candidate.Point.Y,
|
||||
candidate.Point.Z, candidate.Point.O);
|
||||
goalPosition.Relocate(locationGoal->X, locationGoal->Y,
|
||||
locationGoal->Z, 0.0f);
|
||||
if (candidate.DistanceSq <= 2.25f)
|
||||
candidate.LocalCost = 0.0f;
|
||||
else
|
||||
{
|
||||
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
|
||||
locationGoal->MapId, locationGoal->MapId,
|
||||
player->GetInstanceId(), fromPosition, goalPosition, NAV_GROUND);
|
||||
if (!probe.IsComplete() || probe.FinalDistance > 1.5f ||
|
||||
!std::isfinite(probe.PathLength) || probe.PathLength > 500.0f)
|
||||
continue;
|
||||
candidate.LocalCost = probe.PathLength;
|
||||
}
|
||||
exitsByFrom[candidate.FromNode].push_back(std::move(candidate));
|
||||
++exitAccepted;
|
||||
}
|
||||
for (ExitCandidate const& candidate : rawExitCandidates)
|
||||
exitsByFrom[candidate.FromNode].push_back(candidate);
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"C820_EXIT_PREFLIGHT candidates=%u tested=%u accepted=%u radius=%.1f",
|
||||
uint32(rawExitCandidates.size()), exitTests, exitAccepted, radius);
|
||||
"C821_EXIT_CANDIDATES directedEdges=%u radius=%.1f localCost=air-distance validation=on-arrival",
|
||||
uint32(rawExitCandidates.size()), radius);
|
||||
}
|
||||
// Exact road-section cost from recorded arclength; handles reverse
|
||||
// travel as well as virtual entry + exit within the SAME edge.
|
||||
auto roadSectionCost = [&](uint32 edgeId, bool reverse, uint32 begin,
|
||||
uint32 end, float fullCost) -> float
|
||||
{
|
||||
auto itr = _edgePoints.find(edgeId);
|
||||
if (itr == _edgePoints.end() || end >= itr->second.size() || begin > end)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
std::vector<ClientlessTravelEdgePoint> ordered = itr->second;
|
||||
if (reverse)
|
||||
std::reverse(ordered.begin(), ordered.end());
|
||||
float total = 0.0f, section = 0.0f;
|
||||
for (uint32 i = 1; i < ordered.size(); ++i)
|
||||
{
|
||||
float dx = ordered[i].X - ordered[i - 1].X;
|
||||
float dy = ordered[i].Y - ordered[i - 1].Y;
|
||||
float dz = ordered[i].Z - ordered[i - 1].Z;
|
||||
float const segment = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
total += segment;
|
||||
if (i > begin && i <= end)
|
||||
section += segment;
|
||||
}
|
||||
return total > 0.1f ? fullCost * section / total
|
||||
: std::numeric_limits<float>::infinity();
|
||||
};
|
||||
struct ExitSelection
|
||||
{
|
||||
bool Active = false;
|
||||
bool RootPartial = false;
|
||||
ExitCandidate Candidate;
|
||||
bool ForwardPartial = false;
|
||||
uint32 StartIndex = 0;
|
||||
ClientlessTravelEdgePoint EntryPoint;
|
||||
} bestExit;
|
||||
float bestGoalCost = std::numeric_limits<float>::infinity();
|
||||
std::string bestGoalState;
|
||||
@@ -915,23 +912,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float dy = candidateNode.Y - locationGoal->Y;
|
||||
float dz = candidateNode.Z - locationGoal->Z;
|
||||
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
if (direct <= locationGoal->AnchorRadius)
|
||||
// Use the same local access limit for nodes and road exits.
|
||||
// Otherwise a distant node can bypass a closer usable exit.
|
||||
if (direct <= locationAccessRadius)
|
||||
{
|
||||
Position a, b;
|
||||
a.Relocate(candidateNode.X, candidateNode.Y, candidateNode.Z, candidateNode.O);
|
||||
b.Relocate(locationGoal->X, locationGoal->Y, locationGoal->Z, 0.0f);
|
||||
float finalCost = std::numeric_limits<float>::infinity();
|
||||
if (direct <= 1.5f)
|
||||
finalCost = 0.0f;
|
||||
else
|
||||
{
|
||||
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
|
||||
candidateNode.MapId, candidateNode.MapId,
|
||||
player->GetInstanceId(), a, b, NAV_GROUND);
|
||||
if (probe.IsComplete() && probe.FinalDistance <= 1.5f &&
|
||||
std::isfinite(probe.PathLength) && probe.PathLength <= 500.0f)
|
||||
finalCost = probe.PathLength;
|
||||
}
|
||||
// Node anchors and virtual exits use the same estimate.
|
||||
// Neither is rejected because a remote tile is unloaded.
|
||||
std::string const finishKey = "finish:" + candidateNode.Key;
|
||||
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
||||
? std::numeric_limits<float>::infinity() : direct;
|
||||
if (current.Cost + finalCost < bestGoalCost)
|
||||
{
|
||||
bestGoalCost = current.Cost + finalCost;
|
||||
@@ -1081,13 +1070,34 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
if (entry.Edge->ToNode == from.Key)
|
||||
continue;
|
||||
uint32 const entryIndex = entry.Edge->Reverse
|
||||
? entry.PointCount - 1 - entry.Index : entry.Index;
|
||||
if (locationGoal)
|
||||
{
|
||||
auto exitGroup = exitsByFrom.find(entry.Edge->FromNode);
|
||||
if (exitGroup != exitsByFrom.end())
|
||||
for (ExitCandidate const& exit : exitGroup->second)
|
||||
{
|
||||
if (exit.EdgeId != entry.Edge->DatabaseId ||
|
||||
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
|
||||
continue;
|
||||
float const score = current.Cost + probe.PathLength + exit.LocalCost +
|
||||
roadSectionCost(exit.EdgeId, exit.Reverse, entryIndex,
|
||||
exit.EndIndex, entry.Edge->Cost);
|
||||
if (score >= bestGoalCost)
|
||||
continue;
|
||||
bestGoalCost = score;
|
||||
bestGoalState = currentKey;
|
||||
bestExit = { true, false, exit, true, entryIndex, *entry.Point };
|
||||
}
|
||||
}
|
||||
State next{ entry.Edge->ToNode, false };
|
||||
std::string const nextKey = stateKey(next);
|
||||
float remainingFraction = entry.Edge->Reverse
|
||||
? float(entry.Index) / float(entry.PointCount - 1)
|
||||
: float(entry.PointCount - entry.Index - 1) / float(entry.PointCount - 1);
|
||||
float const cost = current.Cost + 1000.0f + probe.PathLength +
|
||||
entry.Edge->Cost * remainingFraction;
|
||||
uint32 const begin = entry.Edge->Reverse
|
||||
? entry.PointCount - 1 - entry.Index : entry.Index;
|
||||
float const cost = current.Cost + probe.PathLength +
|
||||
roadSectionCost(entry.Edge->DatabaseId, entry.Edge->Reverse,
|
||||
begin, entry.PointCount - 1, entry.Edge->Cost);
|
||||
auto old = distance.find(nextKey);
|
||||
if (old != distance.end() && cost >= old->second)
|
||||
continue;
|
||||
@@ -1418,7 +1428,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bestExit.ForwardPartial)
|
||||
{
|
||||
ClientlessTravelRouteStep connector;
|
||||
connector.NodeKey = "virtual_ground_entry";
|
||||
connector.NodeType = "road";
|
||||
connector.EdgeType = "local_bridge";
|
||||
connector.MapId = bestExit.EntryPoint.MapId;
|
||||
connector.X = bestExit.EntryPoint.X;
|
||||
connector.Y = bestExit.EntryPoint.Y;
|
||||
connector.Z = bestExit.EntryPoint.Z;
|
||||
connector.O = bestExit.EntryPoint.O;
|
||||
route.push_back(std::move(connector));
|
||||
}
|
||||
ClientlessTravelRouteStep end;
|
||||
if (bestExit.ForwardPartial)
|
||||
end.GroundStartIndex = int32(bestExit.StartIndex);
|
||||
end.NodeKey = "virtual_ground_exit";
|
||||
end.NodeType = "road";
|
||||
end.EdgeType = "ground";
|
||||
|
||||
@@ -3414,6 +3414,7 @@ void World::UpdateSessions(uint32 diff)
|
||||
continue;
|
||||
}
|
||||
|
||||
session->CompleteClientlessTeleport();
|
||||
WorldSessionFilter updater(session);
|
||||
if (!session->Update(diff, updater))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user