1024 lines
57 KiB
C++
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;
|
|
}
|