--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_CivilianTraffic.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_CivilianTraffic.c @@ -33,6 +33,13 @@ float HiddenSince = -1; float ProgressAt; vector ProgressPosition; + ref SCR_AIVehicleBehavior Boarding; + float NextBoarding; + void CancelBoarding() + { + ERR_VehicleBehaviorCleanup.Complete(Boarding, Driver); + Boarding = null; + } void Report(string state) { if (Status == state) return; @@ -40,7 +47,7 @@ vector position; if (Car) position = Car.GetOrigin(); ERR_HQ.Event("CIVILIAN TRAFFIC", string.Format("car %1 prefab=%2 at %3: %4", Number, Prefab, position, state)); } - void Finish(string reason) { Finished = true; Report(reason); } + void Finish(string reason) { Finished = true; CancelBoarding(); Report(reason); } void Protect(bool protectedFromCleanup) { IEntity entity = Car; if (!entity) entity = Driver; @@ -62,7 +69,7 @@ void Release() { if (Released) return; - Released = true; ClearOrders(); Protect(false); + Released = true; CancelBoarding(); ClearOrders(); Protect(false); if (Group) { Group.SetPermanentLOD(-1); @@ -255,8 +262,19 @@ } protected bool Prepare(ERR_CivilianCar traffic) { - if (!traffic.Car || !traffic.Group || !traffic.Seat || traffic.Seat.GetOccupant() != traffic.Driver || !ERR_Actors.CanFight(traffic.Driver)) - { traffic.Report("waiting for living seated driver"); return false; } + // Compartment spawning is asynchronous. Distant agents must be awake BEFORE + // waiting for their seat; otherwise readiness and activation wait on each other. + if (traffic.Group) + { + traffic.Group.SetPermanentLOD(0); traffic.Group.ActivateAI(); + array agents = {}; traffic.Group.GetAgents(agents); + foreach (AIAgent agent : agents) if (agent) { agent.SetPermanentLOD(0); agent.ActivateAI(); } + } + if (!traffic.Car || !traffic.Group || !traffic.Seat || !ERR_Actors.CanFight(traffic.Driver)) + { + bool seated = traffic.Seat && traffic.Driver && traffic.Seat.GetOccupant() == traffic.Driver; + traffic.Report(string.Format("waiting for driver: present=%1 fit=%2 seated=%3", traffic.Driver != null, ERR_Actors.CanFight(traffic.Driver), seated)); return false; + } if (ERR_Actors.Group(traffic.Driver) != traffic.Group || traffic.Group.GetAgentsCount() != 1) { traffic.Report("waiting for native AI group admission"); return false; } SCR_AIGroupUtilityComponent utility = traffic.Group.GetGroupUtilityComponent(); @@ -264,6 +282,26 @@ if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) { traffic.Report("waiting for native vehicle registration"); return false; } utility.AddUsableVehicle(usage); + if (traffic.Seat.GetOccupant() != traffic.Driver) + { + // Recover the original driver if native compartment admission stalled or + // the driver got out before route assignment. Never replace or teleport him. + if (Foreign(traffic)) { traffic.Release(); return false; } + SCR_ChimeraAIAgent driver = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(traffic.Driver)); + float now = GetGame().GetWorld().GetWorldTime(); + if (driver && driver.m_UtilityComponent && now >= traffic.NextBoarding && !traffic.Seat.GetOccupant() && !CompartmentAccessComponent.GetVehicleIn(traffic.Driver)) + { + array actions = {}; driver.m_UtilityComponent.GetActions(actions); + if (!traffic.Boarding || !actions.Contains(traffic.Boarding)) + { + traffic.CancelBoarding(); traffic.NextBoarding = now + 10000; + traffic.Boarding = new SCR_AIGetInVehicle(driver.m_UtilityComponent, null, traffic.Car, traffic.Seat, EAICompartmentType.Pilot, 1000); + driver.m_UtilityComponent.AddAction(traffic.Boarding); + } + } + traffic.Report("waiting for original driver to board; native boarding recovery active"); return false; + } + traffic.CancelBoarding(); if (!GiveRoute(traffic)) { traffic.Report("waiting for route waypoints"); return false; } traffic.Ready = true; traffic.ProgressAt = GetGame().GetWorld().GetWorldTime(); traffic.ProgressPosition = traffic.Car.GetOrigin(); --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c @@ -78,6 +78,7 @@ protected ref array m_Hunters = {}; protected float m_NextHunter; protected float m_NextSpawn; + protected float m_NextSummary; protected int m_Target = 3; protected ref ERR_HunterOpeningPlan m_Opening; protected ref ERR_HunterCoordination m_Coordination; @@ -283,7 +284,7 @@ protected bool Coordinated(vector position) { return m_Coordination && m_Coordination.Covered(position); } protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now) { - array reports = {}; hq.SupportContacts(reports, now); + array reports = {}; hq.HunterContacts(reports, now); ERR_HQBelief best; foreach (ERR_HQReport report : reports) { @@ -309,8 +310,10 @@ unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000; return; } - if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return; - unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString(); + if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) + { ERR_HQ.Event("HUNTER ROUTE", unit.Source + ": delivered sector has no usable approach road; retaining orders", best.MessageId); return; } + unit.SourceMessage = best.MessageId; + reason = string.Format("investigate last delivered sighting; age=%1s confidence=%2 uncertainty=%3m", (now - best.ObservedAt) / 1000, best.Confidence, best.Radius); } else { @@ -356,6 +359,12 @@ m_NextHunter = now + 1000; ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return; m_Target = hq.Watch().HunterTarget(); + if (now >= m_NextSummary) + { + m_NextSummary = now + 60000; + array searches = {}; hq.HunterContacts(searches, now); + ERR_HQ.Event("HUNTER STATUS", string.Format("active=%1 deployed=%2 allowance=%3 identified bodies=%4 searchable delivered sectors=%5; lost vehicles are not replaced", ActiveHunters(), Deployed(), m_Target, hq.Watch().IdentifiedBodies(), searches.Count())); + } if (!m_Opening) { array openingPlayers = {}; ERR_Actors.Players(openingPlayers); --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c @@ -14,6 +14,7 @@ // delivered sector. Support availability is not a claim of suppressing fire. return armedCover && now >= waitingUntil - 30000; } + static bool DismountRange(float range) { return range >= 140 && range <= 450; } static bool Fresh(float observed, float now) { return observed >= 0 && now >= observed && now - observed <= 60000; } static vector Offset(vector contact, vector forward, int role, int phase) { @@ -84,6 +85,7 @@ protected float m_ApproachStarted; protected float m_ApproachProgress; protected vector m_ApproachPosition; + protected bool m_UnsupportedSearch; protected ERR_GameMode m_Mode; void ERR_HunterTactics(ERR_GameMode mode, ERR_HunterVehicle unit, ERR_HunterAttack attack, int role) { @@ -322,8 +324,11 @@ { m_ApproachProgress = now; m_ApproachPosition = Unit.Entity.GetOrigin(); } bool near = vector.DistanceXZ(Unit.Entity.GetOrigin(), Stage) <= 55; bool local = ERR_HunterCombat.Active(Unit.Group) && vector.DistanceXZ(Position(), Attack.ReportPosition) <= 400; + // A road can pass close to the reported sector and then loop kilometres + // around it to reach the ideal staging point. Continue that last leg on foot. + bool onFootReach = ERR_HunterAttackRules.DismountRange(vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition)); if (Role == 0 && near) { Clear(); State = 3; Note("in covering position; native gunner owns target acquisition and fire"); } - else if (Role > 0 && (near || local) && Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("dismounting original crew"); } + else if (Role > 0 && (near || local || onFootReach) && Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("within infantry approach range; dismounting original crew"); } else if (now >= Deadline) { if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now)) @@ -341,20 +346,22 @@ { bool visualCover = Attack.CoverReady(); bool investigate = ERR_HunterAttackRules.AdvanceWithoutVisualCover(Attack.CoverAlive(), Deadline, now); - if ((visualCover || investigate) && Move(Approach, 20)) - { + bool unsupported = !Attack.CoverAlive() && now >= Deadline; + if ((visualCover || investigate || unsupported) && Move(Approach, 20)) + { + m_UnsupportedSearch = unsupported; State = 4; Deadline = now + ERR_HunterAttackRules.ManeuverBudget(m_TravelDistance); if (visualCover) Note("cover established; moving around flank via " + Approach.ToString()); + else if (unsupported) Note("cover lost; bounded infantry investigation of delivered sector via " + Approach.ToString()); else Note("armed support in position without visual contact; investigating reported flank via " + Approach.ToString()); } else if (now >= Deadline) { - // Do not advance without a surviving, armed support crew in position. - Note("cover unavailable; holding native combat and returning to vehicle"); + Note("infantry approach could not be assigned; regrouping original survivors"); State = 6; Deadline = now + 90000; Board(true); } } - if (State == 4 && !Attack.CoverAlive()) + if (State == 4 && !m_UnsupportedSearch && !Attack.CoverAlive()) { Clear(); State = 3; Deadline = now + 45000; Note("covering crew or mounted ammunition unavailable; manoeuvre paused"); } if (State == 4 && vector.DistanceXZ(Position(), Approach) <= 30) { @@ -391,7 +398,14 @@ bool Covered(vector position) { foreach (ERR_HunterAttack attack : m_Attacks) - if (vector.DistanceXZ(position, attack.ReportPosition) < 300) return true; + { + if (vector.DistanceXZ(position, attack.ReportPosition) >= 300) continue; + int active; + foreach (ERR_HunterTactics team : attack.Teams) if (!team.Stopped && team.Living()) active++; + // A failed/finished partner must not reserve the entire encounter and + // prevent an available hunter receiving a fresh HQ interception order. + if (active >= 2) return true; + } return false; } void Tick(ERR_HQ hq, array hunters, float now) --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c @@ -466,6 +466,20 @@ contacts.Clear(); foreach (ERR_HQReport contact : m_Contacts) if (ERR_MortarPrediction.Fresh(contact, now)) contacts.Insert(contact.Snapshot()); + } + // A ground search takes longer than a mortar fire solution remains valid. + // Read delivered memory, including its decaying confidence/uncertainty, never + // the target entity's current position. A five-minute old sector is the limit. + void HunterContacts(out array contacts, float now) + { + contacts.Clear(); + for (int index = 0; index < m_Memory.Count(); index++) + { + ERR_HQBelief belief = m_Memory.At(index); + if (!belief || !belief.Report || !belief.Actionable() || belief.TravelMode == "aircraft" || belief.Radius > 500) continue; + if (belief.ObservedAt < 0 || now < belief.ObservedAt || now - belief.ObservedAt > 300000) continue; + contacts.Insert(belief.Report.Snapshot()); + } } ERR_HQWatch Watch() { return m_Watch; }