Fix stalled hunter recovery and document intel cleanup
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-20 15:47:18 +02:00
parent cc322572b4
commit ae2aedd131
9 changed files with 186 additions and 20 deletions
@@ -14,6 +14,20 @@ class ERR_VehicleBehaviorCleanup
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
// crew, vehicle teleports, health restoration, or ammunition replacement.
// Readiness failures share one deadline even if the reason changes or combat
// briefly interrupts reboarding. Native combat and moving vehicles defer exit.
class ERR_HunterRecoveryWait
{
float Since = -1;
void Reset() { Since = -1; }
bool Expired(bool registered, bool ready, bool combat, float speed, float now)
{
if (!registered || ready) { Reset(); return false; }
if (Since < 0 || now < Since) Since = now;
return now - Since >= 120000 && !combat && speed >= 0 && speed <= 3;
}
}
class ERR_HunterCrewRecovery
{
protected ERR_HunterVehicle m_Unit;
@@ -22,6 +36,7 @@ class ERR_HunterCrewRecovery
protected float m_NextAttempt;
protected string m_LastReason;
protected bool m_Stopped;
ref ERR_HunterRecoveryWait Wait = new ERR_HunterRecoveryWait();
string Reason;
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
void SuspendBoarding()
@@ -81,6 +81,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
bool DeliveredContact;
ref ERR_HunterCrewRecovery Recovery;
ref ERR_HunterTactics Tactics;
ref ERR_HunterApproachMemory FailedApproaches = new ERR_HunterApproachMemory();
ref ERR_HunterCloseContact CloseContact;
bool CloseContactHeld;
bool CombatHeld;
@@ -770,10 +771,22 @@ class ERR_HunterService : ERR_ConvoyService
// A delivered manoeuvre owns dismount/reboarding. The ordinary recovery
// must not re-add its vehicle while the team is moving on foot.
if (!unit.CloseContact) unit.CloseContact = new ERR_HunterCloseContact(unit);
if (unit.CloseContact.Tick(m_Mode, now)) continue;
if (unit.Tactics && unit.Tactics.Tick(hq, now)) continue;
if (unit.CloseContact.Tick(m_Mode, now))
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
if (unit.Tactics && unit.Tactics.Tick(hq, now))
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
UpdateCombat(unit, now);
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
// Crew failures bypass the route-progress observer. Give them their own
// bounded fallback, including a casualty occupying the driver's seat.
if (unit.Recovery)
{
float recoverySpeed = -1;
Physics recoveryPhysics = unit.Entity.GetPhysics();
if (recoveryPhysics) recoverySpeed = recoveryPhysics.GetVelocity().Length();
if (unit.Recovery.Wait.Expired(unit.Registered, unit.CrewValid(m_Mode), unit.CombatHeld, recoverySpeed, now))
{ AbandonStuckVehicle(unit, "driver recovery timed out after two minutes"); continue; }
}
if (unit.CombatHeld) continue;
if (!unit.Registered)
{
@@ -860,7 +873,7 @@ class ERR_HunterService : ERR_ConvoyService
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
}
protected void AbandonStuckVehicle(ERR_HunterVehicle unit)
protected void AbandonStuckVehicle(ERR_HunterVehicle unit, string reason = "three route retries failed")
{
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
@@ -878,7 +891,7 @@ class ERR_HunterService : ERR_ConvoyService
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
agent.m_UtilityComponent.AddAction(new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, unit.Entity, priority: 1000));
}
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": three route retries failed; original crew ordered out for on-foot HQ duties, vehicle retained");
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + reason + "; original crew ordered out for on-foot HQ duties, vehicle retained");
}
override void Stop()
{
@@ -1,5 +1,33 @@
// A bounded response by original hunter crews. Enemy positions are immutable
// radio-delivered snapshots; native perception still owns aiming and firing.
// Failures belong to the physical crew, not a disposable tactical assignment.
// Bound both memory and lifetime so changed terrain/contact can be tried later.
class ERR_HunterApproachMemory
{
protected ref array<vector> m_Positions = {};
protected ref array<float> m_Expires = {};
float RetryAfter;
void Prune(float now)
{
for (int i = m_Expires.Count() - 1; i >= 0; i--)
if (now >= m_Expires[i]) { m_Expires.Remove(i); m_Positions.Remove(i); }
}
void Reject(vector position, float now)
{
Prune(now); RetryAfter = now + 60000;
for (int i = 0; i < m_Positions.Count(); i++)
if (vector.DistanceXZ(position, m_Positions[i]) < 85)
{ m_Expires[i] = now + 300000; return; }
if (m_Positions.Count() >= 8) { m_Positions.Remove(0); m_Expires.Remove(0); }
m_Positions.Insert(position); m_Expires.Insert(now + 300000);
}
void Append(array<vector> excluded, float now)
{
Prune(now);
foreach (vector position : m_Positions) excluded.Insert(position);
}
}
class ERR_HunterAttackRules
{
static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
@@ -328,12 +356,22 @@ class ERR_HunterTactics
bool Plan(array<vector> occupied)
{
if (!Living()) return false;
float now = GetGame().GetWorld().GetWorldTime();
if (!Delivered && now < Unit.FailedApproaches.RetryAfter) return false;
array<vector> excluded = {};
foreach (vector reservedPosition : occupied) excluded.Insert(reservedPosition);
Unit.FailedApproaches.Append(excluded, now);
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
if (!utility) return false;
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
{ Stage = Unit.Entity.GetOrigin(); return true; }
{
bool rejected;
foreach (vector failedPosition : excluded)
if (vector.DistanceXZ(Unit.Entity.GetOrigin(), failedPosition) < 85) rejected = true;
if (!rejected) { Stage = Unit.Entity.GetOrigin(); return true; }
}
// Approach from this crew's side of the contact. A common cover axis used
// to send the opposite-side truck around the entire fight before dismount.
vector approachForward = ERR_MountedCover.Approach(Unit.Entity.GetOrigin(), Attack.ReportPosition, Attack.Forward);
@@ -348,7 +386,7 @@ class ERR_HunterTactics
if (attempt % 2 == 0) lateral = -lateral;
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
Stage = point; found = true; break;
}
@@ -359,7 +397,7 @@ class ERR_HunterTactics
vector point = desired;
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
point = ERR_Actors.Ground(point);
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point)) continue;
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point)) continue;
if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0), 2.5)) continue;
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
Stage = point; found = true;
@@ -371,7 +409,7 @@ class ERR_HunterTactics
vector parked = Unit.Entity.GetOrigin();
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
bool separate = true;
foreach (vector reserved : occupied) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
foreach (vector reserved : excluded) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
}
if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
@@ -410,6 +448,7 @@ class ERR_HunterTactics
}
protected bool AlternateStage(float now)
{
RejectStage(now);
// Reserve failed positions as well as the other teams. Do not repeatedly
// choose the same screened corner or send two vehicles to one firing point.
array<vector> occupied = {};
@@ -426,9 +465,13 @@ class ERR_HunterTactics
Note("alternate approach assigned=" + Stage.ToString());
return true;
}
protected void RejectStage(float now)
{
Unit.FailedApproaches.Reject(Stage, now);
}
protected bool RecoverApproach(float now)
{
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); Stop(); return false; }
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); RejectStage(now); Stop(); return false; }
m_ApproachRetries++;
bool assigned;
if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
@@ -444,7 +487,7 @@ class ERR_HunterTactics
if (VisualCover()) { m_BlindSince = -1; return true; }
if (m_BlindSince < 0) m_BlindSince = now;
if (now - m_BlindSince < 20000) return true;
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); Stop(); return false; }
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); RejectStage(now); Stop(); return false; }
m_CoverRetries++; m_BlindSince = now;
Note("cover position has no visual contact; looking for another firing angle");
AlternateStage(now);
@@ -500,7 +543,7 @@ class ERR_HunterTactics
{
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
{ Deadline = Math.Min(now + 30000, m_ApproachStarted + 300000); Note("approach still progressing; allowing another bounded route interval"); }
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); Stop(); return false; }
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); RejectStage(now); Stop(); return false; }
}
else if (now - m_ApproachProgress >= 30000 && !RecoverApproach(now)) return false;
}