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

477 lines
27 KiB
C

#ifdef WORKBENCH
// Explicit synthetic observation only. Delivery, HQ scheduling and native fire
// are unchanged; this helper never writes directly into the HQ contact list.
modded class ERR_HQ
{
int ERR_QueueMortarRefreshReport(SCR_AIGroup source, IEntity radio, IEntity target, vector position, out float observedAt)
{
observedAt = -1;
if (!ERR_NativeMortarSalvoProbe.RefreshEnabled() || !target || !radio || ERR_HQRadio.Capability(radio) != 2) return 0;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != source || !unit.ReservedForSupport || unit.Radio != radio || unit.Uplink || unit.PendingReport || unit.PendingSupplyStatus) continue;
ERR_HQRoute route = m_Network.Route(radio);
if (!route || route.Nodes.IsEmpty()) return 0;
observedAt = GetGame().GetWorld().GetWorldTime();
ERR_HQReport report = new ERR_HQReport();
report.MessageId = m_Journal.Message(); report.Target = target; report.Source = source;
report.Position = position; report.ObservedAt = observedAt; report.DetailsObservedAt = observedAt;
report.HasVelocity = true; report.Velocity = vector.Zero;
report.TravelMode = "on foot"; report.SeenEscapists = 1; report.SeenWeapons = "fixture observation";
route.MessageId = report.MessageId; route.Payload = "NEW EXPLICIT SYNTHETIC OBSERVATION: " + report.Description(observedAt);
unit.PendingReport = report; unit.Uplink = route;
m_Journal.Write("FIXTURE REPORT", route.Payload, report.MessageId);
return report.MessageId;
}
return 0;
}
int ERR_QueueMortarSalvoReport(SCR_AIGroup source, IEntity target, vector position, float now)
{
if (!ERR_NativeMortarSalvoProbe.Enabled() || !target || !m_Contacts.IsEmpty()) return 0;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != source || !unit.ReservedForSupport || !unit.Radio || unit.Uplink || unit.PendingReport) continue;
ERR_HQRoute route = m_Network.Route(unit.Radio);
if (!route || route.Nodes.IsEmpty()) return 0;
ERR_HQReport report = new ERR_HQReport();
report.MessageId = m_Journal.Message(); report.Target = target; report.Source = source;
report.Position = position; report.ObservedAt = now; report.DetailsObservedAt = now;
report.HasVelocity = true; report.Velocity = vector.Zero;
report.TravelMode = "on foot"; report.SeenEscapists = 1; report.SeenWeapons = "fixture observation";
route.MessageId = report.MessageId; route.Payload = "EXPLICIT SYNTHETIC OBSERVATION: " + report.Description(now);
unit.PendingReport = report; unit.Uplink = route;
m_Journal.Write("FIXTURE REPORT", route.Payload, report.MessageId);
return report.MessageId;
}
return 0;
}
void ERR_RemoveMortarSalvoReport(IEntity target)
{
if (!target) return;
for (int i = m_Contacts.Count() - 1; i >= 0; i--)
if (m_Contacts[i].Target == target) m_Contacts.Remove(i);
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.PendingReport && unit.PendingReport.Target == target) { unit.PendingReport = null; unit.Uplink = null; }
if ((unit.PendingOrder && unit.PendingOrder.Target == target) || (unit.Assignment && unit.Assignment.Target == target)) unit.Release();
}
}
}
// Production sets Released immediately before reserving shell airspace, then
// invokes native inventory transfer. Observe that existing commit boundary so
// even a synchronous native projectile has a prior recorded authorization.
modded class ERR_MortarBattery
{
override void Audit(string kind, string detail)
{
super.Audit(kind, detail);
ERR_NativeMortarSalvoProbe probe = ERR_NativeMortarSalvoProbe.Active();
if (probe) probe.NativeAudit(this, kind);
}
override void ReserveShellAirspace(float now)
{
super.ReserveShellAirspace(now);
ERR_NativeMortarSalvoProbe probe = ERR_NativeMortarSalvoProbe.Active();
if (probe) probe.NativeCommit(this, now);
}
override void Cancel(string reason = "cancel requested")
{
ERR_NativeMortarSalvoProbe probe = ERR_NativeMortarSalvoProbe.Active();
if (probe) probe.NativeClosing(this, reason, LastFailure);
super.Cancel(reason);
}
}
// Exact completion is separate from native activity success or accepted loading.
class ERR_NativeMortarSalvoRules
{
static bool Complete(int events, int commits, int total, int orderShots, int shells, bool pending)
{
return events == ERR_MortarSalvo.ROUNDS && commits == events && total == events && orderShots == events && shells == 27 - events && !pending;
}
static bool QueueUpdate(bool accepted, bool pending, int shots, float observedAt, float orderedAt, float now)
{
return accepted && !pending && shots < ERR_MortarSalvo.ROUNDS / 2 && now >= observedAt + 20000 && orderedAt >= 0 && now >= orderedAt && now - orderedAt < 90000;
}
}
class ERR_NativeMortarRefreshRecord
{
int MessageId;
float ObservedAt;
int DownlinkId;
bool Accepted;
ref ERR_HQReport Delivered;
bool Matches(ERR_HQReport report, IEntity target, SCR_AIGroup group, vector position)
{
if (!report || report.MessageId != MessageId || report.ObservedAt != ObservedAt || report.Target != target || report.Source != group) return false;
return vector.Distance(report.Position, position) < 0.01 && report.HasVelocity && report.Velocity.Length() < 0.001 && !report.Distress;
}
}
// Observe only the actual native event. Node SUCCESS, accepted inventory moves
// and committed shell loading are never counted as physical projectiles.
modded class SCR_MortarMuzzleComponent
{
override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity)
{
ERR_NativeMortarSalvoProbe probe = ERR_NativeMortarSalvoProbe.Active();
if (probe) probe.NativeProjectile(GetOwner(), projectileEntity);
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
}
}
class ERR_NativeMortarSalvoProbe
{
protected static ERR_NativeMortarSalvoProbe s_Active;
protected ERR_GameMode m_Mode;
protected ERR_MortarBattery m_Battery;
protected IEntity m_Gun;
protected SCR_AIGroup m_Group;
protected IEntity m_Target;
protected vector m_Impact;
protected ref array<IEntity> m_Crew = {};
protected ref array<IEntity> m_InitialShells = {};
protected ref array<IEntity> m_Projectiles = {};
protected int m_Source;
protected int m_Order;
protected int m_Step;
protected int m_Events;
protected int m_StoppedShells;
protected float m_Deadline;
protected float m_NextDiagnostic;
protected float m_StopUntil;
protected float m_ObservedAt;
protected ref ERR_HQReport m_Delivered;
protected bool m_RefreshCase;
protected IEntity m_OriginalRTO;
protected SCR_AIStaticArtilleryActivity m_OriginalActivity;
protected ref array<ref ERR_NativeMortarRefreshRecord> m_Observations = {};
protected int m_UpdatesAccepted;
protected int m_UpdatedCommits;
protected int m_CommitSource;
protected float m_CommitObservedAt;
protected bool m_CommitPending;
protected float m_CommittedAt;
protected int m_CommitOrder;
protected int m_CommitShot;
protected int m_Commits;
protected bool m_ClosureSeen;
protected bool m_ClosureComplete;
protected bool m_Unsafe;
protected bool m_BadProjectile;
protected bool m_SawUplink;
protected bool m_SawDownlink;
protected bool m_SawActivity;
protected bool m_Stopped;
protected string m_BadReason;
static bool Enabled()
{
string enabled, outcome;
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "mortar-salvo" || outcome == "mortar-refresh");
}
static bool RefreshEnabled()
{
string outcome;
return Enabled() && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mortar-refresh";
}
static ERR_NativeMortarSalvoProbe Active() { return s_Active; }
void ERR_NativeMortarSalvoProbe(ERR_GameMode mode)
{
m_Mode = mode;
m_RefreshCase = RefreshEnabled();
m_Deadline = GetGame().GetWorld().GetWorldTime() + 180000;
if (Enabled() && !s_Active) s_Active = this;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: mortar salvo " + detail); return true; }
Print("ERR SMOKE FAIL: mortar salvo " + detail, LogLevel.ERROR); Stop(); return false;
}
protected void Shells(out array<IEntity> shells)
{
shells.Clear();
foreach (IEntity actor : m_Crew)
{
if (!actor) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
foreach (IEntity item : items)
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL && !shells.Contains(item)) shells.Insert(item);
}
}
protected bool CheckNoOccupants(IEntity entity)
{
if (ChimeraCharacter.Cast(entity) || Vehicle.Cast(entity)) { m_Unsafe = true; return false; }
return true;
}
protected bool EmptyImpact(vector position)
{
m_Unsafe = false;
GetGame().GetWorld().QueryEntitiesByAABB(position - "200 5000 200", position + "200 5000 200", CheckNoOccupants, null, EQueryEntitiesFlags.ALL);
return !m_Unsafe;
}
protected bool SelectImpact(ERR_MortarSystem system)
{
// Bounded candidate survey: no clearance assumption from coordinates alone.
array<vector> offsets = {"-800 0 -600", "-600 0 -800", "0 0 -1000", "600 0 -800", "-1000 0 0", "800 0 -600", "-800 0 600", "0 0 1000"};
foreach (vector offset : offsets)
{
vector candidate = ERR_Actors.Ground(m_Battery.Position + offset);
if (candidate[1] < 1 || !EmptyImpact(candidate) || !system.Safe(m_Battery, candidate, 75)) continue;
bool clear = true;
for (int shot = 0; shot < ERR_MortarSalvo.ROUNDS; shot++)
{
vector spread = ERR_Actors.Ground(candidate + ERR_MortarSalvo.Offset(m_Battery.SiteId, m_Battery.OrderId + 1, shot));
if (!system.Safe(m_Battery, spread, 75)) { clear = false; break; }
}
if (!clear) continue;
m_Impact = candidate;
PrintFormat("ERR MORTAR SALVO IMPACT: position=%1 batteryDistance=%2; no characters/vehicles in 400m square; production settlement/friendly exclusions passed", candidate, vector.DistanceXZ(candidate, m_Battery.Position));
return true;
}
return false;
}
protected void Diagnose(float now)
{
if (now < m_NextDiagnostic) return;
m_NextDiagnostic = now + 5000;
if (!m_Battery) { Print("ERR MORTAR SALVO: waiting for one authored battery"); return; }
PrintFormat("ERR MORTAR SALVO: step=%1 startup=%2 source=M%3 observedEvents=%4 ready=%5 failure=%6", m_Step, m_Battery.StartupActivated, m_Source, m_Events, m_Battery.Operational(), m_Battery.SpawnFailure);
m_Battery.SiteStatus(now);
}
protected ERR_NativeMortarRefreshRecord FindObservation(int messageId)
{
foreach (ERR_NativeMortarRefreshRecord observation : m_Observations)
if (observation.MessageId == messageId) return observation;
return null;
}
protected bool AddObservation(int messageId, float observedAt)
{
if (messageId <= 0 || observedAt < 0 || FindObservation(messageId) || m_Observations.Count() >= 8) return false;
if (!m_Observations.IsEmpty() && observedAt <= m_Observations[m_Observations.Count() - 1].ObservedAt) return false;
ERR_NativeMortarRefreshRecord record = new ERR_NativeMortarRefreshRecord();
record.MessageId = messageId; record.ObservedAt = observedAt;
m_Observations.Insert(record);
PrintFormat("ERR MORTAR REFRESH OBSERVATION: source=M%1 actualObservedAt=%2 originalRTO=%3 synthetic stationary inert target; awaiting actual uplink and HQ downlink", messageId, observedAt, m_OriginalRTO);
return true;
}
protected bool ReportKnown(ERR_HQReport report, bool accepted = true)
{
if (!report) return false;
if (!m_RefreshCase) return report.MessageId == m_Source && report.ObservedAt == m_ObservedAt;
ERR_NativeMortarRefreshRecord record = FindObservation(report.MessageId);
return record && (!accepted || record.Accepted) && record.Matches(report, m_Target, m_Group, m_Impact);
}
protected bool AllAccepted()
{
if (!m_RefreshCase) return true;
if (m_Observations.Count() < 2 || m_UpdatesAccepted != m_Observations.Count() - 1 || m_UpdatedCommits < 1) return false;
foreach (ERR_NativeMortarRefreshRecord record : m_Observations)
if (!record.Accepted || !record.Delivered || record.DownlinkId <= 0) return false;
return true;
}
protected bool NoReportReplay(float now)
{
if (!m_RefreshCase) return m_Delivered && !m_Battery.CanRequest(m_Delivered, now);
if (!AllAccepted()) return false;
foreach (ERR_NativeMortarRefreshRecord record : m_Observations)
if (m_Battery.CanRequest(record.Delivered, now)) return false;
return true;
}
// Observational only: never stop/cancel inside production's current call.
void NativeAudit(ERR_MortarBattery battery, string kind)
{
if (!m_RefreshCase || m_Stopped || battery != m_Battery || m_Source <= 0) return;
if (kind == "AIM")
{
if (m_OriginalActivity || !battery.Activity || !m_SawDownlink || battery.SourceId != m_Source || battery.OrderId != m_Order || !ReportKnown(battery.Report, false))
{ m_BadProjectile = true; m_BadReason = "initial native activity does not match recorded report/downlink or was recreated"; return; }
m_OriginalActivity = battery.Activity; m_SawActivity = true;
ERR_NativeMortarRefreshRecord initial = FindObservation(m_Source);
initial.Accepted = true; initial.Delivered = battery.Report.Snapshot();
m_Delivered = initial.Delivered;
return;
}
if (kind != "UPDATE QUEUED" && kind != "UPDATE RECEIVED") return;
ERR_MortarRefresh update = battery.Refresh;
if (!update || !update.Owns(battery) || battery.OrderId != m_Order || battery.Activity != m_OriginalActivity || !ReportKnown(update.Report, false))
{ m_BadProjectile = true; m_BadReason = "update lost recorded observation or original gun/group/order/activity"; return; }
ERR_NativeMortarRefreshRecord record = FindObservation(update.Report.MessageId);
if (kind == "UPDATE QUEUED")
{
if (record.DownlinkId != 0 || record.Accepted || !update.Route || !update.Route.Downlink || update.Route.Nodes.IsEmpty() || update.Route.MessageId != update.RadioId)
{ m_BadProjectile = true; m_BadReason = "refresh downlink was missing, duplicated or already accepted"; return; }
record.DownlinkId = update.RadioId;
return;
}
if (record.Accepted || record.DownlinkId != update.RadioId || update.Route || battery.Released || battery.Report != update.Report || !ReportKnown(battery.Report, false))
{ m_BadProjectile = true; m_BadReason = "refresh acceptance lacks its completed route or overwrites committed shell"; return; }
record.Accepted = true; record.Delivered = battery.Report.Snapshot(); m_UpdatesAccepted++;
m_Delivered = record.Delivered;
PrintFormat("ERR MORTAR REFRESH ACCEPTED: source=M%1 observedAt=%2 downlink=M%3 sameOrder=%4 originalActivity=%5", record.MessageId, record.ObservedAt, record.DownlinkId, battery.OrderId, battery.Activity == m_OriginalActivity);
}
void NativeCommit(ERR_MortarBattery battery, float now)
{
if (m_Stopped || battery != m_Battery) return;
if (m_RefreshCase && (!m_OriginalActivity || battery.Activity != m_OriginalActivity))
{ m_BadProjectile = true; m_BadReason = "refreshed commit requires the original unchanged native activity"; return; }
if (m_Commits >= ERR_MortarSalvo.ROUNDS || battery.OrderShots != m_Commits)
{ m_BadProjectile = true; m_BadReason = "commit count or physical shot sequence exceeds configured finite salvo"; return; }
if (m_Step != 1 || m_CommitPending || !battery.CommandOpen || !battery.Released || battery.SourceId != m_Source || battery.OrderId != m_Order || !ReportKnown(battery.Report) || !ERR_NativeMortarEvidenceRules.CommitFresh(battery.Report.ObservedAt, battery.Report.ObservedAt, now) || !ERR_NativeMortarEvidenceRules.AimFresh(battery.AimedAt, battery.AimValidatedAt, now) || battery.FlightTime <= 0)
{ m_BadProjectile = true; m_BadReason = "physical commit lacks immutable fresh report/current order/native aim, or duplicates pending commit"; return; }
m_CommitPending = true; m_CommittedAt = now; m_CommitOrder = battery.OrderId; m_CommitShot = battery.OrderShots; m_Commits++;
m_CommitSource = battery.Report.MessageId; m_CommitObservedAt = battery.Report.ObservedAt;
if (m_RefreshCase && m_CommitSource != m_Source) m_UpdatedCommits++;
PrintFormat("ERR MORTAR SALVO COMMIT: count=%1 order=%2 shot=%3 acceptedSource=M%4 observedAge=%5 commandAge=%6 validationAge=%7; recorded before native inventory transfer", m_Commits, m_CommitOrder, m_CommitShot, m_CommitSource, now - m_CommitObservedAt, now - battery.AimedAt, now - battery.AimValidatedAt);
}
void NativeClosing(ERR_MortarBattery battery, string reason, string failure)
{
if (m_Stopped || battery != m_Battery || !battery.CommandOpen || battery.SourceId != m_Source || m_Source <= 0) return;
m_ClosureSeen = true;
m_ClosureComplete = battery.OrderShots == ERR_MortarSalvo.ROUNDS && m_Events == ERR_MortarSalvo.ROUNDS && !m_CommitPending && m_Commits == m_Events;
PrintFormat("ERR MORTAR SALVO CLOSURE: order=%1 reason=%2 lastFailure=%3 events=%4 commits=%5 complete=%6", battery.OrderId, reason, failure, m_Events, m_Commits, m_ClosureComplete);
if (battery.Stopped() || battery.OrderId != m_Order || !m_ClosureComplete || !AllAccepted() || reason != "native artillery activity COMPLETED (shot count is separate)")
{ m_BadProjectile = true; m_BadReason = "full salvo requires natural native completion; partial expiry, rejection, timeout or Stop is not success"; }
if (m_RefreshCase && (battery.Activity != m_OriginalActivity || battery.Refresh || battery.RefreshRejected))
{ m_BadProjectile = true; m_BadReason = "natural completion cannot leave a refresh pending or replace original activity"; }
}
void NativeProjectile(IEntity gun, IEntity projectile)
{
if (m_Stopped || !m_Battery || gun != m_Gun) return;
if (!projectile) { m_BadProjectile = true; m_BadReason = "native fired event has no physical projectile"; return; }
if (m_Projectiles.Contains(projectile)) { m_BadProjectile = true; m_BadReason = "same physical projectile observed twice"; return; }
m_Projectiles.Insert(projectile); m_Events++;
float now = GetGame().GetWorld().GetWorldTime();
if (m_Step != 1 || !m_Battery.Released || m_Battery.SourceId != m_Source || !ERR_NativeMortarEvidenceRules.EventBound(m_CommitPending, m_CommitOrder, m_Battery.OrderId, m_CommitShot, m_Battery.OrderShots, m_CommittedAt, now))
{ m_BadProjectile = true; m_BadReason = "projectile lacks the recorded fresh physical commit for its current order/shot"; }
if (!m_Battery.Report || m_Battery.Report.MessageId != m_CommitSource || m_Battery.Report.ObservedAt != m_CommitObservedAt)
{ m_BadProjectile = true; m_BadReason = "committed shell was relabelled with another report before native emission"; }
m_CommitPending = false;
PrintFormat("ERR MORTAR SALVO PROJECTILE: event=%1 entity=%2 origin=%3 order=%4 acceptedSource=M%5 aimAge=%6 flight=%7; impact is not measured", m_Events, projectile, projectile.GetOrigin(), m_Battery.OrderId, m_CommitSource, now - m_Battery.AimedAt, m_Battery.FlightTime);
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (!Enabled() || s_Active != this || !m_Mode || !m_Mode.Active()) { Check(false, "explicit active fixture context retained"); return -1; }
ERR_HQ hq = ERR_HQ.Get();
ERR_MortarSystem system;
if (hq) system = hq.Mortars();
if (!system) { Check(false, "real HQ mortar service available"); return -1; }
if (!m_Battery)
{
array<IEntity> guns = {}; system.AppendGuns(guns);
if (guns.Count() == 1) { m_Gun = guns[0]; m_Battery = system.FindGun(m_Gun); if (m_Battery) m_Group = m_Battery.Group; }
}
Diagnose(now);
if (now >= m_Deadline) { Check(false, "bounded deadline; inspect readiness/route/activity/aim diagnostics"); return -1; }
if (m_BadProjectile) { Check(false, m_BadReason); return -1; }
if (!m_Battery) return 0;
if (!m_Battery.SpawnFailure.IsEmpty()) { Check(false, "battery startup failed: " + m_Battery.SpawnFailure); return -1; }
if (m_Step == 0)
{
if (!m_Battery.StartupActivated || !m_Battery.Operational() || !hq.MortarConnected(m_Battery.Group)) return 0;
m_Group = m_Battery.Group;
if (!Check(m_Group && m_Group.IsExpandComplete() && m_Group.GetAgentsCount() == 4 && m_Battery.TotalShots == 0 && m_Battery.OrderId == 0 && !m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "untasked original gun and complete four-person crew start without shots")) return -1;
array<AIAgent> agents = {}; m_Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
IEntity actor; if (agent) actor = agent.GetControlledEntity();
if (!Check(ERR_Actors.CanFight(actor) && !m_Crew.Contains(actor), "initial physical crew member is conscious and unique")) return -1;
m_Crew.Insert(actor);
}
if (!Check(m_Battery.RadioOperator && ERR_HQRadio.Capability(m_Battery.RadioOperator) == 2, "dedicated actual manpack supplies the radio route")) return -1;
m_OriginalRTO = m_Battery.RadioOperator;
Shells(m_InitialShells);
if (!Check(m_InitialShells.Count() == 27 && m_Battery.CountShells() == 27, "exactly 27 distinct physical initial HE shells")) return -1;
if (!Check(SelectImpact(system), "bounded candidate survey finds an unoccupied production-safe impact area")) return -1;
m_Target = ERR_Actors.Spawn(ERR_Resources.IntelFolder(), m_Impact);
if (!Check(m_Target != null, "fixture-owned inert report identity exists; no enemy perception is claimed")) return -1;
if (m_RefreshCase) m_Source = hq.ERR_QueueMortarRefreshReport(m_Group, m_OriginalRTO, m_Target, m_Impact, m_ObservedAt);
else { m_Source = hq.ERR_QueueMortarSalvoReport(m_Group, m_Target, m_Impact, now); m_ObservedAt = now; }
if (!Check(m_Source > 0, "synthetic observation enters actual existing radio uplink once")) return -1;
if (m_RefreshCase && !Check(AddObservation(m_Source, m_ObservedAt), "first actual-clock observation recorded without replacement")) return -1;
m_SawUplink = true; m_Deadline = now + ERR_MortarSalvo.TIMEOUT_MS + 35000; m_Step = 1; return 0;
}
if (m_Battery.Stopped()) { Check(false, "battery must remain running throughout full salvo and replay observation"); return -1; }
if (m_Battery.Report && !ReportKnown(m_Battery.Report)) { Check(false, "delivered report identity and original observation timestamp remain immutable and accepted"); return -1; }
if (m_Battery.Gun != m_Gun || m_Battery.Group != m_Group) { Check(false, "original gun and crew identity retained"); return -1; }
if (m_RefreshCase && (m_Battery.RadioOperator != m_OriginalRTO || (m_OriginalActivity && m_Battery.Activity && m_Battery.Activity != m_OriginalActivity)))
{ Check(false, "refreshes preserve original RTO and native activity"); return -1; }
if (!EmptyImpact(m_Impact)) { Check(false, "impact exclusion stays unoccupied before native firing/cancellation"); return -1; }
array<IEntity> remaining = {}; Shells(remaining);
foreach (IEntity item : remaining)
if (!m_InitialShells.Contains(item)) { Check(false, "no replacement HE shell appears after initialization"); return -1; }
if (m_Battery.OrderId > 1 || (m_Battery.SourceId != 0 && m_Battery.SourceId != m_Source)) { Check(false, "one unchanged fixture source creates only one HQ order"); return -1; }
if (m_Events > ERR_MortarSalvo.ROUNDS || m_Battery.TotalShots > ERR_MortarSalvo.ROUNDS) { Check(false, "physical event count never exceeds configured salvo"); return -1; }
if (m_Step == 1)
{
if (m_Battery.RadioOrder && m_Battery.SourceId == m_Source)
{
m_SawDownlink = true; m_Order = m_Battery.OrderId;
if (m_RefreshCase)
{
ERR_NativeMortarRefreshRecord first = FindObservation(m_Source);
if (!first || first.DownlinkId == 0)
{
if (!Check(first && m_Battery.RadioOrder.Downlink && !m_Battery.RadioOrder.Nodes.IsEmpty(), "initial order traverses actual nonempty HQ downlink")) return -1;
first.DownlinkId = m_Battery.RadioOrder.MessageId;
}
}
}
if (m_Battery.Activity && m_Battery.Report && m_Battery.Report.MessageId == m_Source)
{
m_SawActivity = true;
if (!m_Delivered) m_Delivered = m_Battery.Report.Snapshot();
}
if (m_RefreshCase && m_Battery.CommandOpen && m_Battery.Activity && !m_Observations.IsEmpty())
{
ERR_NativeMortarRefreshRecord latest = m_Observations[m_Observations.Count() - 1];
if (ERR_NativeMortarSalvoRules.QueueUpdate(latest.Accepted, m_Battery.Refresh != null, m_Battery.OrderShots, latest.ObservedAt, m_Battery.OrderedAt, now))
{
float observed;
int messageId = hq.ERR_QueueMortarRefreshReport(m_Group, m_OriginalRTO, m_Target, m_Impact, observed);
if (!Check(messageId > 0 && AddObservation(messageId, observed), "next distinct actual-clock synthetic observation queued only after previous acceptance")) return -1;
}
}
if (m_Battery.CommandOpen || m_Battery.Activity) return 0;
if (!m_ClosureSeen) return 0;
if (!Check(m_ClosureComplete && m_SawUplink && m_SawDownlink && m_SawActivity && m_Order == 1, "one real uplink/downlink and native activity naturally complete the entire configured salvo")) return -1;
if (m_RefreshCase && !Check(AllAccepted(), "every queued update was accepted under the same order before closure and at least one updated report authorized a commit")) return -1;
if (!Check(ERR_NativeMortarSalvoRules.Complete(m_Events, m_Commits, m_Battery.TotalShots, m_Battery.OrderShots, remaining.Count(), m_CommitPending) && m_Battery.CountShells() == remaining.Count(), "exact configured native projectiles equal fresh commits and missing original inventory shells")) return -1;
if (!Check(NoReportReplay(now), "all delivered messages are ineligible for a second request after completion")) return -1;
if (!Check(!m_Battery.RadioOrder && !m_Battery.Report && !m_Battery.PendingRadioReport && !m_Battery.PendingSupplyReport && !m_Battery.SupplyPending() && !m_Battery.Released && !m_Battery.Refresh, "natural completion clears command, routes, loading and projectile reservation")) return -1;
// Keep the unchanged HQ contact, battery and scheduler alive. This exceeds
// the normal 20-second post-dispatch decision interval without forging time.
m_StoppedShells = remaining.Count(); m_StopUntil = now + 25000; m_Deadline = m_StopUntil + 2000; m_Step = 2;
return 0;
}
if (m_Battery.CommandOpen || m_Battery.Activity || m_Battery.RadioOrder || m_Battery.Report || m_Battery.PendingRadioReport || m_Battery.PendingSupplyReport || m_Battery.SupplyPending() || m_Battery.Released)
{ Check(false, "completed observation must not reopen command, activity, route or physical loading"); return -1; }
if (!ERR_NativeMortarSalvoRules.Complete(m_Events, m_Commits, m_Battery.TotalShots, m_Battery.OrderShots, remaining.Count(), m_CommitPending) || m_Battery.OrderId != m_Order || remaining.Count() != m_StoppedShells)
{ Check(false, "closed order cannot add shots, orders or replacement ammunition during replay window"); return -1; }
if (now < m_StopUntil) return 0;
if (!Check(NoReportReplay(now), "all claimed contacts produce no second request, projectile or replenishment over 25 seconds")) return -1;
array<AIAgent> finalCrew = {}; m_Group.GetAgents(finalCrew);
if (!Check(finalCrew.Count() == 4 && m_Battery.Gun == m_Gun, "original group and gun retain four crew")) return -1;
foreach (AIAgent finalAgent : finalCrew)
if (!Check(finalAgent && m_Crew.Contains(finalAgent.GetControlledEntity()) && ERR_Actors.CanFight(finalAgent.GetControlledEntity()), "final conscious crew entity belongs to original physical roster")) return -1;
Stop(); return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
ERR_HQ hq = ERR_HQ.Get();
if (hq) hq.ERR_RemoveMortarSalvoReport(m_Target);
if (m_Battery && m_Battery.Gun == m_Gun && m_Battery.Group == m_Group) m_Battery.Stop();
ERR_Actors.DeleteSpawned(m_Target); m_Target = null;
if (s_Active == this) s_Active = null;
}
void ~ERR_NativeMortarSalvoProbe() { Stop(); }
}
#endif