Advance standalone admission, finite artillery and native validation
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
// The stored geometry is the command actually emitted to the native aiming
|
||||
// tree, never a chain of refreshed approximations. Entity/report/node links
|
||||
// are weak; cancellation and physical projectile observation clear ownership.
|
||||
class ERR_MortarAimSolution
|
||||
{
|
||||
IEntity Gun;
|
||||
ERR_HQReport Report;
|
||||
int OrderId;
|
||||
int Shot;
|
||||
vector GunOrigin;
|
||||
vector Impact;
|
||||
vector AimTarget;
|
||||
vector SalvoOffset;
|
||||
int Charge;
|
||||
float FlightTime;
|
||||
|
||||
bool Matches(ERR_MortarBattery battery, ERR_HQReport report)
|
||||
{
|
||||
return battery && Gun && Gun == battery.Gun && Report && Report == report && OrderId == battery.OrderId && Shot == battery.OrderShots;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_MortarAimRules
|
||||
{
|
||||
// Three-dimensional tolerances include terrain/aim-height changes. These are
|
||||
// equivalence limits, not permission to move the gun or redirect its barrel.
|
||||
static bool Equivalent(int commandedCharge, int updatedCharge, vector commandedOrigin, vector updatedOrigin, vector commandedImpact, vector updatedImpact, vector commandedAim, vector updatedAim, float commandedFlight, float updatedFlight)
|
||||
{
|
||||
return commandedCharge >= 0 && commandedCharge == updatedCharge && commandedFlight > 0 && updatedFlight > 0 && vector.Distance(commandedOrigin, updatedOrigin) <= 0.05 && vector.Distance(commandedImpact, updatedImpact) <= 0.1 && vector.Distance(commandedAim, updatedAim) <= 0.1 && Math.AbsFloat(commandedFlight - updatedFlight) <= 0.05;
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,20 @@ class ERR_MortarBattery
|
||||
ref ERR_HQReport Report;
|
||||
ref ERR_HQRoute RadioOrder;
|
||||
ref ERR_HQReport PendingRadioReport;
|
||||
ref ERR_HQReport PendingSupplyReport;
|
||||
protected ref ERR_MortarReadiness m_Readiness;
|
||||
protected ref ERR_MortarSupplyTransfer m_Transfer;
|
||||
protected ref array<IEntity> m_AttemptedShells = {};
|
||||
protected bool m_Stopped;
|
||||
protected ref ERR_MortarReportLedger m_ReportLedger = new ERR_MortarReportLedger();
|
||||
vector Position;
|
||||
vector Impact;
|
||||
vector AimOffset;
|
||||
int AimedShot = -1;
|
||||
float FlightTime;
|
||||
float AimedAt = -1;
|
||||
SCR_AIGetArtilleryAimDistanceCompensation AimSolver;
|
||||
ref ERR_MortarAimSolution AimSolution;
|
||||
float AirspaceUntil;
|
||||
vector AirspaceImpact;
|
||||
float AirspaceRadius = 600;
|
||||
@@ -94,6 +102,7 @@ class ERR_MortarBattery
|
||||
|
||||
string OperationalReason()
|
||||
{
|
||||
if (m_Stopped) return "battery stopped";
|
||||
if (!Gun) return "gun missing";
|
||||
if (!SpawnFailure.IsEmpty()) return SpawnFailure;
|
||||
if (!Group || !Group.GetGroupUtilityComponent()) return "crew group missing";
|
||||
@@ -105,8 +114,13 @@ class ERR_MortarBattery
|
||||
return "";
|
||||
}
|
||||
|
||||
void BeginRequest(ERR_HQReport report, float now)
|
||||
bool CanRequest(ERR_HQReport report, float now)
|
||||
{
|
||||
return !m_Stopped && ERR_MortarPrediction.Fresh(report, now) && m_ReportLedger.Available(report.MessageId, report.ObservedAt, now);
|
||||
}
|
||||
bool BeginRequest(ERR_HQReport report, float now)
|
||||
{
|
||||
if (!CanRequest(report, now) || !m_ReportLedger.Claim(report.MessageId, report.ObservedAt, now)) return false;
|
||||
Cancel("superseded by new HQ order");
|
||||
FlushTraces();
|
||||
OrderId++;
|
||||
@@ -118,6 +132,7 @@ class ERR_MortarBattery
|
||||
OrderedAt = now;
|
||||
LastFailure = "";
|
||||
Audit("COMMAND", string.Format("requested rounds=%1 HE radius=%2m; %3", ERR_MortarSalvo.ROUNDS, ERR_MortarSalvo.RADIUS_METRES, report.Description(now)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ObserveProjectile(IEntity projectileEntity, IEntity firingCharacter, bool playerGunner)
|
||||
@@ -134,6 +149,8 @@ class ERR_MortarBattery
|
||||
// Only a physical projectile unlocks the next load. It must be aimed again.
|
||||
Released = false;
|
||||
AimedAt = -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));
|
||||
}
|
||||
else
|
||||
@@ -234,10 +251,11 @@ class ERR_MortarBattery
|
||||
Position = gun.GetOrigin();
|
||||
SpawnedAt = now;
|
||||
Claimed = true;
|
||||
m_Readiness = new ERR_MortarReadiness(this);
|
||||
}
|
||||
|
||||
|
||||
protected int InventoryShells(InventoryStorageManagerComponent inventory)
|
||||
int InventoryShells(InventoryStorageManagerComponent inventory)
|
||||
{
|
||||
if (!inventory)
|
||||
return 0;
|
||||
@@ -252,36 +270,41 @@ class ERR_MortarBattery
|
||||
return count;
|
||||
}
|
||||
|
||||
protected void Equip(IEntity soldier)
|
||||
bool Stopped() { return m_Stopped; }
|
||||
bool SupplyPending() { return m_Transfer && m_Transfer.Unresolved(); }
|
||||
bool SupplyAuthorized(float now)
|
||||
{
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(soldier.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory)
|
||||
{
|
||||
Audit("EQUIP FAILED", string.Format("crew=%1 inventory missing", soldier));
|
||||
return;
|
||||
}
|
||||
if (soldier == RadioOperator)
|
||||
{
|
||||
int capability = ERR_HQRadio.Capability(soldier);
|
||||
Audit("RADIO EQUIP", string.Format("operator=%1 capability=%2 (2=physical manpack); next HQ radio index verifies route", soldier, capability));
|
||||
return;
|
||||
}
|
||||
if (ERR_HQRadio.Capability(soldier) == 0 && !inventory.TrySpawnPrefabToStorage("{E1A5D4B878AA8980}Prefabs/Items/Equipment/Radios/Radio_R148.et"))
|
||||
Print("Rereintroduced AI [mortar]: crew radio did not fit; HQ handoff required", LogLevel.WARNING);
|
||||
// Native mortar-crew backpacks carry three EMPTY three-round boxes.
|
||||
// Fill them once. No infinite ammunition or replacement after casualties.
|
||||
for (int i = InventoryShells(inventory); i < 9; i++)
|
||||
{
|
||||
if (!inventory.TrySpawnPrefabToStorage(SHELL, purpose: EStoragePurpose.PURPOSE_DEPOSIT))
|
||||
break;
|
||||
}
|
||||
Audit("EQUIP", string.Format("crew=%1 inventory HE=%2 requested=9 radio capability=%3 (0=none)", soldier, InventoryShells(inventory), ERR_HQRadio.Capability(soldier)));
|
||||
if (!Replication.IsServer() || m_Stopped || !StartupActivated || !Operational() || !CommandOpen || !ERR_HQ.MissionActive()) return false;
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
if (!system || system.FindGun(Gun) != this) return false;
|
||||
ERR_HQReport suppliedReport = Report;
|
||||
if (!suppliedReport) suppliedReport = PendingSupplyReport;
|
||||
return ERR_MortarPrediction.Fresh(suppliedReport, now);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
Cancel("HQ stopped");
|
||||
if (m_Readiness) m_Readiness.Stop();
|
||||
}
|
||||
|
||||
bool SupplyGunner()
|
||||
ERR_MortarSupplyState SupplyState(float now)
|
||||
{
|
||||
if (!Operational())
|
||||
return SupplyFailed(OperationalReason());
|
||||
// Observe an existing native operation even after its order was canceled.
|
||||
// No second request may race an unresolved transfer on this battery.
|
||||
if (m_Transfer)
|
||||
{
|
||||
ERR_MortarSupplyState observed = m_Transfer.Poll(now);
|
||||
if (m_Transfer.CapturedOrder() == OrderId)
|
||||
{
|
||||
if (observed == ERR_MortarSupplyState.READY) m_Transfer = null;
|
||||
return observed;
|
||||
}
|
||||
if (m_Transfer.Unresolved()) return ERR_MortarSupplyState.FAILED;
|
||||
m_Transfer = null;
|
||||
}
|
||||
if (!SupplyAuthorized(now)) return SupplyStateFailed("current HQ order, crew or report is unavailable");
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
IEntity gunner;
|
||||
if (usage && usage.GetTurretCompartmentSlot())
|
||||
@@ -303,15 +326,15 @@ class ERR_MortarBattery
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!gunner) return SupplyFailed("no fighting gunner found");
|
||||
if (ERR_Actors.Group(gunner) != Group) return SupplyFailed("turret occupant belongs to another group");
|
||||
if (!ERR_Actors.CanFight(gunner)) return SupplyFailed("gunner cannot fight");
|
||||
if (!gunner) return SupplyStateFailed("no fighting gunner found");
|
||||
if (ERR_Actors.Group(gunner) != Group) return SupplyStateFailed("turret occupant belongs to another group");
|
||||
if (!ERR_Actors.CanFight(gunner)) return SupplyStateFailed("gunner cannot fight");
|
||||
InventoryStorageManagerComponent destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!destination)
|
||||
return SupplyFailed("gunner inventory missing");
|
||||
return SupplyStateFailed("gunner inventory missing");
|
||||
if (InventoryShells(destination) > 0)
|
||||
return true;
|
||||
int nearbyDonors, donorShells, storageRejected, moveRejected;
|
||||
return ERR_MortarSupplyState.READY;
|
||||
int nearbyDonors, donorShells, storageRejected;
|
||||
array<IEntity> consideredShells = {};
|
||||
foreach (AIAgent bearer : agents)
|
||||
{
|
||||
@@ -330,20 +353,27 @@ class ERR_MortarBattery
|
||||
{
|
||||
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != SHELL)
|
||||
continue;
|
||||
if (consideredShells.Contains(item)) continue;
|
||||
if (consideredShells.Contains(item) || m_AttemptedShells.Contains(item)) continue;
|
||||
consideredShells.Insert(item);
|
||||
donorShells++;
|
||||
BaseInventoryStorageComponent storage = destination.FindStorageForItem(item, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
if (!storage) { storageRejected++; continue; }
|
||||
if (inventory.TryMoveItemToStorage(item, storage))
|
||||
{
|
||||
Audit("AMMO TRANSFER", string.Format("request accepted donor=%1 gunner=%2 shell=%3; inventory move is not a fired shot", donor, gunner, item));
|
||||
return true;
|
||||
}
|
||||
moveRejected++;
|
||||
m_AttemptedShells.Insert(item); // even rejected/ambiguous requests are never retried
|
||||
m_Transfer = new ERR_MortarSupplyTransfer(this, donor, gunner, item, storage, now);
|
||||
if (!m_Transfer.Begin(now)) return SupplyStateFailed("exact shell transfer request rejected; no retry");
|
||||
Audit("AMMO TRANSFER", string.Format("one exact-item move pending donor=%1 gunner=%2 shell=%3 order=%4; awaiting callback AND physical destination", donor, gunner, item, OrderId));
|
||||
return ERR_MortarSupplyState.PENDING;
|
||||
}
|
||||
}
|
||||
return SupplyFailed(string.Format("gunner empty; fighting donors with inventory within 12m=%1 donor HE=%2 destination storage rejected=%3 native move rejected=%4", nearbyDonors, donorShells, storageRejected, moveRejected));
|
||||
return SupplyStateFailed(string.Format("gunner empty; fighting donors within 12m=%1 unattempted HE=%2 destination storage rejected=%3", nearbyDonors, donorShells, storageRejected));
|
||||
}
|
||||
|
||||
// Compatibility for existing native callers; request acceptance is NOT ready.
|
||||
bool SupplyGunner() { return SupplyState(GetGame().GetWorld().GetWorldTime()) == ERR_MortarSupplyState.READY; }
|
||||
protected ERR_MortarSupplyState SupplyStateFailed(string reason)
|
||||
{
|
||||
SupplyFailed(reason);
|
||||
return ERR_MortarSupplyState.FAILED;
|
||||
}
|
||||
|
||||
bool SupplyFailed(string reason)
|
||||
@@ -363,8 +393,12 @@ class ERR_MortarBattery
|
||||
CommandOpen = false;
|
||||
RadioOrder = null;
|
||||
PendingRadioReport = null;
|
||||
PendingSupplyReport = null;
|
||||
if (m_Transfer) m_Transfer.Invalidate();
|
||||
Report = null;
|
||||
AimedAt = -1;
|
||||
AimSolver = null;
|
||||
AimSolution = null;
|
||||
if (Activity)
|
||||
Activity.Complete();
|
||||
Activity = null;
|
||||
@@ -399,42 +433,21 @@ class ERR_MortarBattery
|
||||
|
||||
void Update(float now)
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
if (!StartupActivated)
|
||||
{
|
||||
if (!SpawnFailure.IsEmpty()) return;
|
||||
if (!m_Readiness) { SpawnFailed("initial support readiness owner missing"); return; }
|
||||
ERR_MortarSupplyState startup = m_Readiness.Poll(now);
|
||||
if (startup != ERR_MortarSupplyState.READY) return;
|
||||
StartupActivated = true;
|
||||
}
|
||||
if (m_Transfer) m_Transfer.Poll(now);
|
||||
if (!Operational())
|
||||
{
|
||||
Cancel(OperationalReason());
|
||||
return;
|
||||
}
|
||||
if (!StartupActivated && now - SpawnedAt >= 10000)
|
||||
{
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage || !Group.GetGroupUtilityComponent().m_VehicleMgr || !Group.GetGroupUtilityComponent().m_VehicleMgr.TryAddVehicle(usage))
|
||||
{
|
||||
SpawnFailure = "native gun registration failed";
|
||||
Cancel(SpawnFailure);
|
||||
return;
|
||||
}
|
||||
array<AIAgent> initialCrew = {};
|
||||
Group.GetAgents(initialCrew);
|
||||
foreach (AIAgent initialAgent : initialCrew)
|
||||
{
|
||||
IEntity initialActor = initialAgent.GetControlledEntity();
|
||||
if (initialActor && initialActor.GetPrefabData() && initialActor.GetPrefabData().GetPrefabName() == RADIO_OPERATOR) RadioOperator = initialActor;
|
||||
}
|
||||
StartupActivated = true;
|
||||
Audit("CREW ACTIVATE", "startup delay elapsed; activating and equipping crew");
|
||||
Group.ActivateAI();
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity()))
|
||||
{
|
||||
agent.SetPermanentLOD(0);
|
||||
agent.ActivateAI();
|
||||
Equip(agent.GetControlledEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
@@ -448,6 +461,7 @@ class ERR_MortarBattery
|
||||
|
||||
void Order(ERR_HQReport report, float now)
|
||||
{
|
||||
if (m_Stopped || !StartupActivated || !Operational() || !CommandOpen || Activity || !ERR_MortarPrediction.Fresh(report, now)) return;
|
||||
Report = report.Snapshot();
|
||||
OrderedAt = now;
|
||||
NextOrder = now + 30000;
|
||||
|
||||
@@ -36,6 +36,8 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
if (!battery)
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
battery.AimedAt = -1;
|
||||
battery.AimSolver = null;
|
||||
battery.AimSolution = null;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
ERR_HQReport report = system.Latest(battery, now);
|
||||
if (!ERR_HQ.MissionActive()) { system.Withhold(battery, "aim: mission inactive"); return ENodeResult.FAIL; }
|
||||
@@ -43,13 +45,56 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
if (battery.Released) { system.Withhold(battery, "aim: waiting for committed projectile"); return ENodeResult.FAIL; }
|
||||
if (!report) { system.Withhold(battery, "aim: " + system.ContactFailure(battery)); return ENodeResult.FAIL; }
|
||||
battery.Trace("AIM START", "solving moving intercept", string.Format("latest source=M%1; %2", report.MessageId, report.Description(now)));
|
||||
ERR_MortarAimSolution solution = ERR_Solve(battery, system, report, now);
|
||||
if (!solution) return ENodeResult.FAIL;
|
||||
battery.Impact = solution.Impact;
|
||||
battery.AimOffset = solution.SalvoOffset;
|
||||
battery.AimedShot = battery.OrderShots;
|
||||
battery.FlightTime = solution.FlightTime;
|
||||
battery.AimSolution = solution;
|
||||
battery.AimSolver = this; // weak: do not retain a destroyed native behavior tree
|
||||
battery.AimedAt = 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);
|
||||
SetVariableOut(PORT_INIT_SPEED_ID, solution.Charge);
|
||||
return ENodeResult.SUCCESS;
|
||||
}
|
||||
|
||||
// Reuse this exact native node's coefficient/elevation APIs. A slow native
|
||||
// barrel can keep its command only if a NEW solve still matches that command.
|
||||
bool ERR_RevalidateAim(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.AimSolver != this || !commanded || !commanded.Matches(battery, report) || gun != battery.Gun || ammo != ERR_MortarBattery.SHELL)
|
||||
return system.Withhold(battery, "original aim solver or command no longer available");
|
||||
if (!ERR_MortarPrediction.Fresh(report, now)) return system.Withhold(battery, system.ContactFailure(battery));
|
||||
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, "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.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;
|
||||
}
|
||||
|
||||
protected ERR_MortarAimSolution ERR_Solve(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now)
|
||||
{
|
||||
IEntity gun = battery.Gun;
|
||||
vector gunOrigin = gun.GetOrigin();
|
||||
ResourceName ammo;
|
||||
GetVariableIn(PORT_AMMO_PREFAB, ammo);
|
||||
if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return ENodeResult.FAIL; }
|
||||
if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return null; }
|
||||
Resource resource = Resource.Load(ammo);
|
||||
if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return ENodeResult.FAIL; }
|
||||
if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return null; }
|
||||
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||
if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return ENodeResult.FAIL; }
|
||||
if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return null; }
|
||||
array<float> coefficients = {};
|
||||
GetAmmoInitialSpeedCoefficients(ammo, coefficients);
|
||||
if (coefficients.IsEmpty())
|
||||
@@ -66,13 +111,13 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
// Reject non-convergence rather than fire with an arbitrary lead time.
|
||||
for (int iteration = 0; iteration < 4; iteration++)
|
||||
{
|
||||
if (!ERR_MortarPrediction.Predict(report, now, flight + 2, impact)) { system.Withhold(battery, "aim prediction: " + ERR_MortarPrediction.FailureReason, string.Format("flight lead=%1s; %2", flight + 2, report.Description(now))); return ENodeResult.FAIL; }
|
||||
if (!system.OutsideSettlements(impact)) { system.Withhold(battery, "aim target inside settlement exclusion", system.SettlementReason); return ENodeResult.FAIL; }
|
||||
if (!ERR_MortarPrediction.Predict(report, now, flight + 2, impact)) { system.Withhold(battery, "aim prediction: " + ERR_MortarPrediction.FailureReason, string.Format("flight lead=%1s; %2", flight + 2, report.Description(now))); return null; }
|
||||
if (!system.OutsideSettlements(impact)) { system.Withhold(battery, "aim target inside settlement exclusion", system.SettlementReason); return null; }
|
||||
impact += salvoOffset;
|
||||
impact[1] = SCR_TerrainHelper.GetTerrainY(impact);
|
||||
float distance = vector.DistanceXZ(gun.GetOrigin(), impact);
|
||||
float altitude = impact[1] - gun.GetOrigin()[1];
|
||||
if (distance < 350 || distance > 4000) { system.Withhold(battery, "predicted impact out of range", string.Format("iteration=%1 distance=%2m impact=%3 allowed=350..4000m", iteration, distance, impact)); return ENodeResult.FAIL; }
|
||||
float distance = vector.DistanceXZ(gunOrigin, impact);
|
||||
float altitude = impact[1] - gunOrigin[1];
|
||||
if (distance < 350 || distance > 4000) { system.Withhold(battery, "predicted impact out of range", string.Format("iteration=%1 distance=%2m impact=%3 allowed=350..4000m", iteration, distance, impact)); return null; }
|
||||
bool reachable;
|
||||
float calculated;
|
||||
for (int i = 0; i < coefficients.Count(); i++)
|
||||
@@ -86,25 +131,26 @@ modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
reachable = true;
|
||||
break;
|
||||
}
|
||||
if (!reachable) { system.Withhold(battery, "no native ballistic solution within elevation/flight-time limits", string.Format("iteration=%1 distance=%2 altitude=%3 charges=%4 elevation radians=%5..%6 flight limit=75s", iteration, distance, altitude, coefficients.Count(), minimum, maximum)); return ENodeResult.FAIL; }
|
||||
if (!reachable) { system.Withhold(battery, "no native ballistic solution within elevation/flight-time limits", string.Format("iteration=%1 distance=%2 altitude=%3 charges=%4 elevation radians=%5..%6 flight limit=75s", iteration, distance, altitude, coefficients.Count(), minimum, maximum)); return null; }
|
||||
converged = Math.AbsFloat(calculated - flight) < 1;
|
||||
flight = calculated;
|
||||
if (converged)
|
||||
break;
|
||||
}
|
||||
if (!converged) { system.Withhold(battery, "moving intercept did not converge in four iterations"); return ENodeResult.FAIL; }
|
||||
if (!system.Safe(battery, impact, flight + 2)) { system.Withhold(battery, "aim impact safety rejected", system.SafetyReason); return ENodeResult.FAIL; }
|
||||
battery.Impact = impact;
|
||||
battery.AimOffset = salvoOffset;
|
||||
battery.AimedShot = battery.OrderShots;
|
||||
battery.FlightTime = flight;
|
||||
battery.AimedAt = now;
|
||||
battery.LastFailure = "";
|
||||
battery.Trace("AIM SOLUTION", "ready", string.Format("predicted impact=%1 flight=%2s charge index=%3 aim height offset=%4m latest source=M%5", impact, flight, charge, offset, report.MessageId));
|
||||
vector aim = impact + Vector(0, offset, 0);
|
||||
SetVariableOut(PORT_AIM_POS, aim);
|
||||
SetVariableOut(PORT_INIT_SPEED_ID, charge);
|
||||
return ENodeResult.SUCCESS;
|
||||
if (!converged) { system.Withhold(battery, "moving intercept did not converge in four iterations"); return null; }
|
||||
if (!system.Safe(battery, impact, flight + 2)) { system.Withhold(battery, "aim impact safety rejected", system.SafetyReason); return null; }
|
||||
ERR_MortarAimSolution solution = new ERR_MortarAimSolution();
|
||||
solution.Gun = gun;
|
||||
solution.Report = report;
|
||||
solution.OrderId = battery.OrderId;
|
||||
solution.Shot = battery.OrderShots;
|
||||
solution.GunOrigin = gunOrigin;
|
||||
solution.Impact = impact;
|
||||
solution.AimTarget = impact + Vector(0, offset, 0);
|
||||
solution.SalvoOffset = salvoOffset;
|
||||
solution.Charge = charge;
|
||||
solution.FlightTime = flight;
|
||||
return solution;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,17 +250,27 @@ modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
|
||||
}
|
||||
if (battery)
|
||||
{
|
||||
if (!battery.CommandOpen) return ENodeResult.FAIL;
|
||||
// Loading can outlast the native pause. Count confirmed shells only.
|
||||
if (battery.OrderShots <= behavior.GetShotCount()) return ENodeResult.RUNNING;
|
||||
int gate = ERR_MortarSupplyRules.CycleGate(battery.CommandOpen, battery.OrderShots, behavior.GetShotCount(), ERR_MortarSalvo.ROUNDS, ERR_MortarSupplyState.READY);
|
||||
if (gate < 0) return ENodeResult.FAIL;
|
||||
if (gate == 0) return ENodeResult.RUNNING;
|
||||
// Native super increments the behavior counter. Wait BEFORE calling it,
|
||||
// so repeated pending ticks cannot count the same projectile twice.
|
||||
if (battery.OrderShots < ERR_MortarSalvo.ROUNDS)
|
||||
{
|
||||
ERR_MortarSupplyState supply = battery.SupplyState(GetGame().GetWorld().GetWorldTime());
|
||||
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");
|
||||
return ENodeResult.FAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
||||
if (battery && result == ENodeResult.SUCCESS)
|
||||
{
|
||||
SetVariableOut(PORT_AIMING_NEEDED, true);
|
||||
// Transfer existing nearby crew ammunition before the next native load.
|
||||
if (battery.CommandOpen && battery.OrderShots < ERR_MortarSalvo.ROUNDS)
|
||||
battery.SupplyGunner();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
enum ERR_MortarSupplyState { FAILED = -1, PENDING, READY }
|
||||
|
||||
class ERR_MortarSupplyRules
|
||||
{
|
||||
static int CycleGate(bool commandOpen, int confirmedShots, int behaviorShots, int limit, int supplyState)
|
||||
{
|
||||
if (!commandOpen) return -1;
|
||||
if (confirmedShots <= behaviorShots) return 0;
|
||||
if (confirmedShots < limit) return supplyState;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_MortarInitialItem : ScriptedInventoryOperationCallback
|
||||
{
|
||||
bool Finished;
|
||||
bool Succeeded;
|
||||
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
|
||||
protected override void OnComplete() { Finish(true); }
|
||||
protected override void OnFailed() { Finish(false); }
|
||||
protected override void OnAllComplete() { Finish(true); }
|
||||
protected override void OnAllFailed() { Finish(false); }
|
||||
void Rejected() { Finish(false); }
|
||||
}
|
||||
|
||||
// One finite allocation, after the ENTIRE original support crew exists. Failed
|
||||
// or stopped setup retains physical assets and retires only its pending spawn intent.
|
||||
class ERR_MortarReadiness
|
||||
{
|
||||
protected ERR_MortarBattery m_Battery;
|
||||
protected ref array<IEntity> m_Members = {};
|
||||
protected ref array<ref ERR_MortarInitialItem> m_Operations = {};
|
||||
protected bool m_Allocated;
|
||||
protected bool m_Ready;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Retired;
|
||||
protected int m_RequestedShells;
|
||||
void ERR_MortarReadiness(ERR_MortarBattery battery) { m_Battery = battery; }
|
||||
static ERR_MortarSupplyState Population(int physical, int gunners, int operators, bool complete, bool expired)
|
||||
{
|
||||
if (physical > 4 || gunners > 3 || operators > 1) return ERR_MortarSupplyState.FAILED;
|
||||
if (physical == 4 && gunners == 3 && operators == 1 && complete && !expired) return ERR_MortarSupplyState.READY;
|
||||
if (expired) return ERR_MortarSupplyState.FAILED;
|
||||
return ERR_MortarSupplyState.PENDING;
|
||||
}
|
||||
static bool AllocationValid(int shells) { return shells >= 0 && shells <= 9; }
|
||||
int RequestedShells() { return m_RequestedShells; }
|
||||
bool Ready() { return m_Ready && !m_Stopped; }
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Ready || m_Retired || !m_Battery || !m_Battery.Group || !Replication.IsServer()) return;
|
||||
m_Retired = true;
|
||||
// As in Core initial guard cleanup: stopping the current request alone does
|
||||
// not retire the native group's still-authored future expansion intent.
|
||||
RetireGroup(m_Battery.Group);
|
||||
}
|
||||
static void RetireGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!group || !Replication.IsServer()) return;
|
||||
group.SetSpawnImmediately(false);
|
||||
group.m_aUnitPrefabSlots = new array<ResourceName>();
|
||||
if (!GetGame()) return;
|
||||
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (world) world.PurgeSpawnRequestsForGroup(group);
|
||||
}
|
||||
|
||||
protected ERR_MortarSupplyState Fail(string reason)
|
||||
{
|
||||
m_Battery.SpawnFailed(reason);
|
||||
Stop();
|
||||
return ERR_MortarSupplyState.FAILED;
|
||||
}
|
||||
protected bool Allocate()
|
||||
{
|
||||
m_Allocated = true; // before native requests; never refill or retry a failure
|
||||
foreach (IEntity actor : m_Members)
|
||||
{
|
||||
if (actor == m_Battery.RadioOperator) continue;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
int existing = m_Battery.InventoryShells(inventory);
|
||||
if (!AllocationValid(existing)) return false;
|
||||
if (ERR_HQRadio.Capability(actor) == 0 && !inventory.TrySpawnPrefabToStorage("{E1A5D4B878AA8980}Prefabs/Items/Equipment/Radios/Radio_R148.et"))
|
||||
m_Battery.Audit("RADIO EQUIP", "optional crew handset did not fit; dedicated manpack remains required");
|
||||
for (int i = existing; i < 9; i++)
|
||||
{
|
||||
ERR_MortarInitialItem operation = new ERR_MortarInitialItem();
|
||||
m_Operations.Insert(operation);
|
||||
m_RequestedShells++;
|
||||
if (!inventory.TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL, purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: operation))
|
||||
{
|
||||
operation.Rejected();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
ERR_MortarSupplyState Poll(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Battery || m_Battery.Stopped()) return ERR_MortarSupplyState.FAILED;
|
||||
if (m_Ready) return ERR_MortarSupplyState.READY;
|
||||
SCR_AIGroup group = m_Battery.Group;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed");
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent));
|
||||
if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed");
|
||||
bool expired = now < m_Battery.SpawnedAt || now - m_Battery.SpawnedAt >= 90000;
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
array<IEntity> physical = {};
|
||||
int gunners, operators;
|
||||
IEntity operatorActor;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !agent.GetControlledEntity()) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (physical.Contains(actor)) return Fail("initial support group reports duplicate physical actor");
|
||||
physical.Insert(actor);
|
||||
if (!ERR_Actors.CanFight(actor) || SCR_FactionManager.SGetFaction(actor) != mode.EnemyFaction() || !actor.GetPrefabData()) return Fail("initial support actor unavailable or has wrong faction/resource");
|
||||
ResourceName prefab = actor.GetPrefabData().GetPrefabName();
|
||||
if (prefab == ERR_MortarBattery.CREW) gunners++;
|
||||
else if (prefab == ERR_MortarBattery.RADIO_OPERATOR) { operators++; operatorActor = actor; }
|
||||
else return Fail("initial support actor does not belong to authored crew catalog");
|
||||
}
|
||||
ERR_MortarSupplyState population = Population(physical.Count(), gunners, operators, group.IsExpandComplete(), expired);
|
||||
if (population == ERR_MortarSupplyState.FAILED) return Fail("initial four-person support population incomplete/invalid within 90 seconds");
|
||||
if (population == ERR_MortarSupplyState.PENDING) return population;
|
||||
if (m_Allocated)
|
||||
{
|
||||
foreach (IEntity original : m_Members)
|
||||
if (!physical.Contains(original)) return Fail("initial support member replaced after finite allocation");
|
||||
}
|
||||
foreach (IEntity member : physical)
|
||||
{
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(member.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return ERR_MortarSupplyState.PENDING;
|
||||
if (!m_Allocated && member != operatorActor && m_Battery.InventoryShells(inventory) < 9 && !inventory.CanInsertResource(ERR_MortarBattery.SHELL, EStoragePurpose.PURPOSE_DEPOSIT)) return ERR_MortarSupplyState.PENDING;
|
||||
}
|
||||
if (ERR_HQRadio.Capability(operatorActor) != 2) return ERR_MortarSupplyState.PENDING;
|
||||
// Preserve the existing earliest activation delay, but it is not readiness.
|
||||
if (now - m_Battery.SpawnedAt < 10000) return ERR_MortarSupplyState.PENDING;
|
||||
if (!m_Allocated)
|
||||
{
|
||||
if (!group.GetGroupUtilityComponent() || !group.GetGroupUtilityComponent().m_VehicleMgr) return ERR_MortarSupplyState.PENDING;
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Battery.Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage || !group.GetGroupUtilityComponent().m_VehicleMgr.TryAddVehicle(usage)) return Fail("native support gun registration failed");
|
||||
m_Members.Copy(physical); m_Battery.RadioOperator = operatorActor;
|
||||
if (!Allocate()) return Fail("finite initial support shell allocation rejected; no retry");
|
||||
return ERR_MortarSupplyState.PENDING; // never accept mutation-frame readiness
|
||||
}
|
||||
foreach (ERR_MortarInitialItem operation : m_Operations)
|
||||
{
|
||||
if (!operation.Finished) return ERR_MortarSupplyState.PENDING;
|
||||
if (!operation.Succeeded) return Fail("finite initial support shell operation failed; no retry");
|
||||
}
|
||||
foreach (IEntity equipped : m_Members)
|
||||
{
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(equipped.FindComponent(InventoryStorageManagerComponent));
|
||||
int shells = m_Battery.InventoryShells(inventory);
|
||||
if (equipped == operatorActor)
|
||||
{
|
||||
if (shells != 0) return Fail("dedicated initial manpack operator unexpectedly carries mortar shells");
|
||||
}
|
||||
else if (shells != 9) return ERR_MortarSupplyState.PENDING;
|
||||
}
|
||||
if (m_Battery.CountShells() != 27) return ERR_MortarSupplyState.PENDING;
|
||||
m_Ready = true;
|
||||
group.ActivateAI();
|
||||
foreach (AIAgent readyAgent : agents)
|
||||
{
|
||||
if (readyAgent) { readyAgent.SetPermanentLOD(0); readyAgent.ActivateAI(); }
|
||||
}
|
||||
m_Battery.Audit("CREW READY", string.Format("four original physical members, one manpack, 27 confirmed HE; new shell requests=%1; allocation permanently closed", m_RequestedShells));
|
||||
return ERR_MortarSupplyState.READY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// One finite response per delivered observation at this battery. Cancellation
|
||||
// does not turn the same radio message into new ammunition authorization.
|
||||
class ERR_MortarReportLedger
|
||||
{
|
||||
protected ref map<int, float> m_Attempts = new map<int, float>();
|
||||
|
||||
bool Available(int messageId, float observedAt, float now)
|
||||
{
|
||||
if (messageId <= 0 || observedAt < 0 || now < observedAt || now - observedAt > ERR_MortarPrediction.MAX_REPORT_AGE_MS) return false;
|
||||
for (int i = m_Attempts.Count() - 1; i >= 0; i--)
|
||||
if (now - m_Attempts.GetElement(i) > ERR_MortarPrediction.MAX_REPORT_AGE_MS) m_Attempts.Remove(m_Attempts.GetKey(i));
|
||||
return !m_Attempts.Contains(messageId) && m_Attempts.Count() < 128;
|
||||
}
|
||||
bool Claim(int messageId, float observedAt, float now)
|
||||
{
|
||||
if (!Available(messageId, observedAt, now)) return false;
|
||||
m_Attempts.Insert(messageId, observedAt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
class ERR_MortarSupplyCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ERR_MortarSupplyTransfer Transfer;
|
||||
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
|
||||
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
|
||||
}
|
||||
|
||||
// Exact-item native transaction. Callbacks only record outcomes; the HQ tick
|
||||
// authorizes progress after checking actual inventory ownership and the order.
|
||||
class ERR_MortarSupplyTransfer
|
||||
{
|
||||
protected ERR_MortarBattery m_Battery;
|
||||
protected IEntity m_Gun;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected IEntity m_Donor;
|
||||
protected IEntity m_Gunner;
|
||||
protected IEntity m_Shell;
|
||||
protected InventoryStorageManagerComponent m_Source;
|
||||
protected InventoryStorageManagerComponent m_Destination;
|
||||
protected BaseInventoryStorageComponent m_Storage;
|
||||
protected ref ERR_MortarSupplyCallback m_Callback;
|
||||
protected int m_Order;
|
||||
protected int m_Acknowledgement;
|
||||
protected float m_Deadline;
|
||||
protected bool m_Accepted;
|
||||
protected bool m_Invalidated;
|
||||
protected bool m_Resolved;
|
||||
protected bool m_Ready;
|
||||
void ERR_MortarSupplyTransfer(ERR_MortarBattery battery, IEntity donor, IEntity gunner, IEntity shell, BaseInventoryStorageComponent storage, float now)
|
||||
{
|
||||
m_Battery = battery; m_Gun = battery.Gun; m_Group = battery.Group;
|
||||
m_Donor = donor; m_Gunner = gunner; m_Shell = shell; m_Storage = storage;
|
||||
m_Order = battery.OrderId; m_Deadline = now + 8000;
|
||||
m_Source = InventoryStorageManagerComponent.Cast(donor.FindComponent(InventoryStorageManagerComponent));
|
||||
m_Destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
|
||||
}
|
||||
static bool Authority(bool valid, bool invalidated, int capturedOrder, int currentOrder)
|
||||
{
|
||||
return valid && !invalidated && capturedOrder == currentOrder;
|
||||
}
|
||||
static bool Proven(bool callbackComplete, bool sourceContains, bool destinationContains, bool exactSlot)
|
||||
{
|
||||
return callbackComplete && !sourceContains && destinationContains && exactSlot;
|
||||
}
|
||||
protected bool Contains(InventoryStorageManagerComponent inventory)
|
||||
{
|
||||
if (!inventory || !m_Shell) return false;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
return items.Contains(m_Shell);
|
||||
}
|
||||
protected bool Valid(float now)
|
||||
{
|
||||
if (!m_Battery || !Authority(m_Battery.SupplyAuthorized(now), m_Invalidated, m_Order, m_Battery.OrderId)) return false;
|
||||
if (m_Battery.Gun != m_Gun || m_Battery.Group != m_Group || !m_Donor || !m_Gunner || m_Donor == m_Gunner || !m_Shell || !m_Source || !m_Destination || !m_Storage) return false;
|
||||
if (!ERR_Actors.CanFight(m_Donor) || !ERR_Actors.CanFight(m_Gunner) || ERR_Actors.Group(m_Donor) != m_Group || ERR_Actors.Group(m_Gunner) != m_Group) return false;
|
||||
Faction faction = m_Group.GetFaction();
|
||||
if (SCR_FactionManager.SGetFaction(m_Donor) != faction || SCR_FactionManager.SGetFaction(m_Gunner) != faction || vector.Distance(m_Donor.GetOrigin(), m_Gunner.GetOrigin()) > 12) return false;
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage || !usage.GetTurretCompartmentSlot()) return false;
|
||||
IEntity occupant = usage.GetTurretCompartmentSlot().GetOccupant();
|
||||
return !occupant || occupant == m_Gunner;
|
||||
}
|
||||
void Acknowledge(bool success)
|
||||
{
|
||||
if (m_Acknowledgement != 0) return;
|
||||
if (success) m_Acknowledgement = 1;
|
||||
else m_Acknowledgement = -1;
|
||||
}
|
||||
bool Begin(float now)
|
||||
{
|
||||
if (m_Callback || !Replication.IsServer() || !Valid(now) || !Contains(m_Source) || Contains(m_Destination)) return false;
|
||||
m_Callback = new ERR_MortarSupplyCallback(); m_Callback.Transfer = this;
|
||||
// Do not use callback.GetItem(): the native contract excludes Move operations.
|
||||
m_Accepted = m_Source.TryMoveItemToStorage(m_Shell, m_Storage, -1, m_Callback);
|
||||
if (!m_Accepted) { Acknowledge(false); m_Resolved = true; }
|
||||
return m_Accepted;
|
||||
}
|
||||
void Invalidate() { m_Invalidated = true; m_Ready = false; }
|
||||
bool Unresolved() { return m_Accepted && !m_Resolved; }
|
||||
int CapturedOrder() { return m_Order; }
|
||||
ERR_MortarSupplyState Poll(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return ERR_MortarSupplyState.FAILED;
|
||||
bool valid = Valid(now);
|
||||
bool sourceContains = Contains(m_Source);
|
||||
bool destinationContains = Contains(m_Destination);
|
||||
bool exactSlot;
|
||||
InventoryItemComponent item;
|
||||
if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));
|
||||
if (item && item.GetParentSlot()) exactSlot = item.GetParentSlot().GetStorage() == m_Storage;
|
||||
bool proven = Proven(m_Acknowledgement == 1, sourceContains, destinationContains, exactSlot);
|
||||
if (proven || m_Acknowledgement < 0) m_Resolved = true;
|
||||
// A successful callback without physical destination proof is ambiguous.
|
||||
// Keep it reserved through timeout/cancellation; never start a second move.
|
||||
if (!valid || now >= m_Deadline)
|
||||
{
|
||||
m_Invalidated = true; m_Ready = false;
|
||||
return ERR_MortarSupplyState.FAILED;
|
||||
}
|
||||
if (proven)
|
||||
{
|
||||
m_Ready = true;
|
||||
return ERR_MortarSupplyState.READY;
|
||||
}
|
||||
if (m_Acknowledgement < 0 || !m_Accepted) return ERR_MortarSupplyState.FAILED;
|
||||
return ERR_MortarSupplyState.PENDING;
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,14 @@ class ERR_MortarSystem
|
||||
if (!gun || !group)
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(gun);
|
||||
ERR_MortarReadiness.RetireGroup(group);
|
||||
ERR_Actors.DeleteSpawned(group);
|
||||
return false;
|
||||
}
|
||||
if (group.GetFaction() != ERR_GameMode.Get().EnemyFaction())
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(gun);
|
||||
ERR_MortarReadiness.RetireGroup(group);
|
||||
ERR_Actors.DeleteSpawned(group);
|
||||
return false;
|
||||
}
|
||||
@@ -125,13 +127,19 @@ class ERR_MortarSystem
|
||||
bool AllowShot(ERR_MortarBattery battery)
|
||||
{
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (!ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive");
|
||||
if (m_Stopped || !ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive or HQ stopped");
|
||||
if (!battery.Operational()) return Withhold(battery, battery.OperationalReason());
|
||||
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 > 10000) return Withhold(battery, "fresh aim solution required");
|
||||
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.AimSolver) return Withhold(battery, "original native aim solver no longer available");
|
||||
if (!battery.AimSolver.ERR_RevalidateAim(battery, this, report, now)) return false;
|
||||
}
|
||||
vector updated;
|
||||
if (!ERR_MortarPrediction.Predict(report, now, battery.FlightTime, updated)) return Withhold(battery, ERR_MortarPrediction.FailureReason);
|
||||
updated += battery.AimOffset;
|
||||
@@ -142,20 +150,41 @@ class ERR_MortarSystem
|
||||
}
|
||||
protected void AdvanceOrder(ERR_MortarBattery battery, float now)
|
||||
{
|
||||
if (!battery.RadioOrder) return;
|
||||
int state = battery.RadioOrder.Advance(now, m_HQ.Journal());
|
||||
if (state == 0) return;
|
||||
ERR_HQReport delivered = battery.PendingRadioReport;
|
||||
battery.RadioOrder = null;
|
||||
battery.PendingRadioReport = null;
|
||||
if (state < 0 || !ERR_MortarPrediction.Fresh(delivered, now) || !battery.Operational() || battery.Activity || !battery.SupplyGunner())
|
||||
if (battery.RadioOrder)
|
||||
{
|
||||
battery.Cancel("radio delivery, report freshness or gunner availability failed");
|
||||
int deliveredState = battery.RadioOrder.Advance(now, m_HQ.Journal());
|
||||
if (deliveredState == 0) return;
|
||||
ERR_HQReport delivered = battery.PendingRadioReport;
|
||||
battery.RadioOrder = null;
|
||||
battery.PendingRadioReport = null;
|
||||
if (deliveredState < 0 || !ERR_MortarPrediction.Fresh(delivered, now) || !battery.Operational() || battery.Activity || !battery.CommandOpen)
|
||||
{
|
||||
battery.Cancel("radio delivery, report freshness or battery availability failed");
|
||||
battery.NextOrder = now + 10000;
|
||||
return;
|
||||
}
|
||||
battery.PendingSupplyReport = delivered;
|
||||
}
|
||||
if (!battery.PendingSupplyReport) return;
|
||||
if (!battery.CommandOpen || !ERR_MortarPrediction.Fresh(battery.PendingSupplyReport, now) || !battery.Operational() || battery.Activity)
|
||||
{
|
||||
battery.Cancel("delivered order expired or became unavailable while awaiting physical ammunition");
|
||||
battery.NextOrder = now + 10000;
|
||||
return;
|
||||
}
|
||||
battery.Order(delivered, now);
|
||||
ERR_MortarSupplyState supply = battery.SupplyState(now);
|
||||
if (supply == ERR_MortarSupplyState.PENDING) return;
|
||||
if (supply == ERR_MortarSupplyState.FAILED)
|
||||
{
|
||||
battery.Cancel("physical gunner ammunition unavailable or transfer failed");
|
||||
battery.NextOrder = now + 10000;
|
||||
return;
|
||||
}
|
||||
ERR_HQReport ready = battery.PendingSupplyReport;
|
||||
battery.PendingSupplyReport = null;
|
||||
battery.Order(ready, now);
|
||||
}
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer()) return;
|
||||
@@ -179,9 +208,10 @@ class ERR_MortarSystem
|
||||
float distance = float.MAX;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
{
|
||||
if (!battery.StartupActivated || !battery.Operational() || battery.Activity || battery.RadioOrder || now < battery.NextOrder || battery.CountShells() == 0) continue;
|
||||
if (!battery.StartupActivated || !battery.Operational() || battery.Activity || battery.RadioOrder || battery.PendingSupplyReport || battery.SupplyPending() || now < battery.NextOrder || battery.CountShells() == 0) continue;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
if (!battery.CanRequest(report, now)) continue;
|
||||
float candidate = vector.DistanceXZ(battery.Position, report.Position);
|
||||
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
|
||||
selected = battery;
|
||||
@@ -192,7 +222,7 @@ class ERR_MortarSystem
|
||||
if (!selected || !target) return;
|
||||
ERR_HQRoute route = m_HQ.Downlink(selected.Group, "MORTAR ORDER", target.Description(now), target.MessageId);
|
||||
if (!route) return;
|
||||
selected.BeginRequest(target, now);
|
||||
if (!selected.BeginRequest(target, now)) return;
|
||||
selected.RadioId = route.MessageId;
|
||||
selected.RadioOrder = route;
|
||||
selected.PendingRadioReport = target.Snapshot();
|
||||
@@ -216,6 +246,6 @@ class ERR_MortarSystem
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries) battery.Cancel("HQ stopped");
|
||||
foreach (ERR_MortarBattery battery : m_Batteries) battery.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#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_QueueMortarFixtureReport(SCR_AIGroup source, IEntity target, vector position, float now)
|
||||
{
|
||||
if (!ERR_NativeMortarFiringProbe.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_RemoveMortarFixtureReport(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 ReserveShellAirspace(float now)
|
||||
{
|
||||
super.ReserveShellAirspace(now);
|
||||
ERR_NativeMortarFiringProbe probe = ERR_NativeMortarFiringProbe.Active();
|
||||
if (probe) probe.NativeCommit(this, now);
|
||||
}
|
||||
override void Cancel(string reason = "cancel requested")
|
||||
{
|
||||
ERR_NativeMortarFiringProbe probe = ERR_NativeMortarFiringProbe.Active();
|
||||
if (probe) probe.NativeClosing(this, reason, LastFailure);
|
||||
super.Cancel(reason);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeMortarEvidenceRules
|
||||
{
|
||||
static bool CommitFresh(float observed, float expectedObserved, float committed)
|
||||
{
|
||||
return observed == expectedObserved && observed >= 0 && committed >= observed && committed - observed <= ERR_MortarPrediction.MAX_REPORT_AGE_MS;
|
||||
}
|
||||
static bool EventBound(bool pending, int order, int currentOrder, int shot, int currentShot, float committed, float emitted)
|
||||
{
|
||||
// The fixture has its own overall deadline. A committed shell does not gain
|
||||
// a second authorization expiry while native physical transfer completes.
|
||||
return pending && order == currentOrder && shot == currentShot && emitted >= committed;
|
||||
}
|
||||
static bool ExpiredClosure(float observed, float closed, string failure)
|
||||
{
|
||||
return observed >= 0 && closed > observed + ERR_MortarPrediction.MAX_REPORT_AGE_MS && (failure.Contains("delivered target report expired") || failure.Contains("observation older than 60s"));
|
||||
}
|
||||
}
|
||||
|
||||
// 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_NativeMortarFiringProbe probe = ERR_NativeMortarFiringProbe.Active();
|
||||
if (probe) probe.NativeProjectile(GetOwner(), projectileEntity);
|
||||
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeMortarFiringProbe
|
||||
{
|
||||
protected static ERR_NativeMortarFiringProbe 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 bool m_WaitForExpiry;
|
||||
protected int m_FinalEvents;
|
||||
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_ClosureExpired;
|
||||
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-fire" || outcome == "mortar-expiry");
|
||||
}
|
||||
static ERR_NativeMortarFiringProbe Active() { return s_Active; }
|
||||
void ERR_NativeMortarFiringProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode;
|
||||
string outcome; System.GetCLIParam("errSmokeOutcome", outcome); m_WaitForExpiry = outcome == "mortar-expiry";
|
||||
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 firing " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: mortar firing " + 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 FIXTURE 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 FIXTURE: waiting for one authored battery"); return; }
|
||||
PrintFormat("ERR MORTAR FIXTURE: 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);
|
||||
}
|
||||
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)
|
||||
{ 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);
|
||||
}
|
||||
void NativeClosing(ERR_MortarBattery battery, string reason, string failure)
|
||||
{
|
||||
if (m_Stopped || !m_WaitForExpiry || battery != m_Battery || !battery.CommandOpen || battery.SourceId != m_Source || m_Source <= 0) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
m_ClosureSeen = true;
|
||||
m_ClosureComplete = battery.OrderShots == ERR_MortarSalvo.ROUNDS && m_Events == ERR_MortarSalvo.ROUNDS;
|
||||
m_ClosureExpired = ERR_NativeMortarEvidenceRules.ExpiredClosure(m_ObservedAt, now, failure);
|
||||
PrintFormat("ERR MORTAR FIXTURE CLOSURE: order=%1 observedAge=%2 reason=%3 lastFailure=%4 complete=%5 expiredWithheld=%6", battery.OrderId, now - m_ObservedAt, reason, failure, m_ClosureComplete, m_ClosureExpired);
|
||||
if (battery.Stopped() || battery.OrderId != m_Order || (!m_ClosureExpired && !m_ClosureComplete))
|
||||
{ m_BadProjectile = true; m_BadReason = "expiry evidence interrupted by Stop or unrelated/incomplete order closure"; }
|
||||
}
|
||||
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_Step != 3) || !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"; }
|
||||
m_CommitPending = false;
|
||||
PrintFormat("ERR MORTAR FIXTURE PROJECTILE: event=%1 entity=%2 origin=%3 order=%4 source=M%5 aimAge=%6 flight=%7; impact is not measured", m_Events, projectile, projectile.GetOrigin(), m_Battery.OrderId, m_Battery.SourceId, 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); }
|
||||
}
|
||||
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;
|
||||
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;
|
||||
m_Source = hq.ERR_QueueMortarFixtureReport(m_Group, m_Target, m_Impact, now);
|
||||
if (!Check(m_Source > 0, "synthetic observation enters actual existing radio uplink once")) return -1;
|
||||
m_ObservedAt = now; m_SawUplink = true; m_Deadline = now + 90000; m_Step = 1; return 0;
|
||||
}
|
||||
if (m_WaitForExpiry && m_Battery.Stopped()) { Check(false, "battery must remain running throughout natural expiry observation"); return -1; }
|
||||
if (m_Battery.Report && (m_Battery.Report.MessageId != m_Source || m_Battery.Report.ObservedAt != m_ObservedAt)) { Check(false, "delivered report identity and original observation timestamp remain immutable"); return -1; }
|
||||
if (m_Battery.Gun != m_Gun || m_Battery.Group != m_Group) { Check(false, "original gun and crew identity retained"); 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_Step == 1)
|
||||
{
|
||||
if (m_Battery.RadioOrder && m_Battery.SourceId == m_Source) { m_SawDownlink = true; m_Order = m_Battery.OrderId; }
|
||||
if (m_Battery.Activity && m_Battery.Report && m_Battery.Report.MessageId == m_Source) m_SawActivity = true;
|
||||
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 == 0) return 0;
|
||||
if (!Check(m_Events == 1 && m_Battery.TotalShots == 1 && m_Battery.OrderShots == 1 && !m_Battery.Released && m_SawUplink && m_SawDownlink && m_SawActivity && m_Order == 1, "one physical projectile follows real uplink/downlink, native activity and fresh aim")) return -1;
|
||||
if (!Check(remaining.Count() == 26 && m_Battery.CountShells() == 26, "one fired projectile consumes one of 27 original inventory shells")) return -1;
|
||||
if (m_WaitForExpiry)
|
||||
{
|
||||
m_Step = 3; m_Deadline = m_ObservedAt + ERR_MortarPrediction.MAX_REPORT_AGE_MS + 50000;
|
||||
return 0;
|
||||
}
|
||||
m_FinalEvents = m_Events;
|
||||
// Cancel on this later fixture tick, never reentrantly inside the firing node.
|
||||
hq.ERR_RemoveMortarFixtureReport(m_Target);
|
||||
system.Stop(); system.Stop();
|
||||
if (!Check(!m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "explicit support Stop closes native order and route idempotently")) return -1;
|
||||
m_StoppedShells = remaining.Count(); m_StopUntil = now + 12000; m_Deadline = m_StopUntil + 2000; m_Step = 2; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (m_Battery.OrderId != m_Order || m_Battery.SourceId != m_Source) { Check(false, "unchanged delivered observation received a second order"); return -1; }
|
||||
if (m_Events > ERR_MortarSalvo.ROUNDS || m_Battery.TotalShots > ERR_MortarSalvo.ROUNDS) { Check(false, "single observation cannot exceed finite salvo"); return -1; }
|
||||
if (now < m_ObservedAt + ERR_MortarPrediction.MAX_REPORT_AGE_MS + 25000) return 0;
|
||||
if (!Check(m_ClosureSeen && (m_ClosureExpired || m_ClosureComplete) && !m_CommitPending && m_Commits == m_Events && m_Events >= 1 && m_Battery.TotalShots == m_Events && m_Battery.OrderShots == m_Events && remaining.Count() == 27 - m_Events && !m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder && !m_Battery.Report, "explicit expired-withheld closure or complete finite salvo, with only fresh commits and observed physical shells consumed")) return -1;
|
||||
m_FinalEvents = m_Events;
|
||||
// Keep HQ running and the expired contact present throughout this next wait.
|
||||
m_StoppedShells = remaining.Count(); m_StopUntil = now + 12000; m_Deadline = m_StopUntil + 2000; m_Step = 4;
|
||||
return 0;
|
||||
}
|
||||
if (now < m_StopUntil) return 0;
|
||||
if (!Check(m_Battery.TotalShots == m_FinalEvents && m_Events == m_FinalEvents && m_Battery.OrderId == m_Order && remaining.Count() == m_StoppedShells && !m_Battery.CommandOpen && !m_Battery.Activity && !m_Battery.RadioOrder, "closed order issues no duplicate order, projectile or replenishment over 12 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;
|
||||
if (m_WaitForExpiry) { hq.ERR_RemoveMortarFixtureReport(m_Target); system.Stop(); system.Stop(); }
|
||||
Stop(); return 1;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq)
|
||||
{
|
||||
hq.ERR_RemoveMortarFixtureReport(m_Target);
|
||||
if (m_Battery && m_Battery.SourceId == m_Source && m_Source > 0) m_Battery.Cancel("mortar firing fixture cleanup");
|
||||
}
|
||||
ERR_Actors.DeleteSpawned(m_Target); m_Target = null;
|
||||
if (s_Active == this) s_Active = null;
|
||||
}
|
||||
void ~ERR_NativeMortarFiringProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,383 @@
|
||||
#ifdef WORKBENCH
|
||||
// Mechanism-only fixture: radio delivery and automatic artillery scheduling are
|
||||
// explicitly bypassed. All ammunition and inventory operations remain physical.
|
||||
modded class ERR_MortarSystem
|
||||
{
|
||||
protected ERR_NativeMortarTransferProbe m_ERRTransferFixture;
|
||||
bool ERR_HoldForTransfer(ERR_NativeMortarTransferProbe owner)
|
||||
{
|
||||
if (!ERR_NativeMortarTransferProbe.Enabled() || m_Stopped || m_ERRTransferFixture || !owner) return false;
|
||||
m_ERRTransferFixture = owner; return true;
|
||||
}
|
||||
void ERR_ReleaseTransferHold(ERR_NativeMortarTransferProbe owner)
|
||||
{
|
||||
if (m_ERRTransferFixture == owner) m_ERRTransferFixture = null;
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_ERRTransferFixture) return;
|
||||
super.Tick(now);
|
||||
}
|
||||
ERR_MortarBattery ERR_TransferBattery()
|
||||
{
|
||||
if (m_Batteries.Count() != 1) return null;
|
||||
return m_Batteries[0];
|
||||
}
|
||||
}
|
||||
modded class ERR_MortarBattery
|
||||
{
|
||||
ERR_MortarSupplyTransfer ERR_TransferForProbe() { return m_Transfer; }
|
||||
int ERR_TransferAttempts() { return m_AttemptedShells.Count(); }
|
||||
}
|
||||
modded class ERR_MortarSupplyTransfer
|
||||
{
|
||||
int ERR_CallbackState() { return m_Acknowledgement; }
|
||||
IEntity ERR_TransferredShell() { return m_Shell; }
|
||||
IEntity ERR_TransferRecipient() { return m_Gunner; }
|
||||
bool ERR_RequestAccepted() { return m_Accepted; }
|
||||
}
|
||||
class ERR_NativeMortarMoveCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
int Result;
|
||||
protected override void OnComplete() { if (Result == 0) Result = 1; }
|
||||
protected override void OnFailed() { if (Result == 0) Result = -1; }
|
||||
}
|
||||
class ERR_NativeMortarShellOwner
|
||||
{
|
||||
IEntity Shell;
|
||||
IEntity OriginalOwner;
|
||||
}
|
||||
|
||||
class ERR_NativeMortarTransferProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_MortarSystem m_System;
|
||||
protected ERR_MortarBattery m_Battery;
|
||||
protected IEntity m_Gunner;
|
||||
protected BaseCompartmentSlot m_Seat;
|
||||
protected ref array<IEntity> m_Crew = {};
|
||||
protected ref array<IEntity> m_Boxes = {};
|
||||
protected ref array<BaseInventoryStorageComponent> m_Stores = {};
|
||||
protected ref array<ref ERR_NativeMortarShellOwner> m_Original = {};
|
||||
protected ref ERR_NativeMortarMoveCallback m_MoveCallback;
|
||||
protected IEntity m_MoveShell;
|
||||
protected BaseInventoryStorageComponent m_MoveDestination;
|
||||
protected float m_MoveDeadline;
|
||||
protected ref ERR_MortarSupplyTransfer m_ObservedTransfer;
|
||||
protected IEntity m_FirstShell;
|
||||
protected IEntity m_SecondShell;
|
||||
protected int m_AttemptsAtStart;
|
||||
protected int m_ShotsAtStart;
|
||||
protected int m_CallbackAtCancel;
|
||||
protected int m_CancelOrder;
|
||||
protected int m_Step;
|
||||
protected float m_Deadline;
|
||||
protected float m_StepDeadline;
|
||||
protected float m_Wait;
|
||||
protected bool m_Held;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
static const ResourceName STORE = "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et";
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "mortar-transfer";
|
||||
}
|
||||
void ERR_NativeMortarTransferProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 180000;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: mortar transfer " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: mortar transfer " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool IsShell(IEntity item)
|
||||
{
|
||||
return item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL;
|
||||
}
|
||||
protected InventoryStorageManagerComponent Inventory(IEntity actor)
|
||||
{
|
||||
if (!actor) return null;
|
||||
return InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
}
|
||||
protected bool Has(IEntity actor, IEntity item)
|
||||
{
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(Inventory(actor), items);
|
||||
return item && items.Contains(item);
|
||||
}
|
||||
protected IEntity CrewOwner(IEntity item)
|
||||
{
|
||||
foreach (IEntity actor : m_Crew) if (Has(actor, item)) return actor;
|
||||
return null;
|
||||
}
|
||||
protected IEntity OriginalOwner(IEntity item)
|
||||
{
|
||||
foreach (ERR_NativeMortarShellOwner entry : m_Original) if (entry.Shell == item) return entry.OriginalOwner;
|
||||
return null;
|
||||
}
|
||||
protected bool InStore(IEntity item)
|
||||
{
|
||||
foreach (BaseInventoryStorageComponent storage : m_Stores) if (storage && storage.Contains(item)) return true;
|
||||
return false;
|
||||
}
|
||||
static bool Conservation(int originals, int actual, bool allOriginal, bool singleOwner)
|
||||
{
|
||||
return originals == 27 && actual == originals && allOriginal && singleOwner;
|
||||
}
|
||||
protected bool Conserved()
|
||||
{
|
||||
array<IEntity> actual = {};
|
||||
foreach (IEntity actor : m_Crew)
|
||||
{
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(Inventory(actor), items);
|
||||
foreach (IEntity item : items) if (IsShell(item) && !actual.Contains(item)) actual.Insert(item);
|
||||
}
|
||||
foreach (BaseInventoryStorageComponent storage : m_Stores)
|
||||
{
|
||||
if (!storage) return false;
|
||||
array<IEntity> items = {}; storage.GetAll(items);
|
||||
foreach (IEntity item : items) if (IsShell(item) && !actual.Contains(item)) actual.Insert(item);
|
||||
}
|
||||
bool allOriginal = true;
|
||||
bool singleOwner = true;
|
||||
foreach (ERR_NativeMortarShellOwner entry : m_Original)
|
||||
{
|
||||
if (!entry.Shell || !actual.Contains(entry.Shell)) allOriginal = false;
|
||||
int owners;
|
||||
foreach (IEntity actor : m_Crew) if (Has(actor, entry.Shell)) owners++;
|
||||
foreach (BaseInventoryStorageComponent storage : m_Stores) if (storage && storage.Contains(entry.Shell)) owners++;
|
||||
if (owners != 1) singleOwner = false;
|
||||
}
|
||||
return Conservation(m_Original.Count(), actual.Count(), allOriginal, singleOwner);
|
||||
}
|
||||
protected IEntity FirstGunnerShell()
|
||||
{
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(Inventory(m_Gunner), items);
|
||||
foreach (IEntity item : items) if (IsShell(item)) return item;
|
||||
return null;
|
||||
}
|
||||
protected bool BeginMove(IEntity shell, BaseInventoryStorageComponent destination, float now)
|
||||
{
|
||||
if (!shell || !destination || m_MoveCallback) return false;
|
||||
IEntity current = CrewOwner(shell);
|
||||
// A vanilla empty box has storage but no inventory manager. The original
|
||||
// recipient manager performs its explicit pickup from the external storage.
|
||||
if (!current) current = OriginalOwner(shell);
|
||||
InventoryStorageManagerComponent manager = Inventory(current);
|
||||
if (!manager) return false;
|
||||
m_MoveShell = shell; m_MoveDestination = destination;
|
||||
m_MoveCallback = new ERR_NativeMortarMoveCallback(); m_MoveDeadline = now + 8000;
|
||||
if (!manager.TryMoveItemToStorage(shell, destination, -1, m_MoveCallback)) return false;
|
||||
return true;
|
||||
}
|
||||
protected int MoveResult(float now)
|
||||
{
|
||||
if (!m_MoveCallback) return 1;
|
||||
if (m_MoveCallback.Result < 0 || now >= m_MoveDeadline) return -1;
|
||||
if (m_MoveCallback.Result == 0 || !m_MoveShell || !m_MoveDestination || !m_MoveDestination.Contains(m_MoveShell)) return 0;
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(m_MoveShell.FindComponent(InventoryItemComponent));
|
||||
if (!item || !item.GetParentSlot() || item.GetParentSlot().GetStorage() != m_MoveDestination) return 0;
|
||||
m_MoveCallback = null; m_MoveShell = null; m_MoveDestination = null;
|
||||
return 1;
|
||||
}
|
||||
protected bool Stash(IEntity shell, float now)
|
||||
{
|
||||
foreach (BaseInventoryStorageComponent storage : m_Stores)
|
||||
if (storage && storage.GetEstimatedCountFitForItem(shell) > 0) return BeginMove(shell, storage, now);
|
||||
return false;
|
||||
}
|
||||
protected void StageOrder(float now)
|
||||
{
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.Target = GetGame().GetPlayerController().GetControlledEntity();
|
||||
report.Source = m_Battery.Group;
|
||||
report.Position = m_Battery.Gun.GetOrigin() + "700 0 0";
|
||||
report.ObservedAt = now; report.DetailsObservedAt = now;
|
||||
report.TravelMode = "on foot"; report.MessageId = 900001 + m_Battery.OrderId;
|
||||
m_Battery.BeginRequest(report, now);
|
||||
m_Battery.PendingSupplyReport = report.Snapshot();
|
||||
PrintFormat("ERR MORTAR TRANSFER FIXTURE: synthetic supply order=%1; radio bypassed; scheduler held; no artillery activity authorized", m_Battery.OrderId);
|
||||
}
|
||||
protected bool NoFire()
|
||||
{
|
||||
return m_Battery && m_Battery.TotalShots == m_ShotsAtStart && !m_Battery.Activity && !m_Battery.RadioOrder && !m_Battery.Released;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Battery) m_Battery.Cancel("native transfer fixture stopped");
|
||||
if (m_System)
|
||||
{
|
||||
m_System.Stop(); // do not resume automatic fire while test items may be stored
|
||||
if (m_Held) m_System.ERR_ReleaseTransferHold(this);
|
||||
}
|
||||
m_Held = false;
|
||||
// Never delete a box containing borrowed shells or an outstanding operation.
|
||||
// Failure preserves it for diagnosis/normal round-owned cleanup.
|
||||
if (!m_MoveCallback)
|
||||
{
|
||||
foreach (IEntity box : m_Boxes)
|
||||
{
|
||||
if (!box) continue;
|
||||
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(box.FindComponent(BaseInventoryStorageComponent));
|
||||
array<IEntity> items = {}; if (storage) storage.GetAll(items);
|
||||
if (storage && items.IsEmpty()) ERR_Actors.DeleteSpawned(box);
|
||||
}
|
||||
}
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || now > m_Deadline) { Check(false, "enabled server fixture and bounded 180-second deadline"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_MoveCallback)
|
||||
{
|
||||
int moved = MoveResult(now);
|
||||
if (moved < 0) { Check(false, "fixture inventory move completed with exact destination before deadline; borrowed shells retained"); return -1; }
|
||||
if (moved == 0) return 0;
|
||||
if (!Check(Conserved(), "all original 27 shell identities conserved after fixture move")) return -1;
|
||||
}
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_System = ERR_MortarSystem.Get();
|
||||
if (!m_System) return 0;
|
||||
m_Battery = m_System.ERR_TransferBattery();
|
||||
if (!m_Battery || !m_Battery.StartupActivated) return 0;
|
||||
if (!Check(m_Battery.Operational() && m_Battery.CountShells() == 27 && !m_Battery.Activity && !m_Battery.RadioOrder && !m_Battery.CommandOpen, "actual initialized idle battery has finite 27 shells")) return -1;
|
||||
if (!Check(m_System.ERR_HoldForTransfer(this), "opt-in mechanism fixture holds automatic mortar scheduler")) return -1;
|
||||
m_Held = true; m_AttemptsAtStart = m_Battery.ERR_TransferAttempts(); m_ShotsAtStart = m_Battery.TotalShots;
|
||||
array<AIAgent> agents = {}; m_Battery.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) continue;
|
||||
IEntity actor = agent.GetControlledEntity(); if (m_Crew.Contains(actor)) continue;
|
||||
m_Crew.Insert(actor);
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(Inventory(actor), items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!IsShell(item)) continue;
|
||||
ERR_NativeMortarShellOwner entry = new ERR_NativeMortarShellOwner(); entry.Shell = item; entry.OriginalOwner = actor; m_Original.Insert(entry);
|
||||
}
|
||||
if (!m_Gunner && actor != m_Battery.RadioOperator) m_Gunner = actor;
|
||||
}
|
||||
if (!Check(m_Crew.Count() == 4 && m_Original.Count() == 27 && m_Gunner != null, "four real crew and original shell-owner identities captured")) return -1;
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Battery.Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (usage) m_Seat = usage.GetTurretCompartmentSlot();
|
||||
if (m_Seat && m_Seat.GetOccupant())
|
||||
{
|
||||
IEntity occupant = m_Seat.GetOccupant();
|
||||
if (!Check(m_Crew.Contains(occupant) && occupant != m_Battery.RadioOperator && ERR_Actors.CanFight(occupant), "existing native seat occupant is original fighting gunner")) return -1;
|
||||
m_Gunner = occupant;
|
||||
}
|
||||
else
|
||||
{
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Gunner.FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(m_Seat && access && access.GetInVehicle(m_Battery.Gun, m_Seat, true, -1, ECloseDoorAfterActions.INVALID, false), "native seating request selects actual original crew gunner")) return -1;
|
||||
}
|
||||
if (!Check(m_Battery.InventoryShells(Inventory(m_Gunner)) == 9, "selected actual gunner owns its original nine physical shells")) return -1;
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
IEntity box = ERR_Actors.Spawn(STORE, ERR_Actors.Ground(m_Battery.Gun.GetOrigin() + Vector(3 + i * 2, 0, 3)));
|
||||
if (!Check(box != null, "fixture-owned empty vanilla storage created")) return -1;
|
||||
m_Boxes.Insert(box); m_Mode.Own(box);
|
||||
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(box.FindComponent(BaseInventoryStorageComponent));
|
||||
if (!Check(storage != null, "real vanilla storage component available without attaching it to gunner inventory")) return -1;
|
||||
m_Stores.Insert(storage);
|
||||
}
|
||||
m_Step = 1; m_StepDeadline = now + 8000; m_Wait = now + 500; return 0;
|
||||
}
|
||||
if (!NoFire()) { Check(false, "no artillery activity, committed load or physical projectile during inventory mechanism test"); return -1; }
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (m_Seat.GetOccupant() != m_Gunner)
|
||||
{
|
||||
if (now >= m_StepDeadline) { Check(false, "actual original gunner occupies native mortar seat"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (!Check(Conserved(), "27 original shells have exactly one observed owner each")) return -1;
|
||||
m_Step = 2;
|
||||
}
|
||||
if (m_Step == 2 || m_Step == 5)
|
||||
{
|
||||
IEntity shell = FirstGunnerShell();
|
||||
if (shell)
|
||||
{
|
||||
if (!Check(Stash(shell, now), "one existing gunner shell move to vanilla store accepted; no shell minted")) return -1;
|
||||
return 0;
|
||||
}
|
||||
if (!Check(m_Battery.InventoryShells(Inventory(m_Gunner)) == 0 && Conserved(), "actual gunner empty while all 27 original shells remain in crew or fixture storage")) return -1;
|
||||
StageOrder(now);
|
||||
ERR_MortarSupplyState started = m_Battery.SupplyState(now);
|
||||
m_ObservedTransfer = m_Battery.ERR_TransferForProbe();
|
||||
if (!Check(started == ERR_MortarSupplyState.PENDING && m_ObservedTransfer && m_ObservedTransfer.ERR_RequestAccepted() && m_ObservedTransfer.ERR_TransferRecipient() == m_Gunner && m_ObservedTransfer.CapturedOrder() == m_Battery.OrderId, "production exact-shell request accepted once with captured current order and real gunner")) return -1;
|
||||
if (m_Step == 2)
|
||||
{
|
||||
m_FirstShell = m_ObservedTransfer.ERR_TransferredShell();
|
||||
m_Step = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_SecondShell = m_ObservedTransfer.ERR_TransferredShell(); m_CallbackAtCancel = m_ObservedTransfer.ERR_CallbackState(); m_CancelOrder = m_Battery.OrderId;
|
||||
m_Battery.Cancel("native fixture cancels authorization immediately after accepted inventory request");
|
||||
if (!Check(m_SecondShell && m_SecondShell != m_FirstShell && !m_Battery.CommandOpen && m_Battery.SupplyState(now) == ERR_MortarSupplyState.FAILED, "canceled current order cannot report supply READY or issue another request")) return -1;
|
||||
PrintFormat("ERR MORTAR TRANSFER CANCEL: callbackBeforeCancel=%1 order=%2; accepted native move is not retracted", m_CallbackAtCancel, m_CancelOrder);
|
||||
m_Step = 6;
|
||||
}
|
||||
m_StepDeadline = now + 8000; m_Wait = now + 250; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
ERR_MortarSupplyState state = m_Battery.SupplyState(now);
|
||||
if (state == ERR_MortarSupplyState.FAILED || now >= m_StepDeadline) { Check(false, "production transfer completes within its bounded deadline"); return -1; }
|
||||
if (state == ERR_MortarSupplyState.PENDING) return 0;
|
||||
if (!Check(m_ObservedTransfer.ERR_CallbackState() == 1 && Has(m_Gunner, m_FirstShell) && !Has(OriginalOwner(m_FirstShell), m_FirstShell) && Conserved() && m_Battery.ERR_TransferAttempts() == m_AttemptsAtStart + 1, "successful native callback and exact donor-to-gunner ownership, one attempt, original 27 conserved")) return -1;
|
||||
if (!Check(m_Battery.SupplyState(now) == ERR_MortarSupplyState.READY && m_Battery.ERR_TransferAttempts() == m_AttemptsAtStart + 1, "repeated supply check reuses actual inventory without another move")) return -1;
|
||||
m_Battery.Cancel("native success subcase finished; no artillery activity requested");
|
||||
m_Step = 5; return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
if (m_Battery.SupplyState(now) != ERR_MortarSupplyState.FAILED || m_Battery.OrderId != m_CancelOrder || m_Battery.ERR_TransferAttempts() != m_AttemptsAtStart + 2) { Check(false, "canceled order/generation remains failed with exactly two total move attempts"); return -1; }
|
||||
if (m_ObservedTransfer.ERR_CallbackState() == 0 || m_ObservedTransfer.Unresolved())
|
||||
{
|
||||
if (now >= m_StepDeadline) { Check(false, "accepted canceled operation reached definite native outcome; no ambiguous retry"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (!Check(m_ObservedTransfer.ERR_CallbackState() == 1 && Has(m_Gunner, m_SecondShell) && Conserved(), "canceled accepted native move has definite physical outcome with exactly two attempts and original 27 conserved")) return -1;
|
||||
if (m_CallbackAtCancel == 0) Print("ERR SMOKE PASS: mortar transfer actual native completion observed after authorization cancellation");
|
||||
else Print("ERR MORTAR TRANSFER LIMIT: native callback completed synchronously before cancellation; late callback timing not exercised");
|
||||
m_Step = 7; return 0;
|
||||
}
|
||||
if (m_Step == 7)
|
||||
{
|
||||
// Return foreign shells first, leaving room for each original nine-round kit.
|
||||
IEntity misplaced;
|
||||
foreach (ERR_NativeMortarShellOwner entry : m_Original)
|
||||
if (Has(m_Gunner, entry.Shell) && entry.OriginalOwner != m_Gunner) { misplaced = entry.Shell; break; }
|
||||
if (!misplaced)
|
||||
foreach (ERR_NativeMortarShellOwner entry : m_Original) if (InStore(entry.Shell)) { misplaced = entry.Shell; break; }
|
||||
if (misplaced)
|
||||
{
|
||||
InventoryStorageManagerComponent recipient = Inventory(OriginalOwner(misplaced));
|
||||
BaseInventoryStorageComponent destination;
|
||||
if (recipient) destination = recipient.FindStorageForItem(misplaced, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
if (!Check(destination && BeginMove(misplaced, destination, now), "one borrowed shell return to its original crew owner accepted")) return -1;
|
||||
return 0;
|
||||
}
|
||||
foreach (ERR_NativeMortarShellOwner entry : m_Original)
|
||||
if (!Check(Has(entry.OriginalOwner, entry.Shell), "original shell identity returned to original crew inventory")) return -1;
|
||||
if (!Check(Conserved() && m_Battery.CountShells() == 27 && NoFire(), "all original ammunition restored; no minted/deleted shell or shot")) return -1;
|
||||
foreach (BaseInventoryStorageComponent storage : m_Stores)
|
||||
{
|
||||
array<IEntity> items = {}; storage.GetAll(items);
|
||||
if (!Check(items.IsEmpty(), "borrowed-ammunition fixture storage empty before deletion")) return -1;
|
||||
}
|
||||
Stop(); m_Complete = true; return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user