#include "AdventureAreaMgr.h" #include "ClientlessTravelTelemetry.h" #include "ClientlessFlightMgr.h" #include #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 #include #include #include 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{{{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{{{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::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.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::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(candidateIsSplineEnabled() && 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 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.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 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 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.ElapsedCanFly() || 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 "); 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 OR test_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; }