Files
Viano-Core/src/server/game/World/ClientlessFlightMgr.cpp
T
Nico ba2a4e629c
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Recover stalled flights locally and fly rejected route approaches
2026-11-04 01:14:02 +01:00

1024 lines
57 KiB
C++

#include "AdventureAreaMgr.h"
#include "ClientlessTravelTelemetry.h"
#include "ClientlessFlightMgr.h"
#include <cstring>
#include "ClientlessLuaEngine.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "DatabaseEnv.h"
#include "BattlefieldMgr.h"
#include "Battlefield.h"
#include "Chat.h"
#include "Config.h"
#include "DBCStores.h"
#include "Log.h"
#include "Map.h"
#include "MoveSplineInit.h"
#include "MoveSpline.h"
#include "PhasingHandler.h"
#include "Player.h"
#include "Group.h"
#include "PathGenerator.h"
#include "Random.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "TerrainMgr.h"
#include "VMapFactory.h"
#include "IVMapManager.h"
#include "VMapManager2.h"
#include "World.h"
#include "WorldSession.h"
#include "WorldPacket.h"
#include "Opcodes.h"
#include "GameTime.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <sstream>
namespace
{
using namespace ClientlessFlightPath;
Point PointOf(Position const & p)
{
return
{
p.GetPositionX(), p.GetPositionY(), p.GetPositionZ()
};
}
float Infinity()
{
return std::numeric_limits < float >::infinity();
}
bool ClearRay(Player * player, Point const & a, Point const & b)
{
Map * map = player->GetMap();
float x, y, z;
return map->isInLineOfSight(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags(0)) && ! map->getObjectHitPos(player->GetPhaseShift(), a.X, a.Y, a.Z, b.X, b.Y, b.Z, x, y, z, 0);
}
bool OpenColumn(Player* player,float x,float y,Surface const& surface,float height=15)
{
if(!surface.Valid)return false;
float top=std::min(surface.Ceiling-5,surface.Ground+height);
if(top<surface.Ground+5.8f)return false;
for(auto offset:std::array<Point,5>{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}})
if(!ClearRay(player,{x+offset.X,y+offset.Y,surface.Ground+0.8f},{x+offset.X,y+offset.Y,top}))return false;
return true;
}
bool OpenColumnFrom(Player* player,float x,float y,float z,float height)
{
// A height query from MAX_HEIGHT can legitimately return a bridge,
// tree canopy or other collision surface far above the player. Such a
// surface is useful as an obstacle, but it is not the player's support
// floor. Test the actual take-off column from the player's feet.
if (!std::isfinite(z) || height < 5.0f) return false;
for (auto offset : std::array<Point,5>{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}})
if (!ClearRay(player,{x+offset.X,y+offset.Y,z+0.8f},
{x+offset.X,y+offset.Y,z+height})) return false;
return true;
}
char const * StateName(ClientlessFlightState state)
{
switch (state)
{
case ClientlessFlightState::Mounting : return "Mounting";
case ClientlessFlightState::Flying : return "Flying";
case ClientlessFlightState::Arrived : return "Arrived";
case ClientlessFlightState::Failed : return "Failed";
}
return "Unknown";
}
uint32 FlightType(SpellInfo const * spell)
{
if (spell) for (auto const & effect : spell->Effects) if (effect.ApplyAuraName == SPELL_AURA_MOUNTED) return uint32(effect.MiscValueB);
return 0;
}
}
ClientlessFlightMgr * ClientlessFlightMgr::instance()
{
static ClientlessFlightMgr mgr;
return & mgr;
}
void ClientlessFlightMgr::BeginPlanning(Player const* player)
{
_planningAreas.clear();_planningTerrain.clear();_planningPreferredZones.clear();
_planningPlayer=player;_planningEnabled=player && Enabled(player);
if(player)
{
_planningMinDistance=sConfigMgr->GetFloatDefault("Clientless.Flight.MinDistance",600);
std::istringstream list(sConfigMgr->GetStringDefault("Clientless.Flight.PreferredZones","210 4197"));
uint32 zone;while(list>>zone)_planningPreferredZones.insert(zone);
}
}
std::pair < uint32, uint32 > ClientlessFlightMgr::AreaZone(Player const * player, uint32 map, Position const & point) const
{
// Exact binary coordinates avoid repeated floating-point text formatting
// in the O(nodes^2) flight alternatives loop, and do not merge nearby points.
std::string key(sizeof(map)+3*sizeof(float),'\0');
float coordinates[]{point.GetPositionX(),point.GetPositionY(),point.GetPositionZ()};
std::memcpy(key.data(),&map,sizeof(map));
std::memcpy(key.data()+sizeof(map),coordinates,sizeof(coordinates));
auto found = _planningAreas.find(key);
if (found != _planningAreas.end()) return found->second;
auto & terrain = _planningTerrain[map];
if (! terrain) terrain = sTerrainMgr.LoadTerrain(map);
uint32 zone = 0, area = 0;
if (terrain) terrain->GetZoneAndAreaId(player->GetPhaseShift(), map, zone, area, point);
return _planningAreas.emplace(key, std::make_pair(area, zone)).first->second;
}
bool ClientlessFlightMgr::Enabled(Player const * player) const
{
auto const * profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
if (! sConfigMgr->GetBoolDefault("Clientless.Flight.Enabled", true) || ! profile || ! profile->CanFreeFly || player->GetSkillValue(SKILL_RIDING) < 225) return false;
for (auto const & mount : profile->FlightMounts) if (player->HasSpell(mount.DisplayId) && FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId))) return true;
return false;
}
bool ClientlessFlightMgr::Preferred(Player const * player, uint32 map, Position const & position) const
{
if (sAdventureAreaMgr->PreferredFlight(player,map,position)) return true;
if(_planningPlayer==player)return _planningPreferredZones.count(AreaZone(player,map,position).second)!=0;
std::istringstream list(sConfigMgr->GetStringDefault("Clientless.Flight.PreferredZones", "210 4197"));
uint32 zone = AreaZone(player, map, position).second, candidate;
while (list >> candidate) if (zone == candidate) return true;
return false;
}
float ClientlessFlightMgr::Estimate(Player const * player, uint32 map, Position const & from, Position const & to, bool recovery) const
{
auto const * entry = sMapStore.LookupEntry(map);
bool planning=_planningPlayer==player;
if (!(planning?_planningEnabled:Enabled(player)) || ! entry || entry->MapType != MAP_COMMON) return Infinity();
if ((map == 0 || map == 1) && ! player->HasSpell(90267)) return Infinity();
if (map == 571 && ! player->HasSpell(54197)) return Infinity();
uint32 const area = AreaZone(player, map, from).first;
if (auto const * data = sAreaTableStore.LookupEntry(area))
{
bool const wintergrasp = map == 571 && DBCManager::IsInArea(area, 4197);
bool const wintergraspFlight = sBattlefieldMgr->IsWintergraspFlightAllowed(map, area);
if (wintergrasp && !wintergraspFlight) return Infinity();
bool const dynamicRules = wintergraspFlight || data->World_pvp_ID != 0 ||(map == player->GetMapId() && area == player->GetAreaId() && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS));
if (! dynamicRules && ! data->GetMountFlags().HasFlag(AreaMountFlags::AllowFlyingMounts)) return Infinity();
}
// Unknown terrain permission is an estimate only; native mount capability
// and spell checks at the actual departure are mandatory.
bool const preferred = Preferred(player, map, from) || Preferred(player, map, to);
float const distance = Distance(PointOf(from), PointOf(to));
// Recovery keeps native eligibility, but has no RP
// distance threshold or zone preference. Start still validates the flight.
float minimum = recovery ? 0.0f : preferred ? 60.0f : std::max(100.0f, planning?_planningMinDistance:sConfigMgr->GetFloatDefault("Clientless.Flight.MinDistance", 600));
if (! std::isfinite(distance) || distance < minimum || (!recovery && distance > 20000)) return Infinity();
if (!sAdventureAreaMgr->AllowsSegment(player,map,from,to)) return Infinity();
return recovery ? distance : preferred ? 40 + distance * 0.4f : 180 + distance * 1.5f;
}
ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const
{
auto i = _records.find(guid);
return i == _records.end() ? nullptr : & i->second.Status;
}
void ClientlessFlightMgr::Clear(uint32 guid)
{
auto it=_records.find(guid);
if(it!=_records.end() && it->second.ApproachingDeparture)sClientlessTravelMgr->CancelTravel(guid);
if(it!=_records.end() && it->second.Status.State!=ClientlessFlightState::Arrived)it->second.Published->Aborted=true;
_records.erase(guid);
}
Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, float y) const
{
Map * map = player->GetMap();
uint32 terrain = PhasingHandler::GetTerrainMapId(player->GetPhaseShift(), map->GetId(), map->GetTerrain(), x, y);
auto grid=Trinity::ComputeGridCoord(x,y);
int32 gx=(MAX_NUMBER_OF_GRIDS-1)-grid.x_coord,gy=(MAX_NUMBER_OF_GRIDS-1)-grid.y_coord;
if(terrain!=map->GetTerrain()->GetId() && !map->GetTerrain()->HasChildTerrainGridFile(terrain,gx,gy))
terrain=map->GetTerrain()->GetId();
std::string key = std::to_string(terrain) + ":" + std::to_string(int(std::floor(x / 533.33333f))) + ":" + std::to_string(int(std::floor(y / 533.33333f)));
auto found = record.Tiles.find(key);
if (found == record.Tiles.end()) found = record.Tiles.emplace(key, TerrainMgr::ExistMapAndVMap(terrain, x, y)).first;
if (! found->second) return
{
};
map->LoadGrid(x, y);
// terrain/VMAP and dynamic objects before ray queries
float ground = map->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
float water = map->GetWaterLevel(player->GetPhaseShift(), x, y);
if (std::isfinite(water) && water > ground) ground = water;
float ceiling = map->GetTerrain()->GetMaxHeight(player->GetPhaseShift(), map->GetId(), x, y);
float terrainFloor=map->GetTerrain()->GetStaticHeight(player->GetPhaseShift(),map->GetId(),x,y,MAX_HEIGHT,false,MAX_HEIGHT);
if(std::isfinite(water) && water>terrainFloor)terrainFloor=water;
return
{
ground, ceiling, std::isfinite(ground) && std::isfinite(ceiling) && ground > INVALID_HEIGHT + 1 && ceiling > ground + 5,
std::isfinite(terrainFloor) && terrainFloor>INVALID_HEIGHT+1?terrainFloor:std::numeric_limits<float>::infinity()
};
}
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error,bool actionOwned)
{
bool started=Begin(player,target,error,false);
if(started)
if(auto found=_records.find(player->GetGUID().GetCounter());found!=_records.end())found->second.ActionOwned=actionOwned;
return started;
}
bool ClientlessFlightMgr::PrepareFollowingMount(Player* player)
{
if(!Enabled(player) || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() ||
player->GetTransport() || player->IsInFlight() || player->IsFalling() || player->IsInWater() ||
!player->IsOutdoors() || player->GetMap()->GetEntry()->MapType!=MAP_COMMON)return false;
if(player->IsMounted() && player->CanFly())return false;
if(player->HasUnitState(UNIT_STATE_CASTING))return true;
if(auto battlefield=sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(),player->GetZoneId()))
if(!battlefield->CanFlyIn())return false;
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
uint32 total=0,spell=0;
for(auto const& mount:profile->FlightMounts)
{
if(!player->HasSpell(mount.DisplayId))continue;
uint32 type=FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId));
auto capability=type?player->GetMountCapability(type):nullptr;
if(!capability || !(capability->Flags&MOUNT_CAPABILITY_FLAG_FLYING))continue;
uint32 weight=std::min(mount.Weight,uint32(10000));if(!weight)continue;
total+=weight;if(urand(1,total)<=weight)spell=mount.DisplayId;
}
if(!spell)return false;
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());player->StopMoving();
sClientlessLuaEngine->DismountGroundMount(player,"group flying mount");
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
return player->CastSpell(player,spell,false)==SPELL_CAST_OK;
}
void ClientlessFlightMgr::HoldFollowing(Player* player,Record& record)
{
bool changed=record.Moving || !player->HasUnitMovementFlag(MOVEMENTFLAG_FLYING);
if(record.Moving)player->StopMoving();
record.Moving=false;
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
if(player->GetByteValue(UNIT_FIELD_BYTES_1,UNIT_BYTES_1_OFFSET_ANIM_TIER)!=uint8(AnimTier::Fly))
{player->SetAnimTier(AnimTier::Fly);changed=true;}
if(!changed)return;
// Clientless players have no client sending movement heartbeats after a
// spline stop. Publish the stationary flight flags to nearby clients.
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);
}
bool ClientlessFlightMgr::Follow(Player* player,Position const& target,std::string& error)
{
if (!player || !sAdventureAreaMgr->AllowsPosition(player,player->GetMapId(),target))
{error="group flight target denied by area catalog";return false;}
auto found=_records.find(player->GetGUID().GetCounter());
if(found!=_records.end() && (found->second.Status.State==ClientlessFlightState::Mounting || found->second.Status.State==ClientlessFlightState::Flying))
{
auto& record=found->second;
if(!record.Status.FollowingPlayer || record.Returning){error="existing flight must finish safely";return false;}
if(!Trinity::IsValidMapCoord(target.GetPositionX(),target.GetPositionY(),target.GetPositionZ()))return false;
auto surface=Sample(player,record,target.GetPositionX(),target.GetPositionY());
if(!surface.Valid || target.GetPositionZ()<surface.Ground || target.GetPositionZ()+5>surface.Ceiling)return false;
record.Status.Target=target;record.Status.Landing=target;record.TargetUpdated=record.Elapsed;return true;
}
return Begin(player,target,error,true);
}
bool ClientlessFlightMgr::LandFollowing(Player* player)
{
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end())return false;
auto& record=found->second;
bool const shared=bool(record.Following);
if(!record.Status.FollowingPlayer && !shared)return false;
record.Following.reset();record.SharedWaitSince=0;
record.Published->Aborted=true;
record.Status.FollowingPlayer=false;
if(record.Status.State==ClientlessFlightState::Mounting)
{Fail(player,record,"group flight cancelled before takeoff");return true;}
auto surface=Sample(player,record,player->GetPositionX(),player->GetPositionY());
float solid=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),MAX_HEIGHT,true,MAX_HEIGHT);
auto sample=[&](float x,float y){return Sample(player,record,x,y);};
auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);};
Point landing{player->GetPositionX(),player->GetPositionY(),solid};
if(surface.Valid && std::fabs(surface.Ground-solid)<0.7f &&
solid<=player->GetPositionZ()+0.7f &&
Corridor(PointOf(*player),landing,landing,record.Settings,sample,ray,true))
{
record.Status.Landing.Relocate(player->GetPositionX(),player->GetPositionY(),solid,player->GetOrientation());
record.Status.Target=record.Status.Landing;record.Memory={};record.Following.reset();record.Status.Travelled=0;
player->StopMoving();record.Moving=false;
if(shared)
{
// This is a regroup landing, not arrival at the requested raid.
record.Start=record.Status.Landing;record.Returning=true;
record.AbortReason="shared flight regroup landing";record.Breadcrumbs.clear();
}
record.Status.Detail="landing after group flight";return true;
}
Fail(player,record,"no safe local landing; returning along validated flight trail");return true;
}
bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record)
{
Point current=PointOf(*player),goal=PointOf(record.Status.Target);
if(Distance(current,goal)<2){HoldFollowing(player,record);record.Status.Detail="holding flight formation";return true;}
auto sample=[&](float x,float y){return Sample(player,record,x,y);};
auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);};
auto settings=record.Settings;settings.Safety=1;
Point best;float score=std::numeric_limits<float>::infinity();
float heading=std::atan2(goal.Y-current.Y,goal.X-current.X);
float length=std::min(25.0f,Distance(current,goal));
for(float turn:{0.0f,0.4f,-0.4f,0.8f,-0.8f})for(float climb:{0.0f,4.0f})
{
auto point=Mix(current,goal,std::min(1.0f,length/std::max(0.1f,Distance(current,goal))));
if(turn!=0){float horizontal=Horizontal(current,point);point.X=current.X+std::cos(heading+turn)*horizontal;point.Y=current.Y+std::sin(heading+turn)*horizontal;}
point.Z+=climb;
Position candidatePos;candidatePos.Relocate(point.X,point.Y,point.Z);
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidatePos))continue;
if(!Corridor(current,point,goal,settings,sample,ray,true))continue;
float candidate=Distance(point,goal)+std::fabs(turn)*4+climb;
if(candidate<score){score=candidate;best=point;}
}
if(!std::isfinite(score)){HoldFollowing(player,record);record.Status.Detail="group flight corridor blocked; holding mounted";return false;}
if(record.Moving && player->IsSplineEnabled() && Distance(best,PointOf(record.Segment))<3)return true;
if(record.Breadcrumbs.size()<4096)record.Breadcrumbs.push_back(*player);
record.Segment.Relocate(best.X,best.Y,best.Z,player->GetOrientation());
Movement::MoveSplineInit move(player);move.MoveTo(best.X,best.Y,best.Z,false);move.SetFly();move.SetWalk(false);move.SetVelocity(player->GetSpeed(MOVE_FLIGHT));
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
if(move.Launch()<=0){record.Moving=false;return false;}
record.Moving=true;record.SegmentElapsed=0;++record.Status.Segments;record.Status.Detail="following moving group flight target";return true;
}
void ClientlessFlightMgr::Store(uint32 guid, Record&& record)
{
auto old=_records.find(guid);
if(old!=_records.end() && old->second.Published!=record.Published)
{
old->second.Published->Aborted=true;
if(old->second.Status.MapId==record.Status.MapId &&
old->second.Status.Target.GetExactDist(record.Status.Target)<1)
old->second.Published->Replacement=record.Published;
}
_records[guid]=std::move(record);
}
bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::string & error, bool following)
{
if (! Trinity::IsValidMapCoord(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
{
error = "invalid free-flight destination coordinates";
return false;
}
if (! player || ! player->IsInWorld() || ! Enabled(player) || ! player->IsAlive() || player->GetMap()->GetEntry()->MapType != MAP_COMMON || player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported() || player->IsInCombat() || player->IsInWater() || player->IsFalling())
{
error = "free flight unavailable in current player/location state";
if(player)ClientlessTravelTelemetry::Note(player,"flight-state-rejected",ClientlessTravelTelemetry::State(player));
return false;
}
if (!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,target))
{error="flight corridor denied by area catalog";return false;}
auto * vmaps = VMAP::VMapFactory::createOrGetVMapManager();
if (! vmaps->isLineOfSightCalcEnabled() || ! vmaps->isHeightCalcEnabled())
{
error = "free flight requires native VMAP height and collision checks";
return false;
}
if (auto * battlefield = sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(), player->GetZoneId())) if (! battlefield->CanFlyIn())
{
error = "battlefield currently forbids flying";
return false;
}
if(((player->GetMapId()==0 || player->GetMapId()==1) && !player->HasSpell(90267)) ||
(player->GetMapId()==571 && !player->HasSpell(54197)))
{error="native regional flying license missing";return false;}
Record record;
auto previous=_records.find(player->GetGUID().GetCounter());
if(previous!=_records.end() && previous->second.ApproachingDeparture)
record.Published=previous->second.Published;
record.Status.MapId = player->GetMapId();
record.Status.Target = target;
record.Start.Relocate(player);
record.LastPosition.Relocate(player);
record.Settings.Clearance = std::clamp(sConfigMgr->GetFloatDefault("Clientless.Flight.Clearance", 60), 20.0f, 200.0f);
auto origin = Sample(player, record, player->GetPositionX(), player->GetPositionY());
float support=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
bool supported=std::isfinite(support) && support>INVALID_HEIGHT+1 && std::fabs(support-player->GetPositionZ())<=3;
if(supported && (!player->IsOutdoors() || !origin.Valid || std::fabs(origin.Ground-player->GetPositionZ())>3 ||
!OpenColumn(player,player->GetPositionX(),player->GetPositionY(),origin)))
{
if(ApproachDeparture(player,record,following))return true;
error="departure-unavailable: no reachable open takeoff point";
ClientlessTravelTelemetry::Note(player,"flight-departure-rejected",error+" "+ClientlessTravelTelemetry::State(player));
return false;
}
auto const * profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
std::vector < std::pair < uint32, uint32 >> candidates;
uint32 total = 0;
for (auto const & mount : profile->FlightMounts)
{
if (! player->HasSpell(mount.DisplayId)) continue;
uint32 type = FlightType(sSpellMgr->GetSpellInfo(mount.DisplayId));
auto const * capability = player->GetMountCapability(type);
if (! capability || !(capability->Flags & MOUNT_CAPABILITY_FLAG_FLYING)) continue;
uint32 weight = std::min(mount.Weight, uint32(10000));
total += weight;
candidates.emplace_back(mount.DisplayId, weight);
}
if (! total)
{
error = "no learned profile mount with native flying capability here";
return false;
}
uint32 roll = urand(1, total), spell = 0;
for (auto const & candidate : candidates)
{
spell = candidate.first;
if (roll <= candidate.second) break;
roll -= candidate.second;
}
uint32 existingMount=0;
if(player->IsMounted() && player->CanFly())
for(auto const& candidate:candidates)if(player->HasAura(candidate.first)){existingMount=candidate.first;break;}
bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f &&
player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5});
float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
bool const localSupport=std::isfinite(feet) && feet>INVALID_HEIGHT+1 &&
std::fabs(feet-player->GetPositionZ())<=3;
bool const overheadSurface=origin.Valid && origin.Ground>player->GetPositionZ()+3;
bool const openFromFeet=overheadSurface && OpenColumnFrom(player,player->GetPositionX(),
player->GetPositionY(),player->GetPositionZ(),
std::min(record.Settings.Clearance,origin.Ground-player->GetPositionZ()-1.0f));
// Not falling/in water was verified above. If the global height sample
// selected a remote surface above us, a clear local vertical column is a
// valid departure even though that remote surface cannot be our floor.
bool const supportedDeparture=localSupport || openFromFeet;
if (!origin.Valid || (!airborne && !supportedDeparture))
{
if(localSupport && ApproachDeparture(player,record,following))return true;
error = "departure-unavailable: no supported open takeoff point";
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u localSupport=%u overhead=%u openFromFeet=%u mounted=%u canFly=%u",
player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,
origin.Valid?1:0,localSupport?1:0,overheadSurface?1:0,openFromFeet?1:0,
player->IsMounted()?1:0,player->CanFly()?1:0);
return false;
}
if(openFromFeet)
ClientlessTravelTelemetry::Note(player,"flight-departure-overhead-surface",
"z="+std::to_string(player->GetPositionZ())+" surface="+std::to_string(origin.Ground)+
" feet="+std::to_string(feet));
bool foundLanding = false;
std::array<uint32,5> landingRejected{}; // surface, water, column, MMAP, area
auto landingSummary=[&]()
{
std::string detail="rejected surface/water/column/mmap/area=";
for(auto count:landingRejected)detail+=std::to_string(count)+"/";
ClientlessTravelTelemetry::Note(player,"flight-landing-search",detail);
};
if(following)
{
auto surface=Sample(player,record,target.GetPositionX(),target.GetPositionY());
if(!surface.Valid || target.GetPositionZ()<surface.Ground || target.GetPositionZ()+5>surface.Ceiling)
{error="group flight target outside verified flight bounds";return false;}
record.Status.Landing=target;record.Status.FollowingPlayer=true;foundLanding=true;
}
if(!following) for (float radius :
{
0.0f, 6.0f, 12.0f, 20.0f, 40.0f, 80.0f, 120.0f, 180.0f
})
{
for (unsigned i = 0; i <(radius == 0 ? 1u : 12u); ++ i)
{
float angle = float(i) * 6.283185307f / 12;
float x = target.GetPositionX() + radius * std::cos(angle), y = target.GetPositionY() + radius * std::sin(angle);
auto landing = Sample(player, record, x, y);
if (! landing.Valid){++landingRejected[0];continue;}
float solid = player->GetMap()->GetHeight(player->GetPhaseShift(), x, y, MAX_HEIGHT, true, MAX_HEIGHT);
if (landing.Ground > solid + 0.7f){++landingRejected[1];continue;}
// water is no solid touchdown
if(!OpenColumn(player,x,y,landing,record.Settings.Clearance)){++landingRejected[2];continue;}
Position candidate;
candidate.Relocate(x, y, landing.Ground, target.GetOrientation());
if (radius > 0 || std::fabs(landing.Ground - target.GetPositionZ()) > 0.7f)
{
auto path = PathGenerator::AnalyzePositions(player->GetMapId(), player->GetMapId(), player->GetInstanceId(), candidate, target, NAV_GROUND);
if (! path.IsComplete() || path.FinalDistance > 1.5f || path.PathLength > 600){++landingRejected[3];continue;}
}
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),candidate,target)){++landingRejected[4];continue;}
ClientlessTravelTelemetry::Note(player,"flight-landing","radius="+std::to_string(radius)+" xyz="+std::to_string(x)+","+std::to_string(y)+","+std::to_string(landing.Ground));
record.Status.Landing = candidate;
foundLanding = true;
break;
}
if (foundLanding) break;
}
landingSummary();
if (! foundLanding)
{
error = "no free solid landing with a validated target approach";
ClientlessTravelTelemetry::Note(player,"flight-landing-rejected",error);
return false;
}
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
player->StopMoving();
if(!existingMount)
{
sClientlessLuaEngine->DismountGroundMount(player, "free-flight-takeoff");
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
}
record.Status.State = ClientlessFlightState::Mounting;
record.Status.MountSpell = existingMount ? existingMount : spell;
record.Status.Detail = "waiting for native flying mount";
record.Published->Landing=PointOf(record.Status.Landing);
record.Published->LandingReady=true;
if(!following)if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID())
{
auto source=_records.find(group->GetLeaderGUID().GetCounter());
if(source!=_records.end() && source->second.Status.MapId==player->GetMapId() &&
source->second.Status.Target.GetExactDist(target)<1 && !source->second.Published->Aborted)
{
// The leader may still be walking to takeoff and have no landing
// yet. Its published trail survives that preparation. Bind the
// verified landing lazily rather than copying an unset Position.
record.Following=source->second.Published;
std::vector<ObjectGuid> members;for(auto const& member:group->GetMemberSlots())if(member.guid!=group->GetLeaderGUID())members.push_back(member.guid);
std::sort(members.begin(),members.end());auto slot=std::find(members.begin(),members.end(),player->GetGUID())-members.begin();
record.FormationDelay=3500+uint32(slot)*600; // native shared corridor, staggered single-file departure
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u landingReady=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter(),record.Following->LandingReady?1u:0u);
}
}
if(airborne)
{
// A resumed hover is not a supported return/landing point. Use the
// verified ground below it only when the full descent column is clear.
FindReturnLanding(player,record);
record.Status.MountSpell=existingMount;record.Status.State=ClientlessFlightState::Flying;
record.Memory.Departed=player->GetPositionZ()-origin.Ground>record.Settings.Safety;
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RESUME_AIRBORNE guid=%u ground=%.3f z=%.3f",player->GetGUID().GetCounter(),origin.Ground,player->GetPositionZ());
Store(player->GetGUID().GetCounter(),std::move(record));return true;
}
Store(player->GetGUID().GetCounter(),std::move(record));
if (!existingMount && player->CastSpell(player, spell, false) != SPELL_CAST_OK)
{
Fail(player, _records.at(player->GetGUID().GetCounter()), "native mount spell rejected");
error = "native mount spell rejected";
return false;
}
ClientlessTravelTelemetry::Note(player,"flight-start","spell="+std::to_string(spell)+" target="+std::to_string(target.GetPositionX())+","+std::to_string(target.GetPositionY())+","+std::to_string(target.GetPositionZ())+" "+ClientlessTravelTelemetry::State(player));
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_START guid=%u map=%u spell=%u target=(%.2f %.2f %.2f)", player->GetGUID().GetCounter(), player->GetMapId(), spell, target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
return true;
}
bool ClientlessFlightMgr::FindReturnLanding(Player* player,Record& record)
{
auto sample=[&](float x,float y){return Sample(player,record,x,y);};
auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);};
Point current=PointOf(*player);
for(float radius:{0.0f,8.0f,16.0f,24.0f,40.0f})for(unsigned i=0;i<(radius==0?1u:8u);++i)
{
float angle=float(i)*0.785398163f;
float x=current.X+radius*std::cos(angle),y=current.Y+radius*std::sin(angle);
auto surface=sample(x,y);Point goal{x,y,surface.Ground};
if(!surface.Valid || Distance(current,goal)>80 || !OpenColumn(player,x,y,surface))continue;
Position candidate;candidate.Relocate(x,y,surface.Ground,player->GetOrientation());
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidate) ||
!Corridor(current,goal,goal,record.Settings,sample,ray,false))continue;
record.Start=candidate;
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RETURN_LANDING guid=%u target=(%.3f %.3f %.3f)",player->GetGUID().GetCounter(),x,y,surface.Ground);
return true;
}
return false;
}
bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool following)
{
// Only called after a real support surface was confirmed at the feet.
// Walk out from under the upper floor; never raise the player through it.
std::array<uint32,6> rejected{}; // surface, indoor, area, column, MMAP, movement
auto summary=[&]()
{
std::string detail="rejected surface/indoor/area/column/mmap/movement=";
for(auto count:rejected)detail+=std::to_string(count)+"/";
ClientlessTravelTelemetry::Note(player,"flight-departure-search",detail);
};
float heading=std::atan2(record.Status.Target.GetPositionY()-player->GetPositionY(),record.Status.Target.GetPositionX()-player->GetPositionX());
for(float radius:{8.0f,16.0f,24.0f,40.0f,64.0f,96.0f,144.0f,200.0f})for(float turn:{0.0f,0.785398f,-0.785398f,1.570796f,-1.570796f,2.356194f,-2.356194f,3.141593f})
{
float x=player->GetPositionX()+radius*std::cos(heading+turn),y=player->GetPositionY()+radius*std::sin(heading+turn);
auto surface=Sample(player,record,x,y);
if(!surface.Valid){++rejected[0];continue;}
PositionFullTerrainStatus terrain;
player->GetMap()->GetFullTerrainStatusForPosition(player->GetPhaseShift(),x,y,surface.Ground,terrain);
if(!terrain.outdoors){++rejected[1];continue;}
Position point;point.Relocate(x,y,surface.Ground,player->GetOrientation());
if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,point)){++rejected[2];continue;}
if(!OpenColumn(player,x,y,surface)){++rejected[3];continue;}
ClientlessPathDiagnostics path;
if(!sClientlessTravelMgr->AnalyzePath(player,point,path) || !path.Calculated || !path.Complete || path.Suspicious ||
path.FinalDistance>1.5f || path.PathLength>600 || path.MaxSegmentLength>8 || path.MaxZDelta>4){++rejected[4];continue;}
if(!sClientlessTravelMgr->StartTravel(player,point,false)){++rejected[5];continue;}
summary();
ClientlessTravelTelemetry::Note(player,"flight-departure-walk","radius="+std::to_string(radius)+" length="+std::to_string(path.PathLength));
record.ApproachingDeparture=true;record.Departure=point;
record.Status.State=ClientlessFlightState::Mounting;record.Status.FollowingPlayer=following;
record.Status.Detail="walking to an open takeoff point";
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_DEPARTURE_WALK guid=%u destination=(%.3f %.3f %.3f) path=%.2f",player->GetGUID().GetCounter(),x,y,surface.Ground,path.PathLength);
Store(player->GetGUID().GetCounter(),std::move(record));return true;
}
summary();return false;
}
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
{
ClientlessTravelTelemetry::Note(player,"flight-failed",reason+" "+ClientlessTravelTelemetry::State(player));
if(record.ApproachingDeparture)
{
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());record.ApproachingDeparture=false;
// No mount cast has started while walking; do not interrupt a combat spell.
record.Status.State=ClientlessFlightState::Failed;
}
record.Published->Aborted=true;
record.Status.FollowingPlayer=false;
player->StopMoving();
record.Moving = false;
if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
if (! record.Returning && record.Status.State == ClientlessFlightState::Flying && player->CanFly() && player->IsAlive() && player->GetMapId() == record.Status.MapId && ! record.Breadcrumbs.empty())
{
record.Returning = true;
record.AbortReason = reason;
record.Status.Detail = "aborting: returning along checked flight trail";
TC_LOG_WARN("server.adventurebot.navigation", "FLIGHT_RETURN guid=%u reason='%s'", player->GetGUID().GetCounter(), reason.c_str());
return;
}
record.Status.State = ClientlessFlightState::Failed;
record.Status.Detail = reason;
// If even the reverse corridor is blocked, remain mounted and stopped;
// never fabricate a landing or drop the player from an unknown altitude.
TC_LOG_WARN("server.adventurebot.navigation", "FLIGHT_FAILED guid=%u map=%u pos=(%.2f %.2f %.2f) reason='%s'", player->GetGUID().GetCounter(), player->GetMapId(), player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), reason.c_str());
}
bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff)
{
if(record.Following && !record.Returning)
{
while(record.Following->Replacement)
{
record.Following=record.Following->Replacement;
record.SharedIndex=0;record.Join={};record.SharedWaitSince=0;
}
if(record.Following->Aborted)
{LandFollowing(player);return true;}
Point landing;
if(!record.Following->BindLanding(landing))
{
if(!record.SharedWaitSince)record.SharedWaitSince=record.Elapsed+1;
if(record.Following->Complete || record.Elapsed+1-record.SharedWaitSince>=120000)
{LandFollowing(player);return true;}
record.Status.Detail="waiting for leader validated landing";return true;
}
record.Status.Landing.Relocate(landing.X,landing.Y,landing.Z);
record.Published->Landing=landing;record.Published->LandingReady=true;
}
Point current = PointOf(* player), goal = PointOf(record.Returning ? record.Start : record.Status.Landing);
auto sample =[&](float x, float y)
{
return Sample(player, record, x, y);
};
auto ray =[&](Point const & a, Point const & b)
{
return ClearRay(player, a, b);
};
if (Horizontal(current, goal) < 1.5f && std::fabs(current.Z - goal.Z) < 0.7f)
{
auto floor = sample(current.X, current.Y);
float feet=player->GetMap()->GetHeight(player->GetPhaseShift(),current.X,current.Y,current.Z+2,true,6);
if (!floor.Valid || !std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(current.Z-feet)>0.7f ||
!ClearRay(player,{current.X,current.Y,current.Z+0.8f},{current.X,current.Y,current.Z+4.5f}))
{
Fail(player, record, "touchdown ground not confirmed");
return false;
}
player->StopMoving();
player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_ASCENDING | MOVEMENTFLAG_DESCENDING);
player->SetAnimTier(AnimTier::Ground);
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
record.Status.State = record.Returning ? ClientlessFlightState::Failed : ClientlessFlightState::Arrived;
record.Status.Detail = record.Returning ? "safely returned after abort: " + record.AbortReason : "landed on validated destination ground";
record.Published->Complete=true;
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_LANDED guid=%u segments=%u avoidances=%u travelled=%.2f error=%.2f", player->GetGUID().GetCounter(), record.Status.Segments, record.Status.Avoidances, record.Status.Travelled, Distance(current, goal));
return true;
}
ClientlessFlightPath::Result next;
if (record.Returning)
{
while (! record.Breadcrumbs.empty() && Distance(current, PointOf(record.Breadcrumbs.back())) < 0.7f) record.Breadcrumbs.pop_back();
Point back = record.Breadcrumbs.empty() ? goal : PointOf(record.Breadcrumbs.back());
next =
{
back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false
};
}
else if(record.Following)
{
auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray,
handoff?player->GetSpeed(MOVE_FLIGHT)*1.5f:0.f,&record.Join);
if(shared.Waiting)
{
if(!record.SharedWaitSince)record.SharedWaitSince=record.Elapsed+1;
if(record.Following->Complete || record.Elapsed+1-record.SharedWaitSince>=120000)
LandFollowing(player);
return true;
}
record.SharedWaitSince=0;
next=shared.Step;
if(!next.Valid)
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_SHARED_REJECT guid=%u index=%u size=%u aborted=%u landing=(%.3f %.3f %.3f)",
player->GetGUID().GetCounter(),uint32(record.SharedIndex),uint32(record.Following->Points.size()),record.Following->Aborted?1u:0u,goal.X,goal.Y,goal.Z);
if(!next.Valid){LandFollowing(player);return true;}
if(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true;
}
else
{
auto const memory = record.Memory;
next = Next(current, goal, record.Settings, record.Memory, sample, ray);
// Reissuing the same endpoint would only restart its final milliseconds.
// Let that spline finish instead; keep planning memory unchanged.
if (handoff && next.Valid && Distance(next.Next, PointOf(record.Segment)) < 1)
{
record.Memory = memory;
return true;
}
}
if (! next.Valid ||(! record.Returning && record.Status.Travelled > Distance(PointOf(record.Start), goal) * 2 + 500))
{
// A detached/pathological flight gets one fresh airborne plan. If the
// new corridor is still impossible, finish with a checked local
// touchdown so the ordinary behavior controller can plan from ground.
if(!record.Returning && record.DetachedRecoveryCount && FindReturnLanding(player,record))
{
record.Returning=true;
record.AbortReason="airborne recovery could not reach the original target";
record.Breadcrumbs.clear();
record.Memory={};record.Memory.Departed=true;
record.Status.Detail="airborne recovery landing";
return Advance(player,record);
}
Fail(player, record, "no safe bounded flight corridor; holding position mounted");
return false;
}
Position catalogNext;catalogNext.Relocate(next.Next.X,next.Next.Y,next.Next.Z);
if(!record.Returning && !sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,catalogNext))
{Fail(player,record,"flight segment denied by area catalog");return false;}
if (! record.Returning) record.Breadcrumbs.push_back(* player);
record.Segment.Relocate(next.Next.X, next.Next.Y, next.Next.Z, player->GetOrientation());
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
Movement::MoveSplineInit init(player);
init.MoveTo(next.Next.X, next.Next.Y, next.Next.Z, false);
init.SetFly();
init.SetWalk(false);
init.SetVelocity(player->GetSpeed(MOVE_FLIGHT));
int32 const duration = init.Launch();
if (duration <= 0)
{
Fail(player, record, "flight spline rejected");
return false;
}
record.Published->Points.push_back(next.Next);
record.Published->UnderOverhang.push_back(next.UnderOverhang);
record.SegmentUnderOverhang=next.UnderOverhang;
record.Moving = true;
record.SegmentElapsed = 0;
record.Progress.Begin(Distance(current, next.Next), uint32(duration));
++ record.Status.Segments;
record.Status.Avoidances += next.Avoiding ? 1 : 0;
if (handoff) ++record.Status.Handoffs;
bool const vertical=Horizontal(current,next.Next)<1.0f && std::fabs(next.Next.Z-current.Z)>1.0f;
record.Status.Detail = next.UnderOverhang ? "elevated approach lateral escape" :
record.Memory.Elevated==Memory::ElevatedPhase::Climb ? "elevated approach vertical climb" :
record.Memory.Elevated==Memory::ElevatedPhase::Cross ? "elevated approach above platform" :
next.Avoiding ? "obstacle avoidance" : next.Landing && vertical ? "vertical landing fallback" :
next.Landing ? "landing approach" : vertical && next.Next.Z>current.Z ? "vertical takeoff fallback" :
vertical ? "vertical descent fallback" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" :
next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_SEGMENT guid=%u n=%u avoid=%u handoff=%u target=(%.2f %.2f %.2f) detail='%s'", player->GetGUID().GetCounter(), record.Status.Segments, next.Avoiding ? 1 : 0, handoff ? 1 : 0, next.Next.X, next.Next.Y, next.Next.Z, record.Status.Detail.c_str());
return true;
}
void ClientlessFlightMgr::Update(uint32 diff)
{
for (auto &[guid, record] : _records)
{
if (record.Status.State == ClientlessFlightState::Arrived || record.Status.State == ClientlessFlightState::Failed) continue;
auto * session = sWorld->FindClientlessSession(guid);
auto * player = session ? session->GetPlayer() : nullptr;
if (! player || ! player->IsInWorld() || sWorld->GetClientlessPlayerState(guid) != ClientlessPlayerState::Online) continue;
record.Elapsed += std::min(diff, uint32(30000));
record.SegmentElapsed += std::min(diff, uint32(30000));
if (player->GetMapId() != record.Status.MapId || ! player->IsAlive() || player->IsBeingTeleported())
{
Fail(player, record, "flight player/map state changed");
continue;
}
if(record.ApproachingDeparture)
{
auto travel=sClientlessTravelMgr->GetStatus(guid);
if(player->IsInCombat() || record.Elapsed>120000 || !travel ||
(travel->State!=ClientlessTravelState::Moving && travel->State!=ClientlessTravelState::Arrived))
{Fail(player,record,"departure-unavailable: ground approach interrupted");continue;}
if(travel->State==ClientlessTravelState::Moving)continue;
if(player->GetExactDist(record.Departure)>2)
{Fail(player,record,"departure-unavailable: takeoff point not reached");continue;}
auto target=record.Status.Target;bool following=record.Status.FollowingPlayer;std::string error;
// Begin replaces this record on success; do not access old fields afterwards.
if(!Begin(player,target,error,following))Fail(player,record,error);
continue;
}
// Count actual motion, not overlapping issued spline lengths.
record.Status.Travelled += Distance(PointOf(record.LastPosition), PointOf(*player));
record.LastPosition.Relocate(player);
if (record.Status.State == ClientlessFlightState::Flying && record.Moving)
record.Progress.Observe(Distance(PointOf(*player), PointOf(record.Segment)), diff);
if (record.Status.State == ClientlessFlightState::Mounting)
{
if (player->IsMounted() && player->CanFly()) record.Status.State = ClientlessFlightState::Flying;
else
{
if (record.Elapsed > 8000) Fail(player, record, "native mount did not grant flight");
continue;
}
}
if(record.Following && record.Elapsed<record.FormationDelay)continue;
if (! player->CanFly() || record.Elapsed >(record.Returning ? 2100000u : 1800000u))
{
Fail(player, record, "flight permission lost or total timeout");
continue;
}
bool const ownerLost=record.ActionOwned && !sClientlessActionExecutorMgr->OwnsActiveFlight(guid);
bool const pathological=record.Status.Segments>=12 && record.Status.Avoidances*2>=record.Status.Segments;
if(!record.Returning && !record.Following && record.DetachedRecoveryCount==0 && (ownerLost || pathological))
{
++record.DetachedRecoveryCount;
if(ownerLost)record.ActionOwned=false;
player->StopMoving();record.Moving=false;record.SegmentElapsed=0;
record.Start.Relocate(player);record.LastPosition.Relocate(player);
record.Memory={};record.Breadcrumbs.clear();
record.Published=record.Published->Replace(PointOf(record.Status.Landing));
record.Status.Detail=ownerLost?"replanning detached free flight":"replanning excessive obstacle avoidance";
ClientlessTravelTelemetry::Note(player,"flight-airborne-replan",record.Status.Detail);
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_AIRBORNE_REPLAN guid=%u ownerLost=%u segments=%u avoidances=%u target=(%.2f %.2f %.2f)",
guid,ownerLost?1:0,record.Status.Segments,record.Status.Avoidances,record.Status.Landing.GetPositionX(),record.Status.Landing.GetPositionY(),record.Status.Landing.GetPositionZ());
Advance(player,record);
continue;
}
if (record.CheckTimer > diff)
{
record.CheckTimer -= diff;
continue;
}
record.CheckTimer = 200;
if(record.Status.FollowingPlayer)
{
if(record.Elapsed-record.TargetUpdated>5000 || player->IsInCombat())LandFollowing(player);
else {AdvanceFollowing(player,record);continue;}
}
if (record.Moving && player->IsSplineEnabled())
{
if (record.Progress.Stalled())
{
TC_LOG_WARN("server.adventurebot.navigation",
"FLIGHT_STALLED guid=%u idleMs=%u segmentMs=%u expectedMs=%u overdue=%u recovery=%u",
guid, record.Progress.IdleMs, record.Progress.ElapsedMs, record.Progress.ExpectedMs,
record.Progress.Overdue()?1u:0u, record.StallRecoveries);
if (record.Returning || record.StallRecoveries >= 2)
{
Fail(player, record, "flight stalled for 60 seconds; local recovery exhausted");
continue;
}
++record.StallRecoveries;
player->StopMoving();
record.Moving = false;
// Keep the leader trail and destination. Re-evaluate the local
// join/corridor; never replace the macro route or return yet.
record.Join = {};
bool const departed = record.Memory.Departed;
record.Memory = {};
record.Memory.Departed = departed;
Advance(player, record);
continue;
}
auto ray =[&](Point const & a, Point const & b)
{
return ClearRay(player, a, b);
};
auto sample =[&](float x, float y)
{
return Sample(player, record, x, y);
};
// Recheck the remaining corridor as moving GO geometry changes.
if (! Corridor(PointOf(* player), PointOf(record.Segment), PointOf(record.Returning ? record.Start : record.Status.Landing), record.Settings, sample, ray, true, record.SegmentUnderOverhang && !record.Returning))
{
player->StopMoving();
record.Moving = false;
Advance(player, record);
}
else if (!record.Returning && player->movespline->timeRemaining() <= 1500 &&
Distance(PointOf(record.Segment), PointOf(record.Status.Landing)) > 0.1f)
{
// Replace a still-running spline from its native current position.
// No StopMoving/arrival packet between ordinary flight segments.
// Landing and conservative breadcrumb return finish their endpoint.
Advance(player, record, true);
}
continue;
}
record.Moving = false;
Advance(player, record);
}
}
bool ClientlessFlightMgr::Command(ChatHandler * handler, char const * args)
{
std::istringstream in(args ? args : "");
std::string op;
uint32 guid = 0;
in >> op >> guid;
auto const * status = GetStatus(guid);
if (op == "abort")
{
auto * session = sWorld->FindClientlessSession(guid);
auto * player = session ? session->GetPlayer() : nullptr;
auto found = _records.find(guid);
if (player && found != _records.end() &&(found->second.Status.State == ClientlessFlightState::Mounting || found->second.Status.State == ClientlessFlightState::Flying))
{
if (found->second.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
Fail(player, found->second, "operator requested safe return");
handler->SendSysMessage("Flight abort requested. An airborne player attempts the checked reverse trail before dismounting.");
}
else handler->SendSysMessage("No active free flight to abort.");
return true;
}
if (op == "probe")
{
float x, y;
auto * session = sWorld->FindClientlessSession(guid);
auto * player = session ? session->GetPlayer() : nullptr;
if (! player || !(in >> x >> y) || ! std::isfinite(x) || ! std::isfinite(y) || std::fabs(x) > 17000 || std::fabs(y) > 17000)
{
handler->SendSysMessage("Usage: .clientless flight probe <onlineGuid> <x> <y>");
return true;
}
Record probe;
auto surface = Sample(player, probe, x, y);
handler->PSendSysMessage("Flight surface map=%u x=%.3f y=%.3f valid=%u ground=%.3f ceiling=%.3f", player->GetMapId(), x, y, surface.Valid ? 1u : 0u, surface.Ground, surface.Ceiling);
auto map=player->GetMap();
float feet=map->GetHeight(player->GetPhaseShift(),x,y,player->GetPositionZ()+2.0f,true,6.0f);
LiquidData liquid{};
auto liquidStatus=map->GetLiquidStatus(player->GetPhaseShift(),x,y,player->GetPositionZ(),map_liquidHeaderTypeFlags::AllLiquids,&liquid,player->GetCollisionHeight());
handler->PSendSysMessage("Flight departure guid=%u position=(%.3f %.3f %.3f) nearFeet=%.3f rawWater=%.3f liquidStatus=%u liquidLevel=%.3f mounted=%u canFly=%u flying=%u falling=%u",
guid,player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),feet,map->GetWaterLevel(player->GetPhaseShift(),x,y),uint32(liquidStatus),liquid.level,
player->IsMounted()?1:0,player->CanFly()?1:0,player->IsFlying()?1:0,player->IsFalling()?1:0);
return true;
}
if (op == "test_node")
{
std::string node;
in >> node;
auto * session = sWorld->FindClientlessSession(guid);
auto * player = session ? session->GetPlayer() : nullptr;
auto const * action = sClientlessActionExecutorMgr->GetStatus(guid);
if (! player || ! sClientlessPlayerMgr->IsManaged(guid) || ! ClientlessRouteRecorderMgr::IsSafeKey(node) ||(action &&(action->State == ClientlessActionExecutionState::Moving || action->State == ClientlessActionExecutionState::SelectingTarget)) ||(status &&(status->State == ClientlessFlightState::Mounting || status->State == ClientlessFlightState::Flying)))
{
handler->SendSysMessage("Use an idle online CPC player and a valid destination node.");
return true;
}
QueryResult target = WorldDatabase.PQuery("SELECT w.map,w.position_x,w.position_y,w.position_z 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", node.c_str());
QueryResult origin = WorldDatabase.PQuery("SELECT n.node_key FROM vcore_clientless_travel_node n JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key WHERE n.enabled=1 AND w.enabled=1 AND w.map=%u AND POW(w.position_x-%.6f,2)+POW(w.position_y-%.6f,2)+POW(w.position_z-%.6f,2)<=9 LIMIT 1", player->GetMapId(), player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
if (! target || ! origin || target->Fetch()[0].GetUInt32() != player->GetMapId())
{
handler->SendSysMessage("Flight test requires a real departure node within 3 yards and a destination node on the same map.");
return true;
}
auto * fields = target->Fetch();
Position point;
point.Relocate(fields[1].GetFloat(), fields[2].GetFloat(), fields[3].GetFloat());
std::string error;
bool ok = Start(player, point, error);
handler->PSendSysMessage("Isolated free-flight test: %s %s", ok ? "started" : "rejected", error.c_str());
return true;
}
if (op != "status" || ! guid)
{
handler->SendSysMessage("Usage: .clientless flight status <guid> OR test_node <guid> <node>. test_node isolates flight execution; normal trips use the planner.");
return false;
}
if(auto session=sWorld->FindClientlessSession(guid))if(auto player=session->GetPlayer())
handler->PSendSysMessage("Flight capability GUID %u: enabled=%u riding=%u flightLicense=%u coldWeather=%u outdoors=%u",guid,Enabled(player)?1:0,
player->GetSkillValue(SKILL_RIDING),player->HasSpell(90267)?1:0,player->HasSpell(54197)?1:0,(player->IsInWorld() && player->IsOutdoors())?1:0);
if (! status) handler->SendSysMessage("No free-flight state for this character.");
else handler->PSendSysMessage("Flight GUID %u: state=%s map=%u mountSpell=%u segments=%u avoidances=%u handoffs=%u travelled=%.1f detail='%s'", guid, StateName(status->State), status->MapId, status->MountSpell, status->Segments, status->Avoidances, status->Handoffs, status->Travelled, status->Detail.c_str());
return true;
}