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
+85 -2
View File
@@ -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)));
}