Validate independent packed addons and complete physical mortar salvos

This commit is contained in:
2026-09-19 11:42:13 +02:00
parent 7c53b4bb81
commit dfd7cc49e2
532 changed files with 63902 additions and 96 deletions
+86 -2
View File
@@ -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 = {};