Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_MortarBattery.c
T

475 lines
17 KiB
C

// Migrated from the independently authored AI extension; successor-owned physical groups.
// Each site/phase has its own repeat limiter. Changed reasons and lifecycle
// events are immediate; only identical polling results are summarized.
class ERR_MortarTraceState
{
string LastReason;
float NextLog;
int Suppressed;
bool Due(string reason, float now)
{
if (reason != LastReason || now >= NextLog)
{
LastReason = reason;
NextLog = now + 30000;
return true;
}
Suppressed++;
return false;
}
}
class ERR_MortarBattery
{
static const ResourceName GUN = "{D1FFE458E8AC4BDB}Prefabs/Weapons/Mortars/2B14/Mortar_2B14.et";
static const ResourceName SHELL = "{98EC9C526AFBA282}Prefabs/Weapons/Ammo/Ammo_Shell_82mm_HE_O832DU.et";
static const ResourceName CREW = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
static const ResourceName RADIO_OPERATOR = "{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et";
IEntity RadioOperator;
IEntity Gun;
SCR_AIGroup Group;
ref SCR_AIStaticArtilleryActivity Activity;
ref ERR_HQReport Report;
ref ERR_HQRoute RadioOrder;
ref ERR_HQReport PendingRadioReport;
ref ERR_HQReport PendingSupplyReport;
protected ref ERR_MortarReadiness m_Readiness;
protected ref ERR_MortarSupplyTransfer m_Transfer;
protected ref array<IEntity> m_AttemptedShells = {};
protected bool m_Stopped;
protected ref ERR_MortarReportLedger m_ReportLedger = new ERR_MortarReportLedger();
vector Position;
vector Impact;
vector AimOffset;
int AimedShot = -1;
float FlightTime;
float AimedAt = -1;
SCR_AIGetArtilleryAimDistanceCompensation AimSolver;
ref ERR_MortarAimSolution AimSolution;
float AirspaceUntil;
vector AirspaceImpact;
float AirspaceRadius = 600;
float OrderedAt;
float NextOrder;
float SpawnedAt;
bool StartupActivated;
bool Released;
bool Rejected;
bool Claimed; // keeps the world slot occupied after the battery is destroyed
int SiteId;
int OrderId;
int RadioId;
int SourceId;
int OrderShots;
int TotalShots;
bool CommandOpen;
string LastFailure;
string SpawnFailure;
vector LastCandidate;
ref map<string, int> SearchCounts = new map<string, int>();
ref map<string, ref ERR_MortarTraceState> Traces = new map<string, ref ERR_MortarTraceState>();
void Audit(string kind, string detail)
{
ERR_HQ.Event("MORTAR " + kind, string.Format("site=%1 order=%2 radio=M%3 source=M%4 position=%5; %6", SiteId, OrderId, RadioId, SourceId, Position, detail), RadioId);
}
void Trace(string phase, string reason, string detail)
{
ERR_MortarTraceState state;
if (!Traces.Find(phase, state))
{
state = new ERR_MortarTraceState();
Traces.Insert(phase, state);
}
string previous = state.LastReason;
if (!state.Due(reason, GetGame().GetWorld().GetWorldTime()))
return;
Audit(phase, string.Format("reason=%1; %2; previous=%3 repeated polls omitted=%4", reason, detail, previous, state.Suppressed));
state.Suppressed = 0;
}
void FlushTraces()
{
foreach (string phase, ERR_MortarTraceState state : Traces)
{
if (state.Suppressed > 0)
Audit("REPEATS", string.Format("phase=%1 reason=%2 repeated polls omitted=%3", phase, state.LastReason, state.Suppressed));
}
Traces.Clear();
}
string OperationalReason()
{
if (m_Stopped) return "battery stopped";
if (!Gun) return "gun missing";
if (!SpawnFailure.IsEmpty()) return SpawnFailure;
if (!Group || !Group.GetGroupUtilityComponent()) return "crew group missing";
if (Group.GetFaction() != ERR_GameMode.Get().EnemyFaction()) return "crew changed faction";
if (Group.GetPlayerCount(true) > 0) return "player in crew group";
if (Group.GetAgentsCount() == 0) return "crew group empty";
DamageManagerComponent damage = DamageManagerComponent.Cast(Gun.FindComponent(DamageManagerComponent));
if (damage && damage.IsDestroyed()) return "gun destroyed";
return "";
}
bool CanRequest(ERR_HQReport report, float now)
{
return !m_Stopped && ERR_MortarPrediction.Fresh(report, now) && m_ReportLedger.Available(report.MessageId, report.ObservedAt, now);
}
bool BeginRequest(ERR_HQReport report, float now)
{
if (!CanRequest(report, now) || !m_ReportLedger.Claim(report.MessageId, report.ObservedAt, now)) return false;
Cancel("superseded by new HQ order");
FlushTraces();
OrderId++;
RadioId = 0;
SourceId = report.MessageId;
OrderShots = 0;
Released = false;
CommandOpen = true;
OrderedAt = now;
LastFailure = "";
Audit("COMMAND", string.Format("requested rounds=%1 HE radius=%2m; %3", ERR_MortarSalvo.ROUNDS, ERR_MortarSalvo.RADIUS_METRES, report.Description(now)));
return true;
}
void ObserveProjectile(IEntity projectileEntity, IEntity firingCharacter, bool playerGunner)
{
if (!projectileEntity)
{
Audit("FIRE CALLBACK", "native OnWeaponFired had no projectile; no confirmed shot counted");
return;
}
TotalShots++;
if (CommandOpen && Released && !playerGunner)
{
OrderShots++;
// Only a physical projectile unlocks the next load. It must be aimed again.
Released = false;
AimedAt = -1;
AimSolver = null;
AimSolution = null;
Audit("FIRED", string.Format("physical projectile=%1 origin=%2 order shots=%3 total shots=%4 predicted impact=%5 predicted flight=%6s gunner=%7 (impact not observed)", projectileEntity, projectileEntity.GetOrigin(), OrderShots, TotalShots, Impact, FlightTime, firingCharacter));
}
else
Audit("FIRED UNASSIGNED", string.Format("physical projectile=%1 origin=%2 player gunner=%3 order open=%4 committed=%5 total shots=%6; no active HQ order attribution", projectileEntity, projectileEntity.GetOrigin(), playerGunner, CommandOpen, Released, TotalShots));
}
void ReserveShellAirspace(float now)
{
// Enclose every still-airborne shell, including an earlier, longer flight.
// A later round must not move/shrink the existing exclusion or deadline.
if (now >= AirspaceUntil)
{
AirspaceImpact = Impact;
AirspaceRadius = 600;
}
AirspaceRadius = Math.Max(AirspaceRadius, vector.DistanceXZ(AirspaceImpact, Impact) + 600);
AirspaceUntil = Math.Max(AirspaceUntil, now + (FlightTime + 10) * 1000);
}
bool SpawnFailed(string reason)
{
SpawnFailure = reason;
Audit("DEPLOY FAILED", string.Format("reason=%1 claimed=%2 gun=%3 group=%4", reason, Claimed, Gun, Group));
return false;
}
void SearchResult(vector candidate, string reason)
{
LastCandidate = candidate;
int count;
SearchCounts.Find(reason, count);
SearchCounts.Set(reason, count + 1);
}
void SiteStatus(float now)
{
if (!SearchCounts.IsEmpty())
{
string counts;
foreach (string reason, int count : SearchCounts) counts += string.Format("%1=%2; ", reason, count);
Audit("SITE SEARCH", string.Format("candidate counts since last summary: %1 last candidate=%2", counts, LastCandidate));
SearchCounts.Clear();
}
int agents, living;
int actionState = -1;
if (Group)
{
array<AIAgent> crew = {};
Group.GetAgents(crew);
agents = crew.Count();
foreach (AIAgent agent : crew)
{
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) living++;
}
}
if (Activity) actionState = Activity.GetActionState();
bool connected;
ERR_HQ hq = ERR_HQ.Get();
if (hq && Group) connected = hq.MortarConnected(Group);
Audit("SITE STATUS", string.Format("claimed=%1 startup=%2 operational=%3 crew agents=%4 fighting=%5 inventory HE=%6 radio connected=%7 cooldown remaining=%8s", Claimed, StartupActivated, Operational(), agents, living, CountShells(), connected, Math.Max(0, NextOrder - now) / 1000));
Audit("ORDER STATUS", string.Format("open=%1 route pending=%2 native action=%3 shell committed=%4 order shots=%5 total shots=%6 age=%7s last failure=%8", CommandOpen, RadioOrder != null, actionState, Released, OrderShots, TotalShots, Math.Max(0, now - OrderedAt) / 1000, LastFailure));
if (!SpawnFailure.IsEmpty()) Audit("PARTIAL DEPLOYMENT", SpawnFailure);
if (!Operational()) Trace("SITE BLOCK", OperationalReason(), "spawn failure=" + SpawnFailure);
}
bool Operational()
{
return OperationalReason().IsEmpty();
}
int CountShells()
{
if (!Group)
return 0;
int count;
array<IEntity> items = {};
array<AIAgent> agents = {};
Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity()))
continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(agent.GetControlledEntity().FindComponent(InventoryStorageManagerComponent));
if (!inventory)
continue;
ERR_Inventory.AppendUnique(inventory, items);
}
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL) count++;
return count;
}
void Bind(int siteId, SCR_AIGroup group, IEntity gun, float now)
{
SiteId = siteId;
Group = group;
Gun = gun;
Position = gun.GetOrigin();
SpawnedAt = now;
Claimed = true;
m_Readiness = new ERR_MortarReadiness(this);
}
int InventoryShells(InventoryStorageManagerComponent inventory)
{
if (!inventory)
return 0;
int count;
array<IEntity> items = {};
ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
{
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL)
count++;
}
return count;
}
bool Stopped() { return m_Stopped; }
bool SupplyPending() { return m_Transfer && m_Transfer.Unresolved(); }
bool SupplyAuthorized(float now)
{
if (!Replication.IsServer() || m_Stopped || !StartupActivated || !Operational() || !CommandOpen || !ERR_HQ.MissionActive()) return false;
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (!system || system.FindGun(Gun) != this) return false;
ERR_HQReport suppliedReport = Report;
if (!suppliedReport) suppliedReport = PendingSupplyReport;
return ERR_MortarPrediction.Fresh(suppliedReport, now);
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
Cancel("HQ stopped");
if (m_Readiness) m_Readiness.Stop();
}
ERR_MortarSupplyState SupplyState(float now)
{
// Observe an existing native operation even after its order was canceled.
// No second request may race an unresolved transfer on this battery.
if (m_Transfer)
{
ERR_MortarSupplyState observed = m_Transfer.Poll(now);
if (m_Transfer.CapturedOrder() == OrderId)
{
if (observed == ERR_MortarSupplyState.READY) m_Transfer = null;
return observed;
}
if (m_Transfer.Unresolved()) return ERR_MortarSupplyState.FAILED;
m_Transfer = null;
}
if (!SupplyAuthorized(now)) return SupplyStateFailed("current HQ order, crew or report is unavailable");
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Gun.FindComponent(SCR_AIVehicleUsageComponent));
IEntity gunner;
if (usage && usage.GetTurretCompartmentSlot())
gunner = usage.GetTurretCompartmentSlot().GetOccupant();
array<AIAgent> agents = {};
Group.GetAgents(agents);
float nearest = float.MAX;
if (!gunner)
{
foreach (AIAgent candidate : agents)
{
if (!candidate || candidate.GetControlledEntity() == RadioOperator || !ERR_Actors.CanFight(candidate.GetControlledEntity()))
continue;
float distance = vector.DistanceXZ(Gun.GetOrigin(), candidate.GetControlledEntity().GetOrigin());
if (distance < nearest)
{
nearest = distance;
gunner = candidate.GetControlledEntity();
}
}
}
if (!gunner) return SupplyStateFailed("no fighting gunner found");
if (ERR_Actors.Group(gunner) != Group) return SupplyStateFailed("turret occupant belongs to another group");
if (!ERR_Actors.CanFight(gunner)) return SupplyStateFailed("gunner cannot fight");
InventoryStorageManagerComponent destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
if (!destination)
return SupplyStateFailed("gunner inventory missing");
if (InventoryShells(destination) > 0)
return ERR_MortarSupplyState.READY;
int nearbyDonors, donorShells, storageRejected;
array<IEntity> consideredShells = {};
foreach (AIAgent bearer : agents)
{
if (!bearer || !ERR_Actors.CanFight(bearer.GetControlledEntity()))
continue;
IEntity donor = bearer.GetControlledEntity();
if (donor == gunner || vector.DistanceXZ(donor.GetOrigin(), gunner.GetOrigin()) > 12)
continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(donor.FindComponent(InventoryStorageManagerComponent));
if (!inventory)
continue;
nearbyDonors++;
array<IEntity> items = {};
ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
{
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != SHELL)
continue;
if (consideredShells.Contains(item) || m_AttemptedShells.Contains(item)) continue;
consideredShells.Insert(item);
donorShells++;
BaseInventoryStorageComponent storage = destination.FindStorageForItem(item, EStoragePurpose.PURPOSE_DEPOSIT);
if (!storage) { storageRejected++; continue; }
m_AttemptedShells.Insert(item); // even rejected/ambiguous requests are never retried
m_Transfer = new ERR_MortarSupplyTransfer(this, donor, gunner, item, storage, now);
if (!m_Transfer.Begin(now)) return SupplyStateFailed("exact shell transfer request rejected; no retry");
Audit("AMMO TRANSFER", string.Format("one exact-item move pending donor=%1 gunner=%2 shell=%3 order=%4; awaiting callback AND physical destination", donor, gunner, item, OrderId));
return ERR_MortarSupplyState.PENDING;
}
}
return SupplyStateFailed(string.Format("gunner empty; fighting donors within 12m=%1 unattempted HE=%2 destination storage rejected=%3", nearbyDonors, donorShells, storageRejected));
}
// Compatibility for existing native callers; request acceptance is NOT ready.
bool SupplyGunner() { return SupplyState(GetGame().GetWorld().GetWorldTime()) == ERR_MortarSupplyState.READY; }
protected ERR_MortarSupplyState SupplyStateFailed(string reason)
{
SupplyFailed(reason);
return ERR_MortarSupplyState.FAILED;
}
bool SupplyFailed(string reason)
{
LastFailure = reason;
Audit("AMMO FAILED", reason);
return false;
}
void Cancel(string reason = "cancel requested")
{
if (CommandOpen)
{
FlushTraces();
Audit("ORDER END", string.Format("reason=%1 observed shots=%2 shell committed=%3 elapsed=%4s last failure=%5", reason, OrderShots, Released, (GetGame().GetWorld().GetWorldTime() - OrderedAt) / 1000, LastFailure));
}
CommandOpen = false;
RadioOrder = null;
PendingRadioReport = null;
PendingSupplyReport = null;
if (m_Transfer) m_Transfer.Invalidate();
Report = null;
AimedAt = -1;
AimSolver = null;
AimSolution = null;
if (Activity)
Activity.Complete();
Activity = null;
}
// Idle distant batteries still report through HQ's scheduler. Their native
// AI needs full detail while firing or when players can interact with them.
protected void UpdateCrewDetail(float now, bool firing = false)
{
if (!Group || Group.GetPlayerCount(true) > 0)
return;
bool active = firing || Activity != null || ERR_MortarSystem.NearPlayer(Position);
int lod = -1;
if (active)
lod = 0;
if (Group.GetPermanentLOD() != lod)
Group.SetPermanentLOD(lod);
if (active && !Group.IsAIActivated())
Group.ActivateAI();
array<AIAgent> agents = {};
Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || EntityUtils.IsPlayer(agent.GetControlledEntity()))
continue;
if (agent.GetPermanentLOD() != lod)
agent.SetPermanentLOD(lod);
if (active && !agent.IsAIActivated() && ERR_Actors.CanFight(agent.GetControlledEntity()))
agent.ActivateAI();
}
}
void Update(float now)
{
if (m_Stopped) return;
if (!StartupActivated)
{
if (!SpawnFailure.IsEmpty()) return;
if (!m_Readiness) { SpawnFailed("initial support readiness owner missing"); return; }
ERR_MortarSupplyState startup = m_Readiness.Poll(now);
if (startup != ERR_MortarSupplyState.READY) return;
StartupActivated = true;
}
if (m_Transfer) m_Transfer.Poll(now);
if (!Operational())
{
Cancel(OperationalReason());
return;
}
if (Activity)
{
Trace("ACTIVITY", string.Format("native state=%1", Activity.GetActionState()), string.Format("age=%1s aim age=%2s committed=%3 shots=%4", (now - OrderedAt) / 1000, (now - AimedAt) / 1000, Released, OrderShots));
if (Activity.GetActionState() == EAIActionState.FAILED) Cancel("native artillery activity FAILED");
else if (Activity.GetActionState() == EAIActionState.COMPLETED) Cancel("native artillery activity COMPLETED (shot count is separate)");
else if (now - OrderedAt > ERR_MortarSalvo.TIMEOUT_MS) Cancel("120s artillery salvo timeout");
}
if (StartupActivated)
UpdateCrewDetail(now);
}
void Order(ERR_HQReport report, float now)
{
if (m_Stopped || !StartupActivated || !Operational() || !CommandOpen || Activity || !ERR_MortarPrediction.Fresh(report, now)) return;
Report = report.Snapshot();
OrderedAt = now;
NextOrder = now + 30000;
Released = false;
UpdateCrewDetail(now, true);
Activity = new SCR_AIStaticArtilleryActivity(Group.GetGroupUtilityComponent(), null, report.Position, SCR_EAIArtilleryAmmoType.HIGH_EXPLOSIVE, ERR_MortarSalvo.ROUNDS);
Group.GetGroupUtilityComponent().AddAction(Activity);
Audit("AIM", string.Format("native activity created; latest source=M%1 observed=%2", report.MessageId, report.Description(now)));
}
}