Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
227 lines
12 KiB
Diff
227 lines
12 KiB
Diff
--- 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<AIAgent> 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<ref AIActionBase> 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<ref ERR_HunterVehicle> 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<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
|
+ array<ref ERR_HQReport> 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<ref ERR_HQReport> 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<IEntity> 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<ref ERR_HunterVehicle> 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<ref ERR_HQReport> 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; }
|