Improve free-flight recovery and fallback handling

This commit is contained in:
Nico committed 2026-11-01 01:20:33 +01:00
1 parent 7b4994400b
commit 856832e7bd
7 files changed
+109 -7

No files matched your search

@@ -206,6 +206,12 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
return itr != _records.end() ? &itr->second.Status : nullptr;
}
bool ClientlessActionExecutorMgr::OwnsActiveFlight(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() && itr->second.Active && itr->second.FlightActive;
}
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
{
record.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
@@ -1025,7 +1031,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
}
Position target;target.Relocate(step.X,step.Y,step.Z,step.O);
if (!validSource || step.MapId!=player->GetMapId() ||
!sClientlessFlightMgr->Start(player,target,error))
!sClientlessFlightMgr->Start(player,target,error,true))
{
ClientlessTravelTelemetry::Note(player,"flight-start-rejected",error+" "+ClientlessTravelTelemetry::State(player));
return RecoverFlightLeg(player,characterGuid,record,error.empty()?"free flight source not physically reached":error);
@@ -1916,7 +1922,7 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
if (!std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target, true)))
return false;
std::string error;
if (!sClientlessFlightMgr->Start(player, target, error))
if (!sClientlessFlightMgr->Start(player, target, error, true))
{
ClientlessTravelTelemetry::Note(player,"flight-fallback-rejected",error+" "+ClientlessTravelTelemetry::State(player));
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_FALLBACK_UNAVAILABLE guid=%u reason='%s'",
@@ -1998,7 +2004,7 @@ void ClientlessActionExecutorMgr::CheckGroundExit(Player* player, uint32 charact
if (std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target)))
{
std::string error;
if (sClientlessFlightMgr->Start(player, target, error))
if (sClientlessFlightMgr->Start(player, target, error, true))
{
record.GroundExitActive = true;
record.FlightActive = true;
@@ -50,6 +50,7 @@ public:
void SetGroundPathAdapter(uint32 guid,GroundPathAdapter adapter);
bool RequestStopAtNode(uint32 characterGuid);
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
bool OwnsActiveFlight(uint32 characterGuid) const;
static char const* StateName(ClientlessActionExecutionState state);
+41 -3
View File
@@ -202,8 +202,13 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f
};
}
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error)
{return Begin(player,target,error,false);}
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() ||
@@ -647,6 +652,18 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
}
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;
}
@@ -672,7 +689,11 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
++ record.Status.Segments;
record.Status.Avoidances += next.Avoiding ? 1 : 0;
if (handoff) ++record.Status.Handoffs;
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing ? "landing approach" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" : next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
bool const vertical=Horizontal(current,next.Next)<1.0f && std::fabs(next.Next.Z-current.Z)>1.0f;
record.Status.Detail = 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;
}
@@ -724,6 +745,23 @@ void ClientlessFlightMgr::Update(uint32 diff)
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=std::make_shared<ClientlessFlightPath::SharedTrail>();
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;
+3 -1
View File
@@ -31,7 +31,7 @@ class TC_GAME_API ClientlessFlightMgr
bool Preferred(Player const * player, uint32 mapId, Position const & position) const;
bool Enabled(Player const * player) const;
void BeginPlanning(Player const* player = nullptr);
bool Start(Player * player, Position const & target, std::string & error);
bool Start(Player * player, Position const & target, std::string & error, bool actionOwned = false);
bool Follow(Player* player, Position const& target, std::string& error);
bool LandFollowing(Player* player);
bool PrepareFollowingMount(Player* player);
@@ -53,6 +53,8 @@ class TC_GAME_API ClientlessFlightMgr
bool ApproachingDeparture = false;
Position Departure;
uint32 TargetUpdated = 0, FormationDelay = 0;
uint32 DetachedRecoveryCount = 0;
bool ActionOwned = false;
std::string AbortReason;
std::vector < Position > Breadcrumbs;
std::unordered_map < std::string, bool > Tiles;
@@ -118,6 +118,13 @@ namespace ClientlessFlightPath
if (memory.Departed && from.Z>=goal.Z && from.Z-goal.Z<=12 &&
Distance(from,goal)<=cfg.Step && Corridor(from,goal,goal,cfg,sample,ray,false))
return {goal,true,false,true};
// A normal diagonal touchdown remains the first choice. When the bot
// is already above the landing column and there is not enough lateral
// room for that slope, descend in the verified column instead of
// opening another wide approach.
if (memory.Departed && distance < 2.0f && from.Z > goal.Z + 1.5f &&
Corridor(from,goal,goal,cfg,sample,ray,false))
return {goal,true,false,true};
if (distance < 2 && std::fabs(from.Z - goal.Z) < 1.5f) return
{
goal, Corridor(from, goal, goal, cfg, sample, ray, true), false, true
@@ -212,6 +219,20 @@ namespace ClientlessFlightPath
return best;
}
}
// Diagonal departure candidates were exhausted. Tight settlements
// can still have a completely open vertical column, so use it before
// asking the caller to walk to a different takeoff point.
if (!best.Valid && departure)
{
float const top = std::min(here.Ceiling - 6.0f,
std::max(from.Z + 20.0f, here.Ground + std::min(cfg.Clearance, 35.0f)));
Point vertical{from.X,from.Y,top};
if (top > from.Z + 2.0f && Corridor(from,vertical,goal,cfg,sample,ray,true))
{
memory.Departed=true;
return {vertical,true,false,false};
}
}
if (best.Valid) memory.Departed = true;
return best;
}
@@ -1273,6 +1273,24 @@ DeletedCharacterTicketTrace = 0
DungeonFinder.OptionsMask = 1
#
# DungeonFinder.LowPopulation.Enable
# DungeonFinder.LowPopulation.FallbackDelay
# DungeonFinder.LowPopulation.MinPlayers
# DungeonFinder.LowPopulation.SoloGroupOutsideTimeout
# Description: Allow the dungeon finder to create a dungeon group below
# the normal group size after the fallback delay. The group
# remains marked as low-population while inside the dungeon.
# Default: Enable = 0
# FallbackDelay = 60 seconds
# MinPlayers = 1
# SoloGroupOutsideTimeout = 300 seconds
DungeonFinder.LowPopulation.Enable = 0
DungeonFinder.LowPopulation.FallbackDelay = 60
DungeonFinder.LowPopulation.MinPlayers = 1
DungeonFinder.LowPopulation.SoloGroupOutsideTimeout = 300
#
# DungeonFinder.CallToArmsEnable
# Description: Enable Call to Arms bonus mechanism for random Cataclysm dungeons
+16
View File
@@ -28,6 +28,15 @@ int main(){
assert(touchdown.Valid&&touchdown.Landing&&Distance(touchdown.Next,{0,0,0})<0.01f);
auto solidWall=[](Point const&,Point const&){return false;};
assert(!Next({6,0,10},{0,0,0},Settings{},finalApproach,flat,solidWall).Valid);
// Diagonal movement is blocked but the local air column is clear: take off
// and touch down vertically instead of rejecting an otherwise safe flight.
auto verticalOnly=[](Point const&a,Point const&b){return Horizontal(a,b)<0.1f;};
Memory verticalTakeoff;
auto ascent=Next({0,0,0},{100,0,0},Settings{},verticalTakeoff,flat,verticalOnly);
assert(ascent.Valid&&Horizontal({0,0,0},ascent.Next)<0.1f&&ascent.Next.Z>10);
Memory verticalLanding;verticalLanding.Departed=true;
auto descent=Next({0,0,40},{0,0,0},Settings{},verticalLanding,flat,verticalOnly);
assert(descent.Valid&&descent.Landing&&Distance(descent.Next,{0,0,0})<0.01f);
auto normal=simulate({0,0,0},{800,0,0},flat,clear);
assert(normal.Arrived&&normal.Peak>30&&normal.Peak<=60.1f);
auto low=[](float,float){return Surface{0,42,true};};
@@ -84,3 +93,10 @@ with tempfile.TemporaryDirectory(prefix='vcore-free-flight-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
flight=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
executor=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
assert 'FLIGHT_AIRBORNE_REPLAN' in flight
assert 'OwnsActiveFlight(guid)' in flight
assert 'found->second.ActionOwned=actionOwned' in flight
assert 'itr->second.Active && itr->second.FlightActive' in executor