Validate independent packed addons and complete physical mortar salvos
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
class ERR_MortarAimSolution
|
||||
{
|
||||
IEntity Gun;
|
||||
// Last delivered observation proved equivalent to this original geometry.
|
||||
ERR_HQReport Report;
|
||||
int OrderId;
|
||||
int Shot;
|
||||
|
||||
@@ -34,10 +34,15 @@ class ERR_MortarBattery
|
||||
ref ERR_HQRoute RadioOrder;
|
||||
ref ERR_HQReport PendingRadioReport;
|
||||
ref ERR_HQReport PendingSupplyReport;
|
||||
ref ERR_MortarRefresh Refresh;
|
||||
bool RefreshRejected; // latched through an already committed projectile
|
||||
protected ref ERR_MortarReadiness m_Readiness;
|
||||
protected ref ERR_MortarSupplyTransfer m_Transfer;
|
||||
protected ref array<IEntity> m_AttemptedShells = {};
|
||||
protected bool m_Stopped;
|
||||
protected SCR_AIStaticArtilleryActivity m_ObservedActivity;
|
||||
protected int m_ObservedActivityOrder;
|
||||
protected EAIActionState m_ActivityTerminal = EAIActionState.EVALUATED;
|
||||
protected ref ERR_MortarReportLedger m_ReportLedger = new ERR_MortarReportLedger();
|
||||
vector Position;
|
||||
vector Impact;
|
||||
@@ -45,6 +50,7 @@ class ERR_MortarBattery
|
||||
int AimedShot = -1;
|
||||
float FlightTime;
|
||||
float AimedAt = -1;
|
||||
float AimValidatedAt = -1;
|
||||
SCR_AIGetArtilleryAimDistanceCompensation AimSolver;
|
||||
ref ERR_MortarAimSolution AimSolution;
|
||||
float AirspaceUntil;
|
||||
@@ -135,6 +141,28 @@ class ERR_MortarBattery
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanRefresh(ERR_HQReport report, float now)
|
||||
{
|
||||
if (m_Stopped || !CommandOpen || !Activity || Refresh || RefreshRejected || Rejected || !Operational()) return false;
|
||||
return ERR_MortarRefreshRules.Newer(Report, report, now) && m_ReportLedger.Available(report.MessageId, report.ObservedAt, now);
|
||||
}
|
||||
bool BeginRefresh(ERR_HQReport report, ERR_HQRoute route, float now)
|
||||
{
|
||||
if (!route || !CanRefresh(report, now) || !m_ReportLedger.Claim(report.MessageId, report.ObservedAt, now)) return false;
|
||||
Refresh = new ERR_MortarRefresh(this, report, route);
|
||||
Audit("UPDATE QUEUED", string.Format("same order; update radio=M%1 newer observation=M%2 observed=%3", route.MessageId, report.MessageId, report.ObservedAt));
|
||||
return true;
|
||||
}
|
||||
void RejectRefresh(string reason)
|
||||
{
|
||||
// Never clear the commit latch or steal the physical projectile callback.
|
||||
// AllowShot refuses further loads now; HQ cancels after it is observed.
|
||||
RefreshRejected = true;
|
||||
Refresh = null;
|
||||
LastFailure = reason;
|
||||
Audit("UPDATE REJECTED", reason);
|
||||
}
|
||||
|
||||
void ObserveProjectile(IEntity projectileEntity, IEntity firingCharacter, bool playerGunner)
|
||||
{
|
||||
if (!projectileEntity)
|
||||
@@ -149,6 +177,7 @@ class ERR_MortarBattery
|
||||
// Only a physical projectile unlocks the next load. It must be aimed again.
|
||||
Released = false;
|
||||
AimedAt = -1;
|
||||
AimValidatedAt = -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));
|
||||
@@ -383,6 +412,52 @@ class ERR_MortarBattery
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void WatchActivity()
|
||||
{
|
||||
DetachActivity();
|
||||
if (!Activity || !CommandOpen || m_Stopped) return;
|
||||
m_ObservedActivity = Activity;
|
||||
m_ObservedActivityOrder = OrderId;
|
||||
Activity.m_OnActionCompleted.Insert(OnActivityCompleted);
|
||||
Activity.m_OnActionFailed.Insert(OnActivityFailed);
|
||||
}
|
||||
protected void DetachActivity()
|
||||
{
|
||||
if (m_ObservedActivity)
|
||||
{
|
||||
m_ObservedActivity.m_OnActionCompleted.Remove(OnActivityCompleted);
|
||||
m_ObservedActivity.m_OnActionFailed.Remove(OnActivityFailed);
|
||||
}
|
||||
m_ObservedActivity = null;
|
||||
m_ObservedActivityOrder = 0;
|
||||
m_ActivityTerminal = EAIActionState.EVALUATED;
|
||||
}
|
||||
protected void OnActivityCompleted(SCR_AIActionBase action)
|
||||
{
|
||||
ObserveActivityTerminal(action, EAIActionState.COMPLETED);
|
||||
}
|
||||
protected void OnActivityFailed(SCR_AIActionBase action)
|
||||
{
|
||||
ObserveActivityTerminal(action, EAIActionState.FAILED);
|
||||
}
|
||||
protected void ObserveActivityTerminal(SCR_AIActionBase action, EAIActionState terminal)
|
||||
{
|
||||
if (m_Stopped || !CommandOpen || !Activity || action != Activity || action != m_ObservedActivity || OrderId != m_ObservedActivityOrder) return;
|
||||
if (m_ActivityTerminal != EAIActionState.EVALUATED) return;
|
||||
if (terminal != EAIActionState.COMPLETED && terminal != EAIActionState.FAILED) return;
|
||||
// Vanilla deselection can overwrite COMPLETED/FAILED with EVALUATED
|
||||
// before the next HQ tick. Record its actual event, never infer completion
|
||||
// from projectile totals, and never cancel from inside the native callback.
|
||||
m_ActivityTerminal = terminal;
|
||||
Audit("ACTIVITY EVENT", string.Format("native terminal event=%1 current state=%2 captured order=%3 physical order shots=%4 total shots=%5 committed=%6", terminal, Activity.GetActionState(), m_ObservedActivityOrder, OrderShots, TotalShots, Released));
|
||||
}
|
||||
protected EAIActionState ActivityTerminal()
|
||||
{
|
||||
if (Activity == m_ObservedActivity && OrderId == m_ObservedActivityOrder)
|
||||
if (m_ActivityTerminal == EAIActionState.COMPLETED || m_ActivityTerminal == EAIActionState.FAILED) return m_ActivityTerminal;
|
||||
return Activity.GetActionState();
|
||||
}
|
||||
|
||||
void Cancel(string reason = "cancel requested")
|
||||
{
|
||||
if (CommandOpen)
|
||||
@@ -391,12 +466,18 @@ class ERR_MortarBattery
|
||||
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;
|
||||
// Remove before our own cleanup calls Complete; that is not a native
|
||||
// successful fire mission and must not manufacture an observed event.
|
||||
DetachActivity();
|
||||
RadioOrder = null;
|
||||
PendingRadioReport = null;
|
||||
PendingSupplyReport = null;
|
||||
Refresh = null;
|
||||
RefreshRejected = false;
|
||||
if (m_Transfer) m_Transfer.Invalidate();
|
||||
Report = null;
|
||||
AimedAt = -1;
|
||||
AimValidatedAt = -1;
|
||||
AimSolver = null;
|
||||
AimSolution = null;
|
||||
if (Activity)
|
||||
@@ -451,8 +532,9 @@ class ERR_MortarBattery
|
||||
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)");
|
||||
EAIActionState terminal = ActivityTerminal();
|
||||
if (terminal == EAIActionState.FAILED) Cancel("native artillery activity FAILED");
|
||||
else if (terminal == 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)
|
||||
@@ -468,6 +550,7 @@ class ERR_MortarBattery
|
||||
Released = false;
|
||||
UpdateCrewDetail(now, true);
|
||||
Activity = new SCR_AIStaticArtilleryActivity(Group.GetGroupUtilityComponent(), null, report.Position, SCR_EAIArtilleryAmmoType.HIGH_EXPLOSIVE, ERR_MortarSalvo.ROUNDS);
|
||||
WatchActivity();
|
||||
Group.GetGroupUtilityComponent().AddAction(Activity);
|
||||
Audit("AIM", string.Format("native activity created; latest source=M%1 observed=%2", report.MessageId, report.Description(now)));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
if (!battery)
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
battery.AimedAt = -1;
|
||||
battery.AimValidatedAt = -1;
|
||||
battery.AimSolver = null;
|
||||
battery.AimSolution = null;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
@@ -54,6 +55,7 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
battery.AimSolution = solution;
|
||||
battery.AimSolver = this; // weak: do not retain a destroyed native behavior tree
|
||||
battery.AimedAt = now;
|
||||
battery.AimValidatedAt = now;
|
||||
battery.LastFailure = "";
|
||||
battery.Trace("AIM SOLUTION", "ready", string.Format("predicted impact=%1 flight=%2s charge index=%3 aim target=%4 latest source=M%5", solution.Impact, solution.FlightTime, solution.Charge, solution.AimTarget, report.MessageId));
|
||||
SetVariableOut(PORT_AIM_POS, solution.AimTarget);
|
||||
@@ -79,11 +81,39 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
return system.Withhold(battery, "new ballistic solution requires native re-aim", string.Format("gun drift=%1m impact drift=%2m aim drift=%3m charge=%4/%5 flight=%6/%7s", vector.Distance(commanded.GunOrigin, updated.GunOrigin), vector.Distance(commanded.Impact, updated.Impact), vector.Distance(commanded.AimTarget, updated.AimTarget), commanded.Charge, updated.Charge, commanded.FlightTime, updated.FlightTime));
|
||||
// Keep the ORIGINAL command/impact/flight. Comparing successive refreshed
|
||||
// solutions would allow a moving target to drift away in small increments.
|
||||
battery.AimedAt = now;
|
||||
battery.AimValidatedAt = now;
|
||||
battery.Trace("AIM REVALIDATED", "same native command", string.Format("fresh ballistic solve matched original; source=M%1 observed age=%2s", report.MessageId, (now - report.ObservedAt) / 1000));
|
||||
return true;
|
||||
}
|
||||
|
||||
// The report may change only after a real HQ downlink. Re-solve against the
|
||||
// ORIGINAL geometry; never replace the commanded solution with an
|
||||
// approximately equivalent successor (which would permit tolerance creep).
|
||||
bool ERR_AdoptReport(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now)
|
||||
{
|
||||
IEntity gun;
|
||||
ResourceName ammo;
|
||||
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
|
||||
GetVariableIn(PORT_AMMO_PREFAB, ammo);
|
||||
ERR_MortarAimSolution commanded = battery.AimSolution;
|
||||
if (battery.Released || battery.RefreshRejected || !battery.CommandOpen || !battery.Refresh || !battery.Refresh.DeliveredFor(battery, report))
|
||||
return system.Withhold(battery, "target update lacks completed delivery for this uncommitted order");
|
||||
if (battery.AimSolver != this || !commanded || !commanded.Matches(battery, battery.Report) || gun != battery.Gun || ammo != ERR_MortarBattery.SHELL)
|
||||
return system.Withhold(battery, "target update lost its original native aim owner");
|
||||
if (battery.AimedAt < 0 || now < battery.AimedAt) return system.Withhold(battery, "original native aim timestamp invalid");
|
||||
if (!ERR_MortarRefreshRules.Newer(battery.Report, report, now)) return system.Withhold(battery, "target update is not a newer fresh same-target observation");
|
||||
ERR_MortarAimSolution updated = ERR_Solve(battery, system, report, now);
|
||||
if (!updated) return false;
|
||||
if (!ERR_MortarAimRules.Equivalent(commanded.Charge, updated.Charge, commanded.GunOrigin, updated.GunOrigin, commanded.Impact, updated.Impact, commanded.AimTarget, updated.AimTarget, commanded.FlightTime, updated.FlightTime))
|
||||
return system.Withhold(battery, "target update requires a different native aim command");
|
||||
// Synchronous adoption changes accepted observation identity together.
|
||||
// Original command time/geometry, order deadline and shot count survive.
|
||||
commanded.Report = report;
|
||||
battery.Report = report;
|
||||
battery.AimValidatedAt = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected ERR_MortarAimSolution ERR_Solve(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now)
|
||||
{
|
||||
IEntity gun = battery.Gun;
|
||||
@@ -257,12 +287,13 @@ modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
|
||||
// so repeated pending ticks cannot count the same projectile twice.
|
||||
if (battery.OrderShots < ERR_MortarSalvo.ROUNDS)
|
||||
{
|
||||
ERR_MortarSupplyState supply = battery.SupplyState(GetGame().GetWorld().GetWorldTime());
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
ERR_MortarSupplyState supply = battery.SupplyState(now);
|
||||
if (supply == ERR_MortarSupplyState.PENDING) return ENodeResult.RUNNING;
|
||||
if (supply == ERR_MortarSupplyState.FAILED)
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
if (system) system.Withhold(battery, "next native load lacks confirmed physical gunner ammunition");
|
||||
if (system) system.Withhold(battery, system.SupplyFailure(battery, now));
|
||||
return ENodeResult.FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// One newer observation in transit for one existing physical firing order.
|
||||
// The report's observed time survives both radio legs unchanged.
|
||||
class ERR_MortarRefresh
|
||||
{
|
||||
IEntity Gun;
|
||||
SCR_AIGroup Group;
|
||||
int OrderId;
|
||||
int RadioId;
|
||||
ref ERR_HQReport Report;
|
||||
ref ERR_HQRoute Route;
|
||||
|
||||
void ERR_MortarRefresh(ERR_MortarBattery battery, ERR_HQReport report, ERR_HQRoute route)
|
||||
{
|
||||
Gun = battery.Gun;
|
||||
Group = battery.Group;
|
||||
OrderId = battery.OrderId;
|
||||
RadioId = route.MessageId;
|
||||
Report = report.Snapshot();
|
||||
Route = route;
|
||||
}
|
||||
bool Owns(ERR_MortarBattery battery)
|
||||
{
|
||||
return battery && Gun && Group && Gun == battery.Gun && Group == battery.Group && OrderId == battery.OrderId;
|
||||
}
|
||||
bool DeliveredFor(ERR_MortarBattery battery, ERR_HQReport report)
|
||||
{
|
||||
return !Route && Report && Report == report && Owns(battery);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_MortarRefreshRules
|
||||
{
|
||||
static bool Newer(ERR_HQReport current, ERR_HQReport incoming, float now)
|
||||
{
|
||||
return current && incoming && current.Target && incoming.Target == current.Target && incoming.MessageId > 0 && incoming.MessageId != current.MessageId && incoming.ObservedAt > current.ObservedAt && ERR_MortarPrediction.Fresh(incoming, now);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ class ERR_MortarSystem
|
||||
protected bool m_Prepared;
|
||||
protected bool m_Stopped;
|
||||
protected float m_NextDecision;
|
||||
protected float m_NextRefreshDecision;
|
||||
string SafetyReason;
|
||||
string SettlementReason;
|
||||
|
||||
@@ -89,6 +90,13 @@ class ERR_MortarSystem
|
||||
if (!battery.Report) return "no delivered target report";
|
||||
return "delivered target report expired or invalid";
|
||||
}
|
||||
// Classify only after SupplyState has observed any accepted inventory move.
|
||||
// Report expiry must not be mislabeled as physical ammunition depletion.
|
||||
string SupplyFailure(ERR_MortarBattery battery, float now)
|
||||
{
|
||||
if (!Latest(battery, now)) return ContactFailure(battery);
|
||||
return "next native load lacks confirmed physical gunner ammunition";
|
||||
}
|
||||
bool OutsideSettlements(vector impact)
|
||||
{
|
||||
bool clear = m_Settlements.OutsideSettlements(impact);
|
||||
@@ -134,13 +142,15 @@ class ERR_MortarSystem
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_Stopped || !ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive or HQ stopped");
|
||||
if (!battery.Operational()) return Withhold(battery, battery.OperationalReason());
|
||||
if (battery.RefreshRejected) return Withhold(battery, "target update failed; order awaiting cancellation");
|
||||
if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) return Withhold(battery, "salvo complete or order closed");
|
||||
if (battery.Released) return Withhold(battery, "waiting for committed projectile");
|
||||
if (battery.AimedShot != battery.OrderShots || battery.AimedAt < 0 || now < battery.AimedAt) return Withhold(battery, "fresh aim solution required");
|
||||
ERR_HQReport report = Latest(battery, now);
|
||||
if (!report) return Withhold(battery, ContactFailure(battery));
|
||||
if (!battery.AimSolution || !battery.AimSolution.Matches(battery, report)) return Withhold(battery, "aim command does not match delivered order");
|
||||
if (now - battery.AimedAt > 10000)
|
||||
if (battery.AimValidatedAt < battery.AimedAt || now < battery.AimValidatedAt) return Withhold(battery, "aim validation timestamp invalid");
|
||||
if (now - battery.AimValidatedAt > 10000)
|
||||
{
|
||||
if (!battery.AimSolver) return Withhold(battery, "original native aim solver no longer available");
|
||||
if (!battery.AimSolver.ERR_RevalidateAim(battery, this, report, now)) return false;
|
||||
@@ -190,12 +200,84 @@ class ERR_MortarSystem
|
||||
battery.Order(ready, now);
|
||||
}
|
||||
|
||||
protected void AdvanceRefresh(ERR_MortarBattery battery, float now)
|
||||
{
|
||||
ERR_MortarRefresh update = battery.Refresh;
|
||||
if (!update) return;
|
||||
// A stale transaction must not cancel or modify a later order.
|
||||
if (update.OrderId != battery.OrderId || !battery.CommandOpen)
|
||||
{
|
||||
battery.Refresh = null;
|
||||
return;
|
||||
}
|
||||
if (!update.Owns(battery) || !battery.Operational() || !battery.Activity || !ERR_MortarRefreshRules.Newer(battery.Report, update.Report, now))
|
||||
{
|
||||
battery.RejectRefresh("target update ownership, observation freshness or activity changed");
|
||||
return;
|
||||
}
|
||||
if (update.Route)
|
||||
{
|
||||
int state = update.Route.Advance(now, m_HQ.Journal());
|
||||
if (state == 0) return;
|
||||
if (state < 0)
|
||||
{
|
||||
battery.RejectRefresh("new target observation failed physical HQ radio delivery");
|
||||
return;
|
||||
}
|
||||
update.Route = null;
|
||||
}
|
||||
// The original committed shell owns its report, geometry and airspace until
|
||||
// native OnWeaponFired confirms it. A delivered update cannot relabel it.
|
||||
if (battery.Released) return;
|
||||
if (battery.AimSolution)
|
||||
{
|
||||
if (!battery.AimSolver || !battery.AimSolver.ERR_AdoptReport(battery, this, update.Report, now))
|
||||
{
|
||||
battery.RejectRefresh("delivered update cannot preserve the original native aim command");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No barrel command exists for the next shell yet. Its normal native aim
|
||||
// node must solve this report; receiving it is not an aim authorization.
|
||||
battery.Report = update.Report;
|
||||
}
|
||||
battery.Audit("UPDATE RECEIVED", string.Format("same order; update radio=M%1 accepted observation=M%2 observed=%3; shots=%4 original aim time=%5", update.RadioId, update.Report.MessageId, update.Report.ObservedAt, battery.OrderShots, battery.AimedAt));
|
||||
battery.Refresh = null;
|
||||
}
|
||||
|
||||
protected void QueueRefreshes(float now)
|
||||
{
|
||||
if (now < m_NextRefreshDecision) return;
|
||||
m_NextRefreshDecision = now + 5000;
|
||||
array<ref ERR_HQReport> reports = {};
|
||||
m_HQ.SupportContacts(reports, now);
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
{
|
||||
ERR_HQReport newest;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
if (!battery.CanRefresh(report, now)) continue;
|
||||
if (!newest || report.ObservedAt > newest.ObservedAt) newest = report;
|
||||
}
|
||||
if (!newest) continue;
|
||||
ERR_HQRoute route = m_HQ.Downlink(battery.Group, "MORTAR UPDATE", newest.Description(now), newest.MessageId);
|
||||
if (!route)
|
||||
{
|
||||
battery.RejectRefresh("HQ has a newer observation but no physical downlink to the active battery");
|
||||
continue;
|
||||
}
|
||||
battery.BeginRefresh(newest, route, now);
|
||||
}
|
||||
}
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer()) return;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
{
|
||||
if (battery.Rejected && !battery.Released)
|
||||
if ((battery.Rejected || battery.RefreshRejected) && !battery.Released)
|
||||
{
|
||||
battery.Cancel("native aim or firing authorization rejected");
|
||||
battery.NextOrder = now + 10000;
|
||||
@@ -203,7 +285,9 @@ class ERR_MortarSystem
|
||||
battery.Rejected = false;
|
||||
battery.Update(now);
|
||||
AdvanceOrder(battery, now);
|
||||
AdvanceRefresh(battery, now);
|
||||
}
|
||||
QueueRefreshes(now);
|
||||
if (now < m_NextDecision) return;
|
||||
m_NextDecision = now + 10000;
|
||||
array<ref ERR_HQReport> reports = {};
|
||||
|
||||
@@ -57,6 +57,10 @@ modded class ERR_MortarBattery
|
||||
|
||||
class ERR_NativeMortarEvidenceRules
|
||||
{
|
||||
static bool AimFresh(float commandedAt, float validatedAt, float now)
|
||||
{
|
||||
return commandedAt >= 0 && now >= commandedAt && validatedAt >= commandedAt && now >= validatedAt && now - validatedAt <= 10000;
|
||||
}
|
||||
static bool CommitFresh(float observed, float expectedObserved, float committed)
|
||||
{
|
||||
return observed == expectedObserved && observed >= 0 && committed >= observed && committed - observed <= ERR_MortarPrediction.MAX_REPORT_AGE_MS;
|
||||
@@ -197,7 +201,7 @@ class ERR_NativeMortarFiringProbe
|
||||
void NativeCommit(ERR_MortarBattery battery, float now)
|
||||
{
|
||||
if (m_Stopped || battery != m_Battery) return;
|
||||
if ((m_Step != 1 && m_Step != 3) || m_CommitPending || !battery.CommandOpen || !battery.Released || battery.SourceId != m_Source || battery.OrderId != m_Order || !battery.Report || battery.Report.MessageId != m_Source || !ERR_NativeMortarEvidenceRules.CommitFresh(battery.Report.ObservedAt, m_ObservedAt, now) || battery.AimedAt < 0 || now < battery.AimedAt || now - battery.AimedAt > 10000 || battery.FlightTime <= 0)
|
||||
if ((m_Step != 1 && m_Step != 3) || m_CommitPending || !battery.CommandOpen || !battery.Released || battery.SourceId != m_Source || battery.OrderId != m_Order || !battery.Report || battery.Report.MessageId != m_Source || !ERR_NativeMortarEvidenceRules.CommitFresh(battery.Report.ObservedAt, m_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++;
|
||||
PrintFormat("ERR MORTAR FIXTURE COMMIT: count=%1 order=%2 shot=%3 source=M%4 observedAge=%5; recorded before native inventory transfer", m_Commits, m_CommitOrder, m_CommitShot, m_Source, now - m_ObservedAt);
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user