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
|
||||
@@ -18,6 +18,8 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestHQResponse;
|
||||
protected bool m_ERRTestMortar;
|
||||
protected bool m_ERRTestMortarFire;
|
||||
protected bool m_ERRTestMortarSalvo;
|
||||
protected ref ERR_NativeMortarSalvoProbe m_ERRMortarSalvo;
|
||||
protected bool m_ERRTestMortarCourier;
|
||||
protected ref ERR_NativeMortarCourierProbe m_ERRMortarCourier;
|
||||
protected bool m_ERRTestMortarTransfer;
|
||||
@@ -40,12 +42,14 @@ modded class ERR_GameMode
|
||||
m_ERRTestMortar = outcome == "mortar-inventory";
|
||||
m_ERRTestMortarFire = outcome == "mortar-fire" || outcome == "mortar-expiry";
|
||||
m_ERRTestMortarCourier = outcome == "mortar-courier";
|
||||
m_ERRTestMortarSalvo = outcome == "mortar-salvo" || outcome == "mortar-refresh";
|
||||
m_ERRTestMortarTransfer = outcome == "mortar-transfer";
|
||||
m_ERRTestPatrolHQ = outcome == "patrol-hq";
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
|
||||
if (m_ERRTestMortarTransfer) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarSalvo) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarFire) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestPatrolHQ) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 420000;
|
||||
OnStop().Insert(ERR_StopHQPatrol);
|
||||
@@ -66,6 +70,7 @@ modded class ERR_GameMode
|
||||
protected void ERR_StopHQPatrol()
|
||||
{
|
||||
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
|
||||
if (m_ERRMortarSalvo) m_ERRMortarSalvo.Stop();
|
||||
if (m_ERRMortarCourier) m_ERRMortarCourier.Stop();
|
||||
if (m_ERRMortarTransfer) m_ERRMortarTransfer.Stop();
|
||||
if (m_ERRMortarFire) m_ERRMortarFire.Stop();
|
||||
@@ -104,6 +109,15 @@ modded class ERR_GameMode
|
||||
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
|
||||
if (m_ERRTestMortarSalvo)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 191, "native configured salvo and unchanged-report replay observation complete before reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: native full configured mortar salvo, exact original ammunition and unchanged-report replay refusal; fixture reset requested");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarCourier)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 181, "native finite courier reached an exhausted battery before explicit reset")) return;
|
||||
@@ -193,6 +207,13 @@ modded class ERR_GameMode
|
||||
if (m_ERRSmokeDone) return;
|
||||
if (now > m_ERRSmokeDeadline) { ERR_Check(false, "timeout at step " + m_ERRSmokeStep.ToString()); return; }
|
||||
if (now < m_ERRSmokeWait) return;
|
||||
if (m_ERRSmokeStep == 190)
|
||||
{
|
||||
int result = m_ERRMortarSalvo.Tick(now);
|
||||
if (result < 0) { ERR_Check(false, "native complete mortar salvo fixture; inspect source diagnostics"); return; }
|
||||
if (result == 1) { m_ERRSmokeStep = 191; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRSmokeStep == 180)
|
||||
{
|
||||
int result = m_ERRMortarCourier.Tick(now);
|
||||
@@ -246,6 +267,13 @@ modded class ERR_GameMode
|
||||
m_ERRSmokeStep = 130;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarSalvo)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRMortarSalvo = new ERR_NativeMortarSalvoProbe(this);
|
||||
m_ERRSmokeStep = 190;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarCourier)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
|
||||
@@ -519,6 +519,9 @@ modded class ERR_GameMode
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPoweredFlightProbe = new ERR_NativePoweredFlightProbe(this);
|
||||
string autohoverMode;
|
||||
if (System.GetCLIParam("errFlightAutohover", autohoverMode) && autohoverMode == "1")
|
||||
if (!ERR_BaseCheck(m_ERRPoweredFlightProbe.SetAutohoverMode(true), "owned native autohover experiment selected before allocation")) return;
|
||||
m_ERRBaseDeadline = now + 250000;
|
||||
m_ERRBaseStep = 1800;
|
||||
return;
|
||||
|
||||
@@ -30,6 +30,22 @@ modded class ERR_NativePoweredFlightProbe
|
||||
return PoweredAuthority();
|
||||
}
|
||||
|
||||
override void PreparePilotActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
|
||||
{
|
||||
int before = m_ControlUpdates;
|
||||
super.PreparePilotActions(controller, owner, am, dt);
|
||||
if (m_ControlUpdates == before || controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
|
||||
m_ERRWrittenSequence = m_ControlUpdates;
|
||||
m_ERRWrittenAt = GetGame().GetWorld().GetWorldTime();
|
||||
// These are actual owned virtual-action values, not native consumption.
|
||||
m_ERRWrittenCollective = am.GetActionValue("HelicopterCollectiveIncrease") - am.GetActionValue("HelicopterCollectiveDecrease");
|
||||
float forward = am.GetActionValue("HelicopterCyclicForward") - am.GetActionValue("HelicopterCyclicBack");
|
||||
float aside = am.GetActionValue("HelicopterCyclicRight") - am.GetActionValue("HelicopterCyclicLeft");
|
||||
float torque = am.GetActionValue("HelicopterAntiTorqueRight") - am.GetActionValue("HelicopterAntiTorqueLeft");
|
||||
m_ERRWrittenCyclic = Vector(forward, aside, torque);
|
||||
m_ERRWrittenThrottle = m_Input.GetVehicleThrottle(); // observation only in action mode
|
||||
}
|
||||
|
||||
override void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
{
|
||||
// Snapshot native context before this callback's owned final writes.
|
||||
|
||||
@@ -5,8 +5,9 @@ modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
|
||||
{
|
||||
super.OnPrepareControls(owner, am, dt, player);
|
||||
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
|
||||
if (probe && !player) probe.PreparePilotActions(this, owner, am, dt);
|
||||
super.OnPrepareControls(owner, am, dt, player);
|
||||
if (probe && !player) probe.ObservePilotPreparation(this, owner);
|
||||
}
|
||||
protected override void OnApplyControls(IEntity owner, float timeSlice)
|
||||
@@ -78,11 +79,24 @@ class ERR_PoweredFlightRules
|
||||
}
|
||||
static float NativeCollective(float signedDemand)
|
||||
{
|
||||
// The native apply stage clamps negative collective to zero (100422).
|
||||
// Keep the feedback controller's signed state; encode its final demand in
|
||||
// the nonnegative native collective domain, unlike signed cyclic/torque.
|
||||
// Preserve the legacy Apply experiment unchanged. Later observations showed
|
||||
// overwritten positive inputs too, so this is not a proven native domain.
|
||||
return (ClampInput(signedDemand) + 1) * 0.5;
|
||||
}
|
||||
static float CollectiveCommand(float signedDemand, bool actions, int stage)
|
||||
{
|
||||
// Preserve pre-engine/startup zero and the old Apply comparison branch.
|
||||
if (!actions) return NativeCollective(signedDemand);
|
||||
if (stage == 9) return 0.05;
|
||||
if (stage == 10) return -0.05;
|
||||
if (stage == 7) return -1; // keep minimum pitch while the stopped engine's rotors coast
|
||||
if (stage >= 4 && stage <= 6) return ClampInput(signedDemand);
|
||||
return 0;
|
||||
}
|
||||
static bool NegativeCollectiveObserved(float command, float signal)
|
||||
{
|
||||
return command >= -0.06 && command <= -0.04 && Math.AbsFloat(signal - command) <= 0.01;
|
||||
}
|
||||
static bool CollectiveObserved(float command, float signal)
|
||||
{
|
||||
return command >= 0.04 && command <= 0.06 && Math.AbsFloat(signal - command) <= 0.01;
|
||||
@@ -134,6 +148,9 @@ class ERR_NativePoweredFlightProbe
|
||||
protected int m_PilotPreparations;
|
||||
protected int m_PositiveGateUpdates;
|
||||
protected float m_PositiveSignalSince = -1;
|
||||
protected int m_NegativeGateUpdates;
|
||||
protected float m_NegativeSignalSince = -1;
|
||||
protected bool m_SignedCollectiveProven;
|
||||
protected int m_HelicopterUpdates;
|
||||
protected bool m_PilotSpawnRequested;
|
||||
protected AIAgent m_PilotAgent;
|
||||
@@ -147,6 +164,14 @@ class ERR_NativePoweredFlightProbe
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
protected bool m_ReadbackOK = true;
|
||||
protected bool m_ActionInputs;
|
||||
protected bool m_Started;
|
||||
protected bool m_AutohoverMode;
|
||||
protected bool m_AutohoverSaved;
|
||||
protected bool m_OriginalAutohover;
|
||||
protected HelicopterControllerComponent m_AutohoverController;
|
||||
protected ActionManager m_OwnedActions;
|
||||
protected int m_ActionContextSamples;
|
||||
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
|
||||
static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";
|
||||
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
@@ -158,10 +183,48 @@ class ERR_NativePoweredFlightProbe
|
||||
static ERR_NativePoweredFlightProbe Current() { return s_Current; }
|
||||
void ERR_NativePoweredFlightProbe(ERR_GameMode mode)
|
||||
{
|
||||
string actionInputs;
|
||||
m_ActionInputs = System.GetCLIParam("errFlightActionInputs", actionInputs) && actionInputs == "1";
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 240000;
|
||||
if (Enabled() && !s_Current) s_Current = this;
|
||||
else m_Stopped = true;
|
||||
}
|
||||
// Fixture configuration only: caller must select this before the first Tick.
|
||||
bool SetAutohoverMode(bool enabled)
|
||||
{
|
||||
if (m_Started || m_Stopped || m_Complete || m_Bound || m_Aircraft) return false;
|
||||
m_AutohoverMode = enabled;
|
||||
return true;
|
||||
}
|
||||
protected bool BeginAutohover()
|
||||
{
|
||||
if (!m_AutohoverMode) return true;
|
||||
if (m_AutohoverSaved || !m_Bound || !PoweredAuthority() || !m_Controller || m_Controller.GetOwner() != m_Aircraft) return false;
|
||||
m_AutohoverController = m_Controller;
|
||||
m_OriginalAutohover = m_Controller.GetAutohoverEnabled();
|
||||
m_AutohoverSaved = true;
|
||||
m_Controller.SetAutohoverEnabled(true);
|
||||
return m_Controller.GetAutohoverEnabled();
|
||||
}
|
||||
protected bool AutohoverReady()
|
||||
{
|
||||
if (!m_AutohoverMode) return true;
|
||||
// Native autohover also rewrites collective. Prove direct input consumption
|
||||
// first, then enable its controller only for the bounded flight phase.
|
||||
if (!m_AutohoverSaved && (m_Stage == 2 || m_Stage == 3 || m_Stage == 9 || m_Stage == 10)) return true;
|
||||
return m_AutohoverSaved && m_AutohoverController && m_AutohoverController == m_Controller && m_Controller.GetOwner() == m_Aircraft && m_Controller.GetAutohoverEnabled();
|
||||
}
|
||||
protected void RestoreAutohover(bool ownedSeat)
|
||||
{
|
||||
if (ownedSeat && m_AutohoverSaved && m_AutohoverController && m_AutohoverController == m_Controller && m_Controller.GetOwner() == m_Aircraft)
|
||||
{
|
||||
m_Controller.SetAutohoverEnabled(m_OriginalAutohover);
|
||||
PrintFormat("ERR POWERED AUTOHOVER RESTORE: original=%1 observed=%2", m_OriginalAutohover, m_Controller.GetAutohoverEnabled());
|
||||
if (m_Controller.GetAutohoverEnabled() != m_OriginalAutohover) Print("ERR SMOKE FAIL: powered-flight original owned autohover state restored", LogLevel.ERROR);
|
||||
}
|
||||
m_AutohoverSaved = false;
|
||||
m_AutohoverController = null;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: powered-flight " + detail); return true; }
|
||||
@@ -200,22 +263,24 @@ class ERR_NativePoweredFlightProbe
|
||||
// Override only this owned fixture controller, never global/user settings.
|
||||
controller.SetCollectiveMode(CollectiveMode.Direct); m_HelicopterUpdates++;
|
||||
}
|
||||
bool UsesActionInputs() { return m_ActionInputs; }
|
||||
void ObservePilotPreparation(CharacterControllerComponent controller, IEntity owner)
|
||||
{
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority()) return;
|
||||
m_PilotPreparations++;
|
||||
}
|
||||
// Native prepare inputs are overwritten before application (101342).
|
||||
// Integrate and write once here, after vanilla OnApplyControls, never in both.
|
||||
void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
// Both selectable input paths share one computation. Only the selected
|
||||
// path integrates per callback; neither path invokes the other writer.
|
||||
protected bool ComputePilotDemand(CharacterControllerComponent controller, IEntity owner, float dt, out float collective, out vector axes)
|
||||
{
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority() || !m_Input || !m_Simulation || dt <= 0 || dt > 0.2) return;
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority() || !AutohoverReady() || !m_Input || !m_Simulation || dt <= 0 || dt > 0.2) return false;
|
||||
if (m_ActionInputs && m_Stage >= 4 && m_Stage <= 6 && !m_SignedCollectiveProven) return false;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector velocity = m_Simulation.GetVelocity();
|
||||
vector angular = m_Simulation.GetAngularVelocityLocal();
|
||||
float height = transform[3][1] - m_Home[1];
|
||||
float forward, aside, torque;
|
||||
float collective = -1;
|
||||
collective = -1;
|
||||
if (m_Stage == 9) collective = -0.9; // native0.05, bounded signal-consumption proof
|
||||
if (m_Stage >= 4 && m_Stage <= 6 && m_Controller.IsEngineOn())
|
||||
{
|
||||
@@ -224,18 +289,73 @@ class ERR_NativePoweredFlightProbe
|
||||
m_Collective = Math.Clamp(m_Collective + error * dt * 0.12, -0.95, 0.8);
|
||||
collective = Math.Clamp(m_Collective + error * 0.12, -1, 0.9);
|
||||
vector offset = m_Home - transform[3]; offset[1] = 0;
|
||||
vector acceleration = offset * 0.08 - velocity * 0.3; acceleration[1] = 0;
|
||||
// Native105226 already brakes before8m refusal; increase only velocity damping.
|
||||
vector acceleration = offset * 0.08 - velocity * 0.6; acceleration[1] = 0;
|
||||
// Small local cyclic corrections counter drift/tilt through native inputs.
|
||||
// Sign/gain and native Direct-mode response remain experimental evidence.
|
||||
forward = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[2]) / 9.81 + transform[2][1]) * 0.8 - angular[0] * 0.3, 0.15);
|
||||
aside = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[0]) / 9.81 + transform[0][1]) * 0.8 + angular[2] * 0.3, 0.15);
|
||||
float headingError = m_Heading - m_Aircraft.GetAngles()[0];
|
||||
forward = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[2]) / 9.81 + transform[2][1]) * 0.8 - angular[0] * 1.2, 0.15);
|
||||
aside = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[0]) / 9.81 + transform[0][1]) * 0.8 + angular[2] * 1.2, 0.15);
|
||||
// Native autohover also adjusts collective; observe its physical response.
|
||||
// Give its lateral controller zero custom cyclic in this experiment.
|
||||
// Preserve collective/yaw and bounds; never combine two cyclic controllers.
|
||||
if (m_AutohoverMode) { forward = 0; aside = 0; }
|
||||
float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0];
|
||||
while (headingError > 180) headingError -= 360;
|
||||
while (headingError < -180) headingError += 360;
|
||||
torque = ERR_PoweredFlightRules.ClampInput(headingError * 0.01 - angular[1] * 0.3, 0.3);
|
||||
if (m_Stage == 6 && ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, velocity.Length(), angular.Length(), transform[1][1])) collective = -1;
|
||||
}
|
||||
collective = ERR_PoweredFlightRules.NativeCollective(collective);
|
||||
collective = ERR_PoweredFlightRules.CollectiveCommand(collective, m_ActionInputs, m_Stage);
|
||||
axes = Vector(forward, aside, torque);
|
||||
return true;
|
||||
}
|
||||
protected bool WriteActionPair(ActionManager am, string positive, string negative, float demand)
|
||||
{
|
||||
float a = Math.Clamp(demand, 0, 1);
|
||||
float b = Math.Clamp(-demand, 0, 1);
|
||||
am.SetActionValue(positive, a); am.SetActionValue(negative, b);
|
||||
return Math.AbsFloat(am.GetActionValue(positive) - a) < 0.001 && Math.AbsFloat(am.GetActionValue(negative) - b) < 0.001;
|
||||
}
|
||||
protected bool OwnActionManager(ActionManager am)
|
||||
{
|
||||
// Read the global manager only to reject shared ownership. Never write it.
|
||||
return am && m_PilotAgent && am == m_PilotAgent.GetActionManager() && am != GetGame().GetInputManager();
|
||||
}
|
||||
void PreparePilotActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
|
||||
{
|
||||
if (!m_ActionInputs || !m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority()) return;
|
||||
if (!OwnActionManager(am)) { m_ReadbackOK = false; return; }
|
||||
m_OwnedActions = am;
|
||||
// Same short activation pattern used by vanilla game.c; this manager belongs
|
||||
// solely to the fixture AI, never to global hardware or the human player.
|
||||
// Native104256 reports false from ActivateContext while already active.
|
||||
// Treat observed active state as authority, not an undocumented return meaning.
|
||||
bool alreadyActive = am.IsContextActive("HelicopterContext");
|
||||
bool activated;
|
||||
if (!alreadyActive) activated = am.ActivateContext("HelicopterContext", 1);
|
||||
bool contextActive = am.IsContextActive("HelicopterContext");
|
||||
if (m_ActionContextSamples < 4)
|
||||
{
|
||||
m_ActionContextSamples++;
|
||||
PrintFormat("ERR POWERED ACTION CONTEXT: sample=%1 activated=%2 active=%3 dt=%4 input=%5 simulation=%6", m_ActionContextSamples, activated, contextActive, dt, m_Input != null, m_Simulation != null);
|
||||
}
|
||||
if (!contextActive) { m_ReadbackOK = false; return; }
|
||||
float collective; vector axes;
|
||||
if (!ComputePilotDemand(controller, owner, dt, collective, axes)) return;
|
||||
bool a = WriteActionPair(am, "HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", collective);
|
||||
bool b = WriteActionPair(am, "HelicopterCyclicForward", "HelicopterCyclicBack", axes[0]);
|
||||
bool c = WriteActionPair(am, "HelicopterCyclicRight", "HelicopterCyclicLeft", axes[1]);
|
||||
bool d = WriteActionPair(am, "HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", axes[2]);
|
||||
m_ReadbackOK = m_ReadbackOK && a && b && c && d;
|
||||
m_ControlUpdates++; m_LastControl = GetGame().GetWorld().GetWorldTime();
|
||||
}
|
||||
void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
{
|
||||
// The experimental actor-action path and old context path never both write.
|
||||
if (m_ActionInputs) return;
|
||||
float collective; vector axes;
|
||||
if (!ComputePilotDemand(controller, owner, dt, collective, axes)) return;
|
||||
float forward = axes[0]; float aside = axes[1]; float torque = axes[2];
|
||||
m_Input.SetVehicleThrottle(1);
|
||||
m_Input.SetVehicleCollective(collective);
|
||||
m_Input.SetVehicleCyclicForward(forward);
|
||||
@@ -294,8 +414,20 @@ class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
if (!m_Simulation || !m_Aircraft || now < m_NextLog) return;
|
||||
m_NextLog = now + 2000;
|
||||
if (m_Stage == 4 && now < m_StageDeadline - 57000) m_NextLog = now + 250;
|
||||
if (m_ActionInputs)
|
||||
{
|
||||
bool ownedActions = OwnActionManager(m_OwnedActions);
|
||||
bool contextActive = m_OwnedActions && m_OwnedActions.IsContextActive("HelicopterContext");
|
||||
PrintFormat("ERR POWERED ACTION MANAGER: selected=1 exactOwnedNonGlobal=%1 contextActive=%2 virtualWrites=%3", ownedActions, contextActive, m_ControlUpdates);
|
||||
}
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
PrintFormat("ERR POWERED AUTOHOVER: selected=%1 saved=%2 enabled=%3 exactController=%4", m_AutohoverMode, m_AutohoverSaved, m_Controller.GetAutohoverEnabled(), m_AutohoverController == m_Controller);
|
||||
PrintFormat("ERR POWERED FLIGHT: stage=%1 height=%2 drift=%3 speed=%4 vertical=%5 up=%6 groundContact=%7 fuel=%8 engine=%9", m_Stage, transform[3][1] - m_Home[1], vector.DistanceXZ(transform[3], m_Home), m_Simulation.GetVelocity().Length(), m_Simulation.GetVelocity()[1], transform[1][1], m_Simulation.HasAnyGroundContact(), m_Fuel.GetTotalFuel(), m_Controller.IsEngineOn());
|
||||
vector rotorAxis; m_Simulation.RotorGetAxis(0, rotorAxis);
|
||||
PrintFormat("ERR POWERED HEADING: desiredYaw=%1 actualYawPitchRoll=%2", m_Heading, m_Aircraft.GetYawPitchRoll());
|
||||
PrintFormat("ERR POWERED ROTOR AXIS: local=%1 acceleration=%2", rotorAxis, m_Simulation.GetAcceleration());
|
||||
PrintFormat("ERR POWERED MOTION: offset=%1 velocity=%2 angularLocal=%3 forward=%4 right=%5", m_Home - transform[3], m_Simulation.GetVelocity(), m_Simulation.GetAngularVelocityLocal(), transform[2], transform[0]);
|
||||
PrintFormat("ERR POWERED INPUT: pilotHooks=%1 vehicleHooks=%2 readback=%3 collective=%4 cyclicF=%5 cyclicR=%6 antiTorque=%7 rotorRPM=%8 targetRPM=%9", m_ControlUpdates, m_HelicopterUpdates, m_ReadbackOK, m_Input.GetVehicleCollective(), m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque(), m_Simulation.RotorGetRPM(0), m_Simulation.RotorGetRPMTarget(0));
|
||||
}
|
||||
protected bool PollNativeFlightComponents(float now)
|
||||
@@ -378,14 +510,24 @@ class ERR_NativePoweredFlightProbe
|
||||
if (m_Stopped) return;
|
||||
// Input cleanup is confined to the same unclaimed test pilot/aircraft.
|
||||
bool ours = m_Bound && OwnPilot() && !PlayerPresent();
|
||||
bool ownSeat = ours && m_Seat && m_Seat.GetOccupant() == m_Pilot && m_Controller && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
RestoreAutohover(ownSeat);
|
||||
m_Stopped = true; m_Bound = false;
|
||||
if (s_Current == this) s_Current = null;
|
||||
if (ours && m_Input)
|
||||
if (ours && m_ActionInputs && OwnActionManager(m_OwnedActions))
|
||||
{
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCyclicForward", "HelicopterCyclicBack", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCyclicRight", "HelicopterCyclicLeft", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", 0);
|
||||
}
|
||||
if (ours && !m_ActionInputs && m_Input)
|
||||
{
|
||||
m_Input.SetVehicleCollective(0); m_Input.SetVehicleThrottle(0);
|
||||
m_Input.SetVehicleCyclicForward(0); m_Input.SetVehicleCyclicAside(0); m_Input.SetVehicleAntiTorque(0);
|
||||
if (m_Controller && m_ModeSaved) m_Controller.SetCollectiveMode(m_OriginalMode);
|
||||
}
|
||||
if (ours && m_Controller && m_ModeSaved) m_Controller.SetCollectiveMode(m_OriginalMode);
|
||||
m_OwnedActions = null;
|
||||
if (m_PilotLODSaved && m_PilotAgent && OwnPilot() && !PlayerPresent()) m_PilotAgent.SetPermanentLOD(m_OriginalPilotLOD);
|
||||
if (m_Group && m_Group.GetPlayerCount(true) == 0) m_Group.SetPermanentLOD(-1);
|
||||
// Keep real airborne/failed test actors for diagnosis and mode-owned cleanup.
|
||||
@@ -395,8 +537,9 @@ class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
m_Started = true;
|
||||
if (!Enabled() || !m_Mode || now >= m_Deadline || ERR_MissionRules.Terminal(m_Mode.Phase())) { Check(false, "server fixture active within 240-second bound"); return -1; }
|
||||
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) { m_Stable = -1; m_PositiveSignalSince = -1; }
|
||||
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) { m_Stable = -1; m_PositiveSignalSince = -1; m_NegativeSignalSince = -1; }
|
||||
m_PreviousTick = now;
|
||||
if (m_Stage == 0)
|
||||
{
|
||||
@@ -448,13 +591,13 @@ class ERR_NativePoweredFlightProbe
|
||||
m_Character = CharacterControllerComponent.Cast(m_Pilot.FindComponent(CharacterControllerComponent));
|
||||
if (m_Character) m_Input = m_Character.GetInputContext();
|
||||
if (!Check(m_Input && m_Group.GetAgentsCount() == 1 && PoweredAuthority(), "one living native AI pilot physically occupies its own fueled helicopter")) return -1;
|
||||
m_Home = m_Aircraft.GetOrigin(); m_Heading = m_Aircraft.GetAngles()[0];
|
||||
m_Home = m_Aircraft.GetOrigin(); m_Heading = m_Aircraft.GetYawPitchRoll()[0];
|
||||
m_OriginalMode = m_Controller.GetCollectiveMode(); m_ModeSaved = true;
|
||||
m_InitialFuel = m_Fuel.GetTotalFuel(); m_Bound = true;
|
||||
m_Stage = 2; m_StageDeadline = now + 5000; return 0;
|
||||
}
|
||||
Diagnostic(now);
|
||||
if (!PoweredAuthority() || !m_ReadbackOK) { Check(false, "current owned conscious pilot, fuel, rotors and exact input readback remain valid"); return -1; }
|
||||
if (!PoweredAuthority() || !m_ReadbackOK || !AutohoverReady()) { Check(false, "current owned conscious pilot, fuel, rotors, selected autohover and exact input readback remain valid"); return -1; }
|
||||
if (m_Stage == 2)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "native AI input and helicopter preparation hooks execute before engine start"); return -1; }
|
||||
@@ -487,7 +630,7 @@ class ERR_NativePoweredFlightProbe
|
||||
m_PositiveGateUpdates = m_ControlUpdates; m_PositiveSignalSince = -1;
|
||||
m_Stage = 9; m_StageDeadline = now + 3000; return 0;
|
||||
}
|
||||
if (((m_Stage >= 4 && m_Stage <= 6) || m_Stage == 9) && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
|
||||
if (((m_Stage >= 4 && m_Stage <= 6) || m_Stage == 9 || m_Stage == 10) && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
|
||||
if (m_Stage == 9)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "positive native collective reaches actual rotor signal before climb within three seconds"); return -1; }
|
||||
@@ -502,6 +645,31 @@ class ERR_NativePoweredFlightProbe
|
||||
if (m_PositiveSignalSince < 0) m_PositiveSignalSince = now;
|
||||
if (now - m_PositiveSignalSince < 500) return 0;
|
||||
if (!Check(true, "positive collective persists in existing native rotor signal across actual ticks before climb")) return -1;
|
||||
if (m_ActionInputs)
|
||||
{
|
||||
m_NegativeGateUpdates = m_ControlUpdates; m_NegativeSignalSince = -1;
|
||||
m_Stage = 10; m_StageDeadline = now + 3000; return 0;
|
||||
}
|
||||
if (!Check(BeginAutohover(), "selected native autohover enabled after direct collective consumption proof; exact owned controller readback")) return -1;
|
||||
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
|
||||
}
|
||||
if (m_Stage == 10)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "negative actor collective reaches actual rotor signal before climb within three seconds"); return -1; }
|
||||
if (!Check(m_ActionInputs && m_Controller.GetCollectiveMode() == CollectiveMode.Direct && !m_Controller.GetAutohoverEnabled(), "negative consumption proof uses owned actor actions in Direct mode with autohover off")) return -1;
|
||||
SignalsManagerComponent negativeSignals = m_Controller.GetSignalsManager();
|
||||
int negativeIndex = -1;
|
||||
if (negativeSignals) negativeIndex = negativeSignals.FindSignal("MainRotorCollective");
|
||||
if (!Check(negativeIndex >= 0, "existing native main rotor collective signal available for negative proof")) return -1;
|
||||
float negativeObserved = negativeSignals.GetSignalValue(negativeIndex);
|
||||
bool negativeMatched = m_ControlUpdates >= m_NegativeGateUpdates + 3 && ERR_PoweredFlightRules.NegativeCollectiveObserved(m_Input.GetVehicleCollective(), negativeObserved);
|
||||
PrintFormat("ERR POWERED NEGATIVE COLLECTIVE GATE: command=%1 rotorSignal=%2 actionUpdates=%3 matched=%4", m_Input.GetVehicleCollective(), negativeObserved, m_ControlUpdates - m_NegativeGateUpdates, negativeMatched);
|
||||
if (!negativeMatched) { m_NegativeSignalSince = -1; return 0; }
|
||||
if (m_NegativeSignalSince < 0) m_NegativeSignalSince = now;
|
||||
if (now - m_NegativeSignalSince < 500) return 0;
|
||||
if (!Check(true, "negative actor collective persists in existing native rotor signal across actual ticks before signed flight demand")) return -1;
|
||||
m_SignedCollectiveProven = true;
|
||||
if (!Check(BeginAutohover(), "selected native autohover enabled only after both direct collective consumption proofs")) return -1;
|
||||
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
|
||||
}
|
||||
if (m_Stage == 4 || m_Stage == 5)
|
||||
@@ -532,6 +700,7 @@ class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "normal engine shutdown completed on actual ground"); return -1; }
|
||||
if (m_Controller.IsEngineOn()) return 0;
|
||||
PrintFormat("ERR POWERED SHUTDOWN: height=%1 speed=%2 angular=%3 up=%4 contact=%5 authority=%6 collective=%7 rotorRPM=%8", height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1], m_Simulation.HasAnyGroundContact(), PoweredAuthority(), m_Input.GetVehicleCollective(), m_Simulation.RotorGetRPM(0));
|
||||
if (!Check(ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1]) && PoweredAuthority(), "native engine off after contact landing; no transform/velocity/health/fuel correction")) return -1;
|
||||
Stop(); m_Complete = true; return 1;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class ERR_ObjectiveText
|
||||
}
|
||||
static string Extraction(float radius)
|
||||
{
|
||||
return string.Format("The map marks the initial helicopter pickup site. Board the helicopter and fly beyond %1 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.", radius / 1000.0);
|
||||
return string.Format("The map marks the initial helicopter pickup site. Board the helicopter and fly beyond %1 kilometres from the prison. To load an unconscious prisoner, place them on the ground beside the grounded, stationary helicopter and use 'Load casualty into extraction helicopter' on them. Living prisoners aboard escape; dead or absent prisoners do not.", radius / 1000.0);
|
||||
}
|
||||
static string RadioFeedbackMessage(ERR_RadioFeedback feedback)
|
||||
{
|
||||
@@ -46,8 +46,8 @@ class ERR_ObjectiveText
|
||||
mode.Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
|
||||
return "The radio station is identified on your map. Reach the station and tune its radio to start the transmission.";
|
||||
return "Choose either marked supply cache. Each has equipment and enough radio documents to identify the station. If one cache is lost, use the other. If every document is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can be discovered by approaching with a clear view.";
|
||||
return "The radio station is identified on your map. Reach its radio and complete the 'Contact extraction HQ' action to start the transmission.";
|
||||
return "Choose either marked supply cache. Take its intelligence folders into your own inventory to reveal the station on your map. If only a search area appears, take another folder. Each cache has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
|
||||
}
|
||||
if (phase == ERR_Phase.RADIO_DEFENCE)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,29 @@ enum ERR_ObserverReason
|
||||
SPAWN_UNAVAILABLE
|
||||
}
|
||||
|
||||
// Presentation only. Existing server admission and observer selection own state.
|
||||
class ERR_ObserverGuidance
|
||||
{
|
||||
static string Message(int reason, bool survivorAvailable)
|
||||
{
|
||||
if (reason == ERR_ObserverReason.NONE) return "";
|
||||
string message = "You are spectating until the next round.";
|
||||
if (reason == ERR_ObserverReason.SPENT_LIFE)
|
||||
message = "Your life for this round has been used. You can play again next round.";
|
||||
else if (reason == ERR_ObserverReason.LATE_EXTRACTION)
|
||||
message = "Extraction has started. New prisoners can join next round.";
|
||||
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
|
||||
message = "Your prisoner could not be spawned. You are spectating until the next round.";
|
||||
if (!survivorAvailable)
|
||||
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
||||
return message + " Use /esc spectate next or /esc spectate previous to change who you follow.";
|
||||
}
|
||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||
{
|
||||
return reason != ERR_ObserverReason.NONE && (!shown || previousReason != reason || previousAvailable != survivorAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
// A local follow camera. It never possesses its target or offers free flight.
|
||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
@@ -66,6 +89,8 @@ modded class SCR_PlayerController
|
||||
protected SCR_ManualCamera m_ERRObserverCamera;
|
||||
protected bool m_ERRObserverMonitor;
|
||||
protected bool m_ERRObserverReady;
|
||||
protected ERR_ObserverReason m_ERRShownObserverReason;
|
||||
protected bool m_ERRShownObserverAvailable;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
|
||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||
@@ -197,11 +222,17 @@ modded class SCR_PlayerController
|
||||
if (!manager || manager.CurrentCamera() != m_ERRObserverCamera) return;
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) respawn.DestroyLoadingPlaceholder();
|
||||
if (!m_ERRObserverReady)
|
||||
// The server-selected replicated ID distinguishes no target from a target
|
||||
// whose local entity is still streaming. Never imply no survivors from delay.
|
||||
bool survivorAvailable = m_ERRObservedId != RplId.Invalid();
|
||||
if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable))
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (hints) hints.ShowCustom("You are spectating the surviving prisoners until the next round.", "Spectating", 12);
|
||||
if (!hints) return; // retry on the existing monitor; do not lose the first hint
|
||||
hints.ShowCustom(ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable), "Spectating", 12);
|
||||
m_ERRObserverReady = true;
|
||||
m_ERRShownObserverReason = m_ERRObserverReason;
|
||||
m_ERRShownObserverAvailable = survivorAvailable;
|
||||
}
|
||||
}
|
||||
protected void ERR_CloseObserverCamera()
|
||||
|
||||
Reference in New Issue
Block a user