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
|
||||
@@ -0,0 +1,284 @@
|
||||
#ifdef WORKBENCH
|
||||
// Read-only evidence hooks. Every production method delegates unchanged.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
void ERR_ResponseUnits(out array<ERR_HQUnit> units)
|
||||
{
|
||||
units.Clear();
|
||||
if (!ERR_NativeHQResponseProof.Enabled()) return;
|
||||
foreach (ERR_HQUnit unit : m_Units) units.Insert(unit);
|
||||
}
|
||||
protected override void Observe(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_HQReport previous = unit.PendingReport;
|
||||
super.Observe(unit, now);
|
||||
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
||||
if (proof && unit.PendingReport && unit.PendingReport != previous) proof.Observed(this, unit, now);
|
||||
}
|
||||
protected override void Accept(ERR_HQReport report, float now)
|
||||
{
|
||||
super.Accept(report, now);
|
||||
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
||||
if (!proof || !report) return;
|
||||
foreach (ERR_HQReport accepted : m_Contacts)
|
||||
if (accepted.MessageId == report.MessageId) { proof.Accepted(this, accepted, now); break; }
|
||||
}
|
||||
protected override void DeliverOrder(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_HQReport order = unit.PendingOrder;
|
||||
ERR_HQRoute route = unit.Downlink;
|
||||
super.DeliverOrder(unit, now);
|
||||
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
||||
if (proof && order && route && unit.Assignment == order && unit.Waypoint) proof.Delivered(this, unit, route, now);
|
||||
}
|
||||
}
|
||||
modded class ERR_HQJournal
|
||||
{
|
||||
override void Write(string kind, string message, int messageId = 0)
|
||||
{
|
||||
super.Write(kind, message, messageId);
|
||||
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
||||
if (proof) proof.JournalEvent(this, kind, message, messageId);
|
||||
}
|
||||
}
|
||||
modded class SCR_AIGroupUtilityComponent
|
||||
{
|
||||
IEntity ERR_ResponseVisualObserver(ERR_HQReport report, float now)
|
||||
{
|
||||
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
|
||||
{
|
||||
if (!info || !info.m_Perception) continue;
|
||||
AIAgent agent = AIAgent.Cast(info.GetOwner());
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) continue;
|
||||
array<BaseTarget> targets = {}; info.m_Perception.GetTargetsList(targets, ETargetCategory.ENEMY);
|
||||
foreach (BaseTarget target : targets)
|
||||
{
|
||||
ERR_HQVisualContact sample = ERR_HQObservation.Capture(target, now);
|
||||
if (sample && sample.Entity == report.Target && sample.Escapist && Math.AbsFloat(sample.ObservedAt - report.ObservedAt) < 1 && vector.Distance(sample.Position, report.Position) < 0.1) return agent.GetControlledEntity();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class ERR_NativeResponseUnit
|
||||
{
|
||||
ERR_HQUnit Unit;
|
||||
SCR_AIGroup Group;
|
||||
IEntity Radio;
|
||||
ref array<IEntity> Actors = {};
|
||||
}
|
||||
class ERR_NativeResponseJournal
|
||||
{
|
||||
bool Report;
|
||||
bool Transmit;
|
||||
bool HQReceive;
|
||||
bool HQTransmit;
|
||||
bool UnitReceive;
|
||||
bool Delivered;
|
||||
}
|
||||
class ERR_NativeResponseLineage
|
||||
{
|
||||
ref ERR_HQReport Report;
|
||||
ERR_HQUnit Source;
|
||||
IEntity Observer;
|
||||
IEntity SourceRadio;
|
||||
ref ERR_HQRoute Uplink;
|
||||
bool Accepted;
|
||||
float AcceptedAt;
|
||||
ERR_HQUnit Recipient;
|
||||
IEntity RecipientRadio;
|
||||
int DeliveryId;
|
||||
AIWaypoint Waypoint;
|
||||
IEntity MovingActor;
|
||||
vector DeliveredOrigin;
|
||||
float DeliveredAt;
|
||||
}
|
||||
class ERR_NativeHQResponseProof
|
||||
{
|
||||
protected static ERR_NativeHQResponseProof s_Active;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_HQ m_HQ;
|
||||
protected IEntity m_Player;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected ERR_PlayerLife m_Life;
|
||||
protected ERR_HQUnit m_Source;
|
||||
protected DamageManagerComponent m_Damage;
|
||||
protected bool m_DamageWasEnabled;
|
||||
protected bool m_Protected;
|
||||
protected bool m_Started;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Failed;
|
||||
protected string m_Failure;
|
||||
protected float m_Deadline;
|
||||
protected float m_NextDiagnostic;
|
||||
protected ref array<ref ERR_NativeResponseUnit> m_Units = {};
|
||||
protected ref array<IEntity> m_Forces = {};
|
||||
protected ref array<ref ERR_NativeResponseLineage> m_Reports = {};
|
||||
protected ref map<int, ref ERR_NativeResponseJournal> m_Journal = new map<int, ref ERR_NativeResponseJournal>();
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-response";
|
||||
}
|
||||
static ERR_NativeHQResponseProof Active() { return s_Active; }
|
||||
void ERR_NativeHQResponseProof(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
m_Mode = mode; m_Player = player; m_HQ = ERR_HQ.Get(); m_Deadline = GetGame().GetWorld().GetWorldTime() + 140000;
|
||||
if (Enabled() && !s_Active) s_Active = this;
|
||||
}
|
||||
protected bool Fail(string reason) { m_Failed = true; m_Failure = reason; return false; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: HQ response " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: HQ response " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected ERR_NativeResponseUnit Original(ERR_HQUnit unit)
|
||||
{
|
||||
foreach (ERR_NativeResponseUnit original : m_Units) if (original.Unit == unit && original.Group == unit.Group) return original;
|
||||
return null;
|
||||
}
|
||||
protected ERR_NativeResponseLineage Report(int id)
|
||||
{
|
||||
foreach (ERR_NativeResponseLineage report : m_Reports) if (report.Report.MessageId == id) return report;
|
||||
return null;
|
||||
}
|
||||
protected bool Canonical()
|
||||
{
|
||||
return m_Player && m_Controller && m_Life && m_Mode.Players().CurrentActor(m_Player) == m_Life && m_Controller == GetGame().GetPlayerController() && m_Controller.GetControlledEntity() == m_Player && m_Life.Actor == m_Player && m_Life.Connected && m_Life.UsedLife;
|
||||
}
|
||||
protected bool UnchangedForces()
|
||||
{
|
||||
array<ERR_HQUnit> current = {}; m_HQ.ERR_ResponseUnits(current);
|
||||
if (current.Count() != m_Units.Count()) return false;
|
||||
foreach (ERR_HQUnit unit : current)
|
||||
{
|
||||
ERR_NativeResponseUnit original = Original(unit);
|
||||
if (!original) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != original.Actors.Count()) return false;
|
||||
foreach (AIAgent agent : agents) if (!agent || !original.Actors.Contains(agent.GetControlledEntity())) return false;
|
||||
}
|
||||
array<IEntity> forces = {}; m_HQ.PhysicalForces(forces);
|
||||
if (forces.Count() != m_Forces.Count()) return false;
|
||||
foreach (IEntity entity : forces) if (!m_Forces.Contains(entity)) return false;
|
||||
return true;
|
||||
}
|
||||
protected bool Arrange()
|
||||
{
|
||||
array<ERR_HQUnit> units = {}; m_HQ.ERR_ResponseUnits(units);
|
||||
foreach (ERR_HQUnit unit : units)
|
||||
{
|
||||
if (!unit.Group || !unit.Utility || !unit.Radio || ERR_HQRadio.Capability(unit.Radio) != 2) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.IsEmpty() || !unit.Group.IsExpandComplete()) return false;
|
||||
ERR_NativeResponseUnit original = new ERR_NativeResponseUnit(); original.Unit = unit; original.Group = unit.Group; original.Radio = unit.Radio;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
original.Actors.Insert(agent.GetControlledEntity());
|
||||
}
|
||||
m_Units.Insert(original);
|
||||
if (!m_Source && !unit.ReservedForSupport) m_Source = unit;
|
||||
}
|
||||
if (!m_Source || m_Units.Count() < 2) return false;
|
||||
m_HQ.PhysicalForces(m_Forces);
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); m_Life = m_Mode.Players().CurrentActor(m_Player);
|
||||
if (!Canonical()) return false;
|
||||
m_Damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
||||
if (!m_Damage) return false;
|
||||
m_DamageWasEnabled = m_Damage.IsDamageHandlingEnabled(); m_Damage.EnableDamageHandling(false); m_Protected = true;
|
||||
// Preserve the established fixture arrangement: move only the real prisoner.
|
||||
// Native perception must discover him; neither targets nor reports are seeded.
|
||||
vector position = ERR_Actors.Ground(m_Source.Group.GetOrigin() + "0 0 20");
|
||||
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = position;
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
m_Started = true;
|
||||
PrintFormat("ERR HQ RESPONSE ARRANGEMENT: canonical prisoner moved to=%1 near original source group=%2; temporary player damage protection=%3; no AI/report/radio cadence changes", position, m_Source.Id, m_Protected);
|
||||
return true;
|
||||
}
|
||||
void JournalEvent(ERR_HQJournal journal, string kind, string message, int messageId)
|
||||
{
|
||||
if (!m_Started || m_Stopped || !m_HQ || journal != m_HQ.Journal() || messageId <= 0) return;
|
||||
ERR_NativeResponseJournal entry = m_Journal.Get(messageId);
|
||||
if (!entry)
|
||||
{
|
||||
if (m_Journal.Count() >= 128) { Fail("bounded journal evidence map exhausted"); return; }
|
||||
entry = new ERR_NativeResponseJournal(); m_Journal.Insert(messageId, entry);
|
||||
}
|
||||
if (kind == "REPORT") entry.Report = true;
|
||||
if (kind == "TRANSMIT") { entry.Transmit = true; if (message.StartsWith("HQ -> ")) entry.HQTransmit = true; }
|
||||
if (kind == "RECEIVE") { if (message.StartsWith("HQ received from ")) entry.HQReceive = true; else if (message.StartsWith("unit ")) entry.UnitReceive = true; }
|
||||
if (kind == "ORDER DELIVERED") entry.Delivered = true;
|
||||
}
|
||||
void Observed(ERR_HQ hq, ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (!m_Started || m_Stopped || hq != m_HQ || unit != m_Source || unit.PendingReport.Target != m_Player) return;
|
||||
ERR_HQReport report = unit.PendingReport;
|
||||
ERR_NativeResponseUnit original = Original(unit);
|
||||
IEntity observer = unit.Utility.ERR_ResponseVisualObserver(report, now);
|
||||
if (!Canonical() || !original || !original.Actors.Contains(observer) || !unit.Uplink || unit.Uplink.Nodes.IsEmpty() || unit.Uplink.Nodes[0].Carrier != unit.Radio || unit.Uplink.MessageId != report.MessageId || unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || report.Source != original.Group || report.ObservedAt > now || Report(report.MessageId)) { Fail("real observed report lacks canonical original observer/radio/source lineage"); return; }
|
||||
if (m_Reports.Count() >= 32) { Fail("bounded report evidence exhausted"); return; }
|
||||
ERR_NativeResponseLineage proof = new ERR_NativeResponseLineage(); proof.Report = report.Snapshot(); proof.Source = unit; proof.Observer = observer; proof.SourceRadio = unit.Radio; proof.Uplink = unit.Uplink; m_Reports.Insert(proof);
|
||||
PrintFormat("ERR HQ RESPONSE SIGHTING: source=%1 report=M%2 observer=%3 radio=%4 target=%5 observedAt=%6; actual native visual sample", unit.Id, report.MessageId, observer, unit.Radio, m_Player, report.ObservedAt);
|
||||
}
|
||||
void Accepted(ERR_HQ hq, ERR_HQReport report, float now)
|
||||
{
|
||||
if (!m_Started || m_Stopped || hq != m_HQ) return;
|
||||
ERR_NativeResponseLineage proof = Report(report.MessageId); if (!proof) return;
|
||||
if (report.Target != m_Player || report.Source != proof.Report.Source || report.ObservedAt != proof.Report.ObservedAt || vector.Distance(report.Position, proof.Report.Position) > 0.01 || !proof.Uplink || proof.Uplink.Failed || proof.Uplink.Sending || proof.Uplink.Cursor < proof.Uplink.Nodes.Count()) { Fail("accepted report lacks completed actual uplink or immutable observed time"); return; }
|
||||
proof.Accepted = true; proof.AcceptedAt = now;
|
||||
}
|
||||
void Delivered(ERR_HQ hq, ERR_HQUnit unit, ERR_HQRoute route, float now)
|
||||
{
|
||||
if (!m_Started || m_Stopped || hq != m_HQ || !unit.Assignment) return;
|
||||
ERR_NativeResponseLineage proof = Report(unit.Assignment.MessageId); if (!proof || proof.Recipient) return;
|
||||
ERR_NativeResponseUnit original = Original(unit);
|
||||
if (!proof.Accepted || !original || unit == proof.Source || !unit.Assignment || unit.Assignment.Target != m_Player || unit.Assignment.Source != proof.Report.Source || unit.Assignment.ObservedAt != proof.Report.ObservedAt || vector.Distance(unit.Assignment.Position, proof.Report.Position) > 0.01 || unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || !route.Downlink || route.Nodes.IsEmpty() || route.Nodes[route.Nodes.Count() - 1].Carrier != unit.Radio || route.Failed || route.Sending || route.Cursor != route.Nodes.Count() - 1 || !unit.Waypoint) { Fail("delivered response lacks immutable accepted report and original physical recipient/downlink"); return; }
|
||||
proof.Recipient = unit; proof.RecipientRadio = unit.Radio; proof.DeliveryId = route.MessageId; proof.Waypoint = unit.Waypoint; proof.DeliveredAt = now;
|
||||
foreach (IEntity actor : original.Actors) if (ERR_Actors.CanFight(actor)) { proof.MovingActor = actor; break; }
|
||||
if (proof.MovingActor) proof.DeliveredOrigin = proof.MovingActor.GetOrigin();
|
||||
PrintFormat("ERR HQ RESPONSE DELIVERED: report=M%1 downlink=M%2 source=%3 recipient=%4 radio=%5 waypoint=%6 observedAt=%7", proof.Report.MessageId, route.MessageId, proof.Source.Id, unit.Id, unit.Radio, unit.Waypoint, unit.Assignment.ObservedAt);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || s_Active != this || !m_Mode || !m_Mode.Active() || !m_HQ || now > m_Deadline || m_Failed) { Check(false, "bounded active evidence context: " + m_Failure); return -1; }
|
||||
if (!m_Started)
|
||||
{
|
||||
if (!m_HQ.ERR_ProbeSquadsReady()) return 0;
|
||||
array<IEntity> guns = {}; m_HQ.Mortars().AppendGuns(guns);
|
||||
foreach (IEntity gun : guns) { ERR_MortarBattery battery = m_HQ.Mortars().FindGun(gun); if (!battery || !battery.StartupActivated || !battery.Operational() || !m_HQ.MortarConnected(battery.Group)) return 0; }
|
||||
if (!Check(Arrange(), "original physical force, RTOs and canonical prisoner captured before arranged sighting")) return -1;
|
||||
return 0;
|
||||
}
|
||||
if (!Canonical() || !UnchangedForces()) { Check(false, "canonical player and all original physical group/actor identities retained without added force"); return -1; }
|
||||
if (now >= m_NextDiagnostic)
|
||||
{
|
||||
PrintFormat("ERR HQ RESPONSE PROOF: observed source reports=%1; %2", m_Reports.Count(), m_HQ.ERR_ProbePerception()); m_NextDiagnostic = now + 10000;
|
||||
}
|
||||
foreach (ERR_NativeResponseLineage proof : m_Reports)
|
||||
{
|
||||
if (!proof.Recipient || !proof.MovingActor) continue;
|
||||
if (proof.Recipient.Waypoint != proof.Waypoint || !proof.Recipient.Assignment || proof.Recipient.Assignment.MessageId != proof.Report.MessageId) continue;
|
||||
ERR_NativeResponseJournal uplink = m_Journal.Get(proof.Report.MessageId);
|
||||
ERR_NativeResponseJournal downlink = m_Journal.Get(proof.DeliveryId);
|
||||
if (!uplink || !downlink || !uplink.Report || !uplink.Transmit || !uplink.HQReceive || !downlink.HQTransmit || !downlink.UnitReceive || !downlink.Delivered) continue;
|
||||
if (proof.Recipient.Group.GetCurrentWaypoint() != proof.Waypoint || vector.DistanceXZ(proof.MovingActor.GetOrigin(), proof.DeliveredOrigin) < 3) continue;
|
||||
if (vector.DistanceXZ(proof.MovingActor.GetOrigin(), proof.Report.Position) >= vector.DistanceXZ(proof.DeliveredOrigin, proof.Report.Position) - 1) continue;
|
||||
if (!Check(proof.SourceRadio && proof.RecipientRadio && m_HQ.ERR_ProbeUniqueAssignments(), "actual sighting/report/uplink/HQ receipt/downlink journal lineage reaches distinct original infantry with bounded unique waypoints")) return -1;
|
||||
if (!Check(UnchangedForces(), "original recipient physically moves toward its native Search waypoint with no replacement or additional force")) return -1;
|
||||
Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Protected && m_Damage) m_Damage.EnableDamageHandling(m_DamageWasEnabled);
|
||||
m_Protected = false;
|
||||
if (s_Active == this) s_Active = null;
|
||||
}
|
||||
void ~ERR_NativeHQResponseProof() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
@@ -17,8 +17,13 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestRadioNetwork;
|
||||
protected bool m_ERRTestHQResponse;
|
||||
protected bool m_ERRTestMortar;
|
||||
protected bool m_ERRTestMortarFire;
|
||||
protected bool m_ERRTestMortarTransfer;
|
||||
protected ref ERR_NativeMortarTransferProbe m_ERRMortarTransfer;
|
||||
protected ref ERR_NativeMortarFiringProbe m_ERRMortarFire;
|
||||
protected float m_ERRNextProbeLog;
|
||||
protected ref ERR_NativeRadioProbe m_ERRRadioProbe;
|
||||
protected ref ERR_NativeHQResponseProof m_ERRHQResponseProof;
|
||||
protected ref array<IEntity> m_ERRCacheFolders = {};
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
@@ -31,9 +36,13 @@ modded class ERR_GameMode
|
||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestMortar = outcome == "mortar-inventory";
|
||||
m_ERRTestMortarFire = outcome == "mortar-fire" || outcome == "mortar-expiry";
|
||||
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_ERRTestMortarTransfer) 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);
|
||||
OnPhaseChanged().Insert(ERR_SmokePhase);
|
||||
@@ -52,6 +61,9 @@ modded class ERR_GameMode
|
||||
}
|
||||
protected void ERR_StopHQPatrol()
|
||||
{
|
||||
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
|
||||
if (m_ERRMortarTransfer) m_ERRMortarTransfer.Stop();
|
||||
if (m_ERRMortarFire) m_ERRMortarFire.Stop();
|
||||
if (m_ERRHQPatrolMovement) m_ERRHQPatrolMovement.Stop();
|
||||
if (m_ERRHQPatrolOrder) m_ERRHQPatrolOrder.Stop();
|
||||
}
|
||||
@@ -87,6 +99,24 @@ modded class ERR_GameMode
|
||||
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
|
||||
if (m_ERRTestMortarTransfer)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 161, "native exact shell transfers and original-owner restoration complete before explicit reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: native exact mortar inventory transfer, accepted-operation cancellation and original-owner restoration; radio bypassed; fixture reset requested");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarFire)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 151, "native mortar first shot and stop complete before explicit reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: native HQ radio mortar projectile and configured cancellation/expiry; fixture reset requested");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestPatrolHQ)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 133, "HQ patrol preemption and physical resumption before explicit reset")) return;
|
||||
@@ -149,6 +179,20 @@ 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 == 160)
|
||||
{
|
||||
int result = m_ERRMortarTransfer.Tick(now);
|
||||
if (result < 0) { ERR_Check(false, "native mortar exact-shell transfer fixture"); return; }
|
||||
if (result == 1) { m_ERRSmokeStep = 161; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRSmokeStep == 150)
|
||||
{
|
||||
int result = m_ERRMortarFire.Tick(now);
|
||||
if (result < 0) { ERR_Check(false, "native mortar firing probe failed; see diagnostics"); return; }
|
||||
if (result == 1) { m_ERRSmokeStep = 151; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRSmokeStep == 130 || m_ERRSmokeStep == 132)
|
||||
{
|
||||
int result = m_ERRHQPatrolMovement.Tick(now);
|
||||
@@ -181,6 +225,20 @@ modded class ERR_GameMode
|
||||
m_ERRSmokeStep = 130;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarTransfer)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRMortarTransfer = new ERR_NativeMortarTransferProbe(this);
|
||||
m_ERRSmokeStep = 160;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortarFire)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRMortarFire = new ERR_NativeMortarFiringProbe(this);
|
||||
m_ERRSmokeStep = 150;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestMortar)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
@@ -189,7 +247,7 @@ modded class ERR_GameMode
|
||||
}
|
||||
if (m_ERRTestHQResponse)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRHQResponseProof = new ERR_NativeHQResponseProof(this, m_ERRSmokePlayer);
|
||||
m_ERRSmokeStep = 110;
|
||||
return;
|
||||
}
|
||||
@@ -343,23 +401,9 @@ modded class ERR_GameMode
|
||||
}
|
||||
else if (m_ERRSmokeStep == 110)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Cast(Command());
|
||||
if (!hq || !hq.ERR_ProbeSquadsReady()) return;
|
||||
ERR_MovePlayer(hq.ERR_ProbeResponseOrigin());
|
||||
m_ERRSmokeStep = 111;
|
||||
m_ERRSmokeWait = now + 1000;
|
||||
}
|
||||
else if (m_ERRSmokeStep == 111)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Cast(Command());
|
||||
if (now >= m_ERRNextProbeLog)
|
||||
{
|
||||
Print("ERR HQ PROBE: " + hq.ERR_ProbePerception());
|
||||
m_ERRNextProbeLog = now + 10000;
|
||||
}
|
||||
if (!hq.ERR_ProbeDeliveredResponse()) return;
|
||||
if (!ERR_Check(true, "native visual contact reaches HQ and orders a different existing squad")) return;
|
||||
if (!ERR_Check(hq.ERR_ProbeUniqueAssignments(), "HQ uses distinct waypoints within the existing force and order limits")) return;
|
||||
int result = m_ERRHQResponseProof.Tick(now);
|
||||
if (result < 0) { ERR_Check(false, "native HQ response evidence failed; inspect report lineage diagnostics"); return; }
|
||||
if (result == 0) return;
|
||||
m_ERRSmokeStep = 9;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
// Discovery markers are authored objective information, not player annotations.
|
||||
// Native local insertion overwrites owner ID before OnCreateMarker; clear it at
|
||||
// that boundary so native edit/drag cannot move a discovered objective.
|
||||
class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
||||
{
|
||||
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
|
||||
override void OnCreateMarker(bool skipProfanityFilter = false)
|
||||
{
|
||||
SetMarkerOwnerID(-1);
|
||||
// Text is scenario-authored, never user input. Avoid delayed UGC callbacks
|
||||
// overwriting a newer discovery label after the location becomes identified.
|
||||
super.OnCreateMarker(true);
|
||||
}
|
||||
override void SetCustomText(string text)
|
||||
{
|
||||
super.SetCustomText(text);
|
||||
// Native SetCustomText updates data only; an already-open map needs its
|
||||
// existing widget updated too. Closed maps use the saved text on creation.
|
||||
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
|
||||
if (GetRootWidget() && widget) widget.SetText(text, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Read replicated discovery locally. A server-wide marker list never leaks
|
||||
// locations which have not been recovered through gameplay.
|
||||
modded class ERR_LocationComponent
|
||||
@@ -17,15 +40,6 @@ modded class ERR_LocationComponent
|
||||
if (!GetGame().GetPlayerController() || Knowledge() <= 0 || (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel)) return;
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return;
|
||||
if (!m_ERRMarker)
|
||||
{
|
||||
m_ERRMarker = new SCR_MapMarkerBase();
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
vector p = Position();
|
||||
m_ERRMarker.SetWorldPos(Math.Round(p[0]), Math.Round(p[2]));
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
manager.InsertStaticMarker(m_ERRMarker, true);
|
||||
}
|
||||
string label = "Unknown location";
|
||||
if (Knowledge() == 2)
|
||||
{
|
||||
@@ -36,6 +50,16 @@ modded class ERR_LocationComponent
|
||||
else if (Kind() == ERR_LocationKind.PRISON) label = "Prison";
|
||||
else label = Id();
|
||||
}
|
||||
if (!m_ERRMarker)
|
||||
{
|
||||
m_ERRMarker = new ERR_DiscoveryMarker();
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
vector p = Position();
|
||||
m_ERRMarker.SetWorldPos(Math.Round(p[0]), Math.Round(p[2]));
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
manager.InsertStaticMarker(m_ERRMarker, true);
|
||||
}
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
m_ERRShown = Knowledge();
|
||||
m_ERRShownLabel = Label();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// A round-local roster identity is not a Bohemia/platform identity. Only the
|
||||
// real native admission callback may select it for the sole offline controller.
|
||||
class ERR_LocalAdmissionRules
|
||||
{
|
||||
static bool Context(bool server, int sessionMode, int count, bool connected, bool sameLocal, bool sameManager, int connection, int controllerId)
|
||||
{
|
||||
return server && sessionMode == RplMode.None && count == 1 && connected && sameLocal && sameManager && connection > 0 && controllerId == connection;
|
||||
}
|
||||
}
|
||||
class ERR_LocalAdmission
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected int m_Connection;
|
||||
protected UUID m_Key;
|
||||
protected bool m_Selected;
|
||||
protected bool m_OfflineMode;
|
||||
protected bool m_LocalKey;
|
||||
protected bool m_Failed;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_RefusalLogged;
|
||||
void ERR_LocalAdmission(ERR_GameMode mode) { m_Mode = mode; m_OfflineMode = RplSession.Mode() == RplMode.None; }
|
||||
bool OfflineMode() { return m_OfflineMode; }
|
||||
bool Selected() { return m_Selected; }
|
||||
bool LocalKey() { return m_LocalKey; }
|
||||
protected bool Context(ERR_GameMode mode, int connection, SCR_PlayerController controller)
|
||||
{
|
||||
if (!GetGame()) return false;
|
||||
PlayerManager manager = GetGame().GetPlayerManager();
|
||||
bool ownedMode = mode && mode == m_Mode && mode == ERR_GameMode.Get();
|
||||
int count = -1;
|
||||
bool connected, sameManager;
|
||||
if (manager)
|
||||
{
|
||||
count = manager.GetPlayerCount();
|
||||
connected = manager.IsPlayerConnected(connection);
|
||||
sameManager = controller && manager.GetPlayerController(connection) == controller;
|
||||
}
|
||||
bool sameLocal = controller && controller == GetGame().GetPlayerController();
|
||||
int controllerId;
|
||||
if (controller) controllerId = controller.GetPlayerId();
|
||||
bool valid = ownedMode && manager && controller && ERR_LocalAdmissionRules.Context(Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection, controllerId);
|
||||
if (!valid && !m_RefusalLogged)
|
||||
{
|
||||
string project;
|
||||
if (System.GetCLIParam("errOrdinaryScenario", project) && (project == "core" || project == "chat"))
|
||||
{
|
||||
m_RefusalLogged = true;
|
||||
PrintFormat("ERR ORDINARY LOCAL CONTEXT: server=%1 sessionMode=%2 count=%3 connected=%4 sameLocal=%5 sameManager=%6 positiveMatchingId=%7 ownedMode=%8 controllerPresent=%9", Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection > 0 && controllerId == connection, ownedMode, controller != null);
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
UUID Resolve(ERR_GameMode mode, int connection, SCR_PlayerController controller, UUID nativeIdentity)
|
||||
{
|
||||
// This helper is inert for modes created as Listen/Dedicated, even after Stop.
|
||||
if (!m_OfflineMode) return nativeIdentity;
|
||||
if (m_Failed || m_Stopped) return UUID.NULL_UUID;
|
||||
if (m_Selected)
|
||||
{
|
||||
// A late backend value or name change never grants another local life.
|
||||
if (mode != m_Mode || connection != m_Connection || controller != m_Controller || !Validate()) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
return m_Key;
|
||||
}
|
||||
if (!Context(mode, connection, controller)) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
m_Controller = controller; m_Connection = connection; m_Key = nativeIdentity;
|
||||
m_Selected = true;
|
||||
if (m_Key.IsNull()) { m_Key = UUID.GenV4(); m_LocalKey = true; }
|
||||
if (m_Key.IsNull()) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
if (m_LocalKey) Print("Rereintroduced [admission]: sole offline controller uses a generated local-round roster key; backend identity and permissions unchanged.");
|
||||
return m_Key;
|
||||
}
|
||||
bool Validate()
|
||||
{
|
||||
if (!m_OfflineMode) return true;
|
||||
if (m_Failed || m_Stopped) return false;
|
||||
if (!m_Selected) return true;
|
||||
if (!Context(m_Mode, m_Connection, m_Controller)) m_Failed = true;
|
||||
return !m_Failed;
|
||||
}
|
||||
bool Disconnected(int connection)
|
||||
{
|
||||
if (!m_Selected || m_Stopped || connection != m_Connection) return false;
|
||||
m_Failed = true;
|
||||
return true;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
m_Controller = null;
|
||||
m_Key = UUID.NULL_UUID;
|
||||
// Keep selection/failure latched: this object cannot admit a replacement.
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
void ERR_FailLocalAdmission()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
string reason = "The offline player context changed. Start a new scenario to establish a fresh session.";
|
||||
if (Phase() == ERR_Phase.PREPARING) { FailPreparation(reason); return; }
|
||||
if (!Active()) return;
|
||||
m_EndReason = reason;
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,14 @@ modded class ERR_GameMode
|
||||
protected ref ERR_NativeCasualtyServiceProbe m_ERRCasualtyServiceProbe;
|
||||
protected ref ERR_NativePrisonerProbe m_ERRPrisonerProbe;
|
||||
protected ref ERR_NativeGuardCancelProbe m_ERRGuardCancelProbe;
|
||||
protected ref ERR_NativeLocalAdmissionProbe m_ERRLocalAdmissionProbe;
|
||||
protected ref ERR_NativePoweredFlightProbe m_ERRPoweredFlightProbe;
|
||||
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
|
||||
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
|
||||
protected ref ERR_NativeCasualtyLoadProbe m_ERRCasualtyLoadProbe;
|
||||
protected ref ERR_NativeEnvironmentProbe m_ERRBaseEnvironmentProbe;
|
||||
protected bool m_ERRCatalogChecked;
|
||||
protected bool m_ERRDiscoveryMapChecked;
|
||||
protected ref ERR_NativePatrolProbe m_ERRPatrolEast;
|
||||
protected ref ERR_NativePatrolProbe m_ERRPatrolWest;
|
||||
protected int m_ERRBaseStep;
|
||||
@@ -79,12 +82,15 @@ modded class ERR_GameMode
|
||||
}
|
||||
protected void ERR_StopHealthProbes()
|
||||
{
|
||||
ERR_NativeDiscoveryMapProbe.Stop();
|
||||
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
|
||||
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
|
||||
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
|
||||
if (m_ERRBaseEnvironmentProbe) m_ERRBaseEnvironmentProbe.Stop();
|
||||
if (m_ERRPrisonerProbe) m_ERRPrisonerProbe.Stop();
|
||||
if (m_ERRGuardCancelProbe) m_ERRGuardCancelProbe.Stop();
|
||||
if (m_ERRLocalAdmissionProbe) m_ERRLocalAdmissionProbe.Stop();
|
||||
if (m_ERRPoweredFlightProbe) m_ERRPoweredFlightProbe.Stop();
|
||||
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
|
||||
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
|
||||
if (m_ERRBaseMedicalProbe) m_ERRBaseMedicalProbe.Stop();
|
||||
@@ -156,6 +162,20 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (!ERR_MissionRules.Terminal(current)) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
if (m_ERRExtractionCase == "powered-flight")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1802, "native powered-flight fixture finished before explicit test reset")) return;
|
||||
ERR_StopHealthProbes();
|
||||
Print("ERR SMOKE COMPLETE: native pilot-input powered takeoff, hover, contact landing and shutdown; isolated mechanism fixture reset");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "local-admission")
|
||||
{
|
||||
// Completion is emitted by the calling tick after synchronous teardown and
|
||||
// its post-transition invariants; a terminal event alone is insufficient.
|
||||
ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1700, "local admission fixture received its production fail-closed transition");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "early-helicopter-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 201 && Players().HasSurvivor() && Extraction().State() == ERR_ExtractionState.LOST, "lost helicopter ends prison phase despite a living prisoner")) return;
|
||||
@@ -285,6 +305,25 @@ modded class ERR_GameMode
|
||||
Print("ERR SMOKE COMPLETE: expected startup-failure");
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1800)
|
||||
{
|
||||
int result = m_ERRPoweredFlightProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "native powered-flight feasibility fixture"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1802; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1700)
|
||||
{
|
||||
int result = m_ERRLocalAdmissionProbe.Tick(now);
|
||||
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1)
|
||||
{
|
||||
m_ERRBaseStep = 1702;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
Print("ERR SMOKE COMPLETE: native local admission callback replay and fail-closed method injection; not multiplayer evidence");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1400)
|
||||
{
|
||||
int result = m_ERRCasualtyServiceProbe.Tick(now);
|
||||
@@ -423,6 +462,25 @@ modded class ERR_GameMode
|
||||
if (!ERR_NativeFactionCatalogProbe.Run(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRCatalogChecked = true;
|
||||
}
|
||||
if (!m_ERRDiscoveryMapChecked)
|
||||
{
|
||||
if (!ERR_NativeDiscoveryMapProbe.Run()) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRDiscoveryMapChecked = true;
|
||||
}
|
||||
if (m_ERRExtractionCase == "powered-flight")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPoweredFlightProbe = new ERR_NativePoweredFlightProbe(this);
|
||||
m_ERRBaseDeadline = now + 250000;
|
||||
m_ERRBaseStep = 1800;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "local-admission")
|
||||
{
|
||||
m_ERRLocalAdmissionProbe = new ERR_NativeLocalAdmissionProbe(this);
|
||||
m_ERRBaseStep = 1700;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "casualty-service")
|
||||
{
|
||||
m_ERRCasualtyServiceProbe = new ERR_NativeCasualtyServiceProbe(this);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in hidden native-widget comparison; no discovery, inventory or mission
|
||||
// state is changed, and no map menu or player input is fabricated.
|
||||
// Separate owned subclasses preserve the vanilla base RPC serializer identity.
|
||||
// Their small widget adapters are repeated to compare the unchanged native
|
||||
// setter against the real successor subclass without modding a replicated type.
|
||||
class ERR_NativePlainMapMarker : SCR_MapMarkerBase
|
||||
{
|
||||
bool ERR_ProbeAttachMapWidget()
|
||||
{
|
||||
ERR_ProbeDetachMapWidget();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return false;
|
||||
m_ConfigEntry = manager.GetMarkerConfig().GetMarkerEntryConfigByType(GetType());
|
||||
if (!m_ConfigEntry) return false;
|
||||
m_wRoot = GetGame().GetWorkspace().CreateWidgets(m_ConfigEntry.GetMarkerLayout());
|
||||
if (!m_wRoot) return false;
|
||||
m_wRoot.SetVisible(false);
|
||||
m_MarkerWidgetComp = SCR_MapMarkerWidgetComponent.Cast(m_wRoot.FindHandler(SCR_MapMarkerWidgetComponent));
|
||||
if (!m_MarkerWidgetComp) return false;
|
||||
m_MarkerWidgetComp.SetMarkerObject(this);
|
||||
m_ConfigEntry.InitClientSettings(this, m_MarkerWidgetComp, true);
|
||||
return m_wRoot.FindAnyWidget("MarkerText") != null;
|
||||
}
|
||||
string ERR_ProbeMapText()
|
||||
{
|
||||
if (!m_wRoot) return "";
|
||||
TextWidget text = TextWidget.Cast(m_wRoot.FindAnyWidget("MarkerText"));
|
||||
if (!text) return "";
|
||||
return text.GetText();
|
||||
}
|
||||
void ERR_ProbeDetachMapWidget()
|
||||
{
|
||||
// Remove only this probe object's native subscriptions/widgets.
|
||||
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
|
||||
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
|
||||
if (m_wRoot) m_wRoot.RemoveFromHierarchy();
|
||||
m_wRoot = null;
|
||||
m_MarkerWidgetComp = null;
|
||||
}
|
||||
}
|
||||
class ERR_NativeObjectiveMapMarker : ERR_DiscoveryMarker
|
||||
{
|
||||
bool ERR_ProbeAttachMapWidget()
|
||||
{
|
||||
ERR_ProbeDetachMapWidget();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return false;
|
||||
m_ConfigEntry = manager.GetMarkerConfig().GetMarkerEntryConfigByType(GetType());
|
||||
if (!m_ConfigEntry) return false;
|
||||
m_wRoot = GetGame().GetWorkspace().CreateWidgets(m_ConfigEntry.GetMarkerLayout());
|
||||
if (!m_wRoot) return false;
|
||||
m_wRoot.SetVisible(false);
|
||||
m_MarkerWidgetComp = SCR_MapMarkerWidgetComponent.Cast(m_wRoot.FindHandler(SCR_MapMarkerWidgetComponent));
|
||||
if (!m_MarkerWidgetComp) return false;
|
||||
m_MarkerWidgetComp.SetMarkerObject(this);
|
||||
m_ConfigEntry.InitClientSettings(this, m_MarkerWidgetComp, true);
|
||||
return m_wRoot.FindAnyWidget("MarkerText") != null;
|
||||
}
|
||||
string ERR_ProbeMapText()
|
||||
{
|
||||
if (!m_wRoot) return "";
|
||||
TextWidget text = TextWidget.Cast(m_wRoot.FindAnyWidget("MarkerText"));
|
||||
if (!text) return "";
|
||||
return text.GetText();
|
||||
}
|
||||
void ERR_ProbeDetachMapWidget()
|
||||
{
|
||||
// Remove only this probe object's native subscriptions/widgets.
|
||||
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
|
||||
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
|
||||
if (m_wRoot) m_wRoot.RemoveFromHierarchy();
|
||||
m_wRoot = null;
|
||||
m_MarkerWidgetComp = null;
|
||||
}
|
||||
}
|
||||
class ERR_NativeDiscoveryMapProbe
|
||||
{
|
||||
protected static ref ERR_NativePlainMapMarker s_Plain;
|
||||
protected static ref ERR_NativeObjectiveMapMarker s_Objective;
|
||||
static void Stop()
|
||||
{
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (s_Plain)
|
||||
{
|
||||
s_Plain.ERR_ProbeDetachMapWidget();
|
||||
if (manager) manager.RemoveStaticMarker(s_Plain);
|
||||
}
|
||||
if (s_Objective)
|
||||
{
|
||||
s_Objective.ERR_ProbeDetachMapWidget();
|
||||
if (manager) manager.RemoveStaticMarker(s_Objective);
|
||||
}
|
||||
s_Plain = null;
|
||||
s_Objective = null;
|
||||
}
|
||||
protected static bool Check(bool success, string detail)
|
||||
{
|
||||
if (success) { Print("ERR SMOKE PASS: discovery map " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: discovery map " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
// Invoke once after ordinary fixture admission and native map manager init.
|
||||
static bool Run()
|
||||
{
|
||||
Stop();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!Check(manager && manager.GetMarkerConfig() && GetGame().GetPlayerController(), "native local controller and marker manager available")) return false;
|
||||
int originalCount = manager.GetStaticMarkers().Count();
|
||||
s_Plain = new ERR_NativePlainMapMarker();
|
||||
s_Plain.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
s_Plain.SetMarkerConfigID(0);
|
||||
s_Plain.SetCustomText("Unknown location");
|
||||
manager.InsertStaticMarker(s_Plain, true);
|
||||
if (!Check(s_Plain.ERR_ProbeAttachMapWidget() && s_Plain.ERR_ProbeMapText() == "Unknown location", "native baseline label rendered in actual hidden marker layout")) return false;
|
||||
s_Plain.SetCustomText("Radio station");
|
||||
if (!Check(s_Plain.GetCustomText() == "Radio station" && s_Plain.ERR_ProbeMapText() == "Unknown location", "native data-only setter reproduces stale already-open label")) return false;
|
||||
if (!Check(s_Plain.CanBeRemovedByOwner() && SCR_MapMarkersUI.IsOwnedMarker(s_Plain), "plain local marker exposes native delete/edit/drag eligibility")) return false;
|
||||
|
||||
s_Objective = new ERR_NativeObjectiveMapMarker();
|
||||
s_Objective.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
s_Objective.SetMarkerConfigID(0);
|
||||
s_Objective.SetCustomText("Unknown location");
|
||||
manager.InsertStaticMarker(s_Objective, true);
|
||||
if (!Check(s_Objective.GetMarkerOwnerID() == -1 && !SCR_MapMarkersUI.IsOwnedMarker(s_Objective) && !s_Objective.CanBeRemovedByOwner(), "local insertion retains objective identity and disables native edit/drag/delete eligibility")) return false;
|
||||
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Unknown location", "objective first label rendered through actual native layout")) return false;
|
||||
Widget originalWidget = s_Objective.GetRootWidget();
|
||||
s_Objective.SetCustomText("Radio station");
|
||||
if (!Check(s_Objective.GetRootWidget() == originalWidget && s_Objective.ERR_ProbeMapText() == "Radio station", "identification refreshes existing native widget without recreating marker")) return false;
|
||||
if (!Check(manager.GetStaticMarkers().Count() == originalCount + 2, "label change creates no additional marker")) return false;
|
||||
s_Objective.ERR_ProbeDetachMapWidget();
|
||||
s_Objective.SetCustomText("Initial helicopter pickup site");
|
||||
if (!Check(!s_Objective.GetRootWidget() && s_Objective.GetCustomText() == "Initial helicopter pickup site", "closed widget accepts data-only update safely")) return false;
|
||||
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Initial helicopter pickup site", "new native widget renders latest saved label")) return false;
|
||||
Stop();
|
||||
return Check(manager.GetStaticMarkers().Count() == originalCount, "owned probe widgets and markers removed; mission markers preserved");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,129 @@
|
||||
#ifdef WORKBENCH
|
||||
[BaseContainerProps(category: "Respawn")]
|
||||
modded class ERR_SpawnLogic
|
||||
{
|
||||
ERR_LocalAdmission ERR_LocalProbeHelper()
|
||||
{
|
||||
if (!ERR_NativeLocalAdmissionProbe.Enabled()) return null;
|
||||
return m_LocalAdmission;
|
||||
}
|
||||
bool ERR_LocalProbeIdle(int connection)
|
||||
{
|
||||
return ERR_NativeLocalAdmissionProbe.Enabled() && !m_Attempts.Contains(connection) && !m_Retries.Contains(connection) && !m_Pending.Contains(connection) && !m_AuthorizedPositions.Contains(connection) && m_SpawnTickets.IsEmpty();
|
||||
}
|
||||
bool ERR_LocalProbeClosed()
|
||||
{
|
||||
return ERR_NativeLocalAdmissionProbe.Enabled() && m_AdmissionsStopped && m_LocalAdmissionFailed;
|
||||
}
|
||||
bool ERR_LocalProbeRefuseController(int connection)
|
||||
{
|
||||
if (!ERR_NativeLocalAdmissionProbe.Enabled() || !m_LocalAdmission || !m_LocalAdmission.OfflineMode() || !m_LocalAdmission.Selected() || !m_Identities.Contains(connection)) return false;
|
||||
// Deliberately invalid METHOD ARGUMENT, not a fabricated native disconnect or
|
||||
// replacement controller. The real controller and backend remain untouched.
|
||||
UUID rejected = m_LocalAdmission.Resolve(ERR_GameMode.Get(), connection, null, m_Identities.Get(connection));
|
||||
if (!rejected.IsNull()) return false;
|
||||
return !CheckLocalAdmissionContext();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeLocalAdmissionProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected IEntity m_Actor;
|
||||
protected ERR_SpawnLogic m_Logic;
|
||||
protected ERR_LocalAdmission m_Helper;
|
||||
protected ERR_PlayerLife m_Life;
|
||||
protected UUID m_Key;
|
||||
protected int m_Connection;
|
||||
protected int m_Participants;
|
||||
protected int m_Changes;
|
||||
protected int m_Step;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
protected bool m_Listening;
|
||||
protected bool m_Stopped;
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "local-admission";
|
||||
}
|
||||
void ERR_NativeLocalAdmissionProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: local admission " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: local admission " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected void Changed(int connection) { m_Changes++; }
|
||||
protected int Participants()
|
||||
{
|
||||
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives); return lives.Count();
|
||||
}
|
||||
protected bool Unchanged()
|
||||
{
|
||||
return m_Controller && m_Actor && m_Life && m_Mode.Players().Get(m_Connection) == m_Life && m_Mode.Players().CurrentActor(m_Actor) == m_Life && m_Life.AccountId == m_Key && m_Life.Connected && m_Life.UsedLife && m_Life.Actor == m_Actor && m_Controller.GetControlledEntity() == m_Actor && GetGame().GetPlayerManager().GetPlayerController(m_Connection) == m_Controller && Participants() == m_Participants && !m_Mode.Players().CanEnter(m_Connection, ERR_Phase.PRISON) && m_Logic.ERR_LocalProbeIdle(m_Connection);
|
||||
}
|
||||
protected bool DisposableHelpers()
|
||||
{
|
||||
ERR_LocalAdmission disconnected = new ERR_LocalAdmission(m_Mode);
|
||||
if (!Check(disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key) == m_Key && disconnected.Validate() && !disconnected.LocalKey(), "disposable helper accepts actual sole native controller using existing round key")) return false;
|
||||
// Opaque alternative input only: no backend read/write, roster bind or login.
|
||||
UUID alternative = "00e77500-0000-0000-0000-000000000001";
|
||||
if (!Check(disconnected.Resolve(m_Mode, m_Connection, m_Controller, alternative) == m_Key && disconnected.Resolve(m_Mode, m_Connection, m_Controller, UUID.NULL_UUID) == m_Key, "late opaque identity input and null input retain the selected key")) return false;
|
||||
if (!Check(!disconnected.Disconnected(m_Connection + 1) && disconnected.Validate(), "unrelated disconnect argument does not close helper")) return false;
|
||||
if (!Check(disconnected.Disconnected(m_Connection) && !disconnected.Validate() && disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull(), "matching disconnect argument latches disposable helper closed")) return false;
|
||||
disconnected.Stop();
|
||||
if (!Check(!disconnected.Validate() && disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull(), "stopped disposable helper cannot reopen with the genuine controller")) return false;
|
||||
ERR_LocalAdmission invalid = new ERR_LocalAdmission(m_Mode);
|
||||
if (!Check(invalid.Resolve(m_Mode, m_Connection, null, m_Key).IsNull() && invalid.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull() && !invalid.Selected(), "invalid controller argument before selection fails closed without selecting another life")) return false;
|
||||
invalid.Stop();
|
||||
Print("ERR LOCAL ADMISSION LIMIT: disposable method-argument fault injection; no native disconnect or replacement occurred; Listen/Dedicated pass-through remains source-policy coverage, not multiplayer evidence");
|
||||
return true;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Listening && m_Mode) m_Mode.Players().OnChanged().Remove(Changed);
|
||||
m_Listening = false;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || now > m_Deadline) { Check(false, "explicit enabled fixture completes within fifteen seconds"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) m_Logic = ERR_SpawnLogic.Cast(respawn.GetSpawnLogic());
|
||||
if (!Check(RplSession.Mode() == RplMode.None && m_Controller && m_Logic && GetGame().GetPlayerManager().GetPlayerCount() == 1, "actual standalone session has one native local controller and successor spawn logic")) return -1;
|
||||
m_Connection = m_Controller.GetPlayerId(); m_Actor = m_Controller.GetControlledEntity();
|
||||
m_Life = m_Mode.Players().Get(m_Connection); m_Helper = m_Logic.ERR_LocalProbeHelper();
|
||||
if (!Check(m_Actor && m_Life && m_Helper && m_Helper.OfflineMode() && m_Helper.Selected() && m_Helper.Validate(), "real native admission already created the local controlled prisoner and selected helper")) return -1;
|
||||
m_Key = m_Life.AccountId; m_Participants = Participants();
|
||||
if (!Check(!m_Key.IsNull() && Unchanged(), "existing prisoner has one spent life and no pending spawn metadata")) return -1;
|
||||
if (!DisposableHelpers()) return -1;
|
||||
m_Mode.Players().OnChanged().Insert(Changed); m_Listening = true;
|
||||
Print("ERR LOCAL ADMISSION REPLAY: invoking existing callback twice for already admitted real controller; this does not represent new native authentication/audits");
|
||||
m_Logic.OnPlayerAuditSuccess_S(m_Connection);
|
||||
m_Logic.OnPlayerAuditSuccess_S(m_Connection);
|
||||
if (!Check(Unchanged() && m_Changes == 0, "replayed callbacks preserve exact roster object, used life, key, actor and no roster-change event")) return -1;
|
||||
m_Step = 1; m_Wait = now + 1500; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Check(Unchanged() && m_Changes == 0 && m_Helper.Validate(), "native update interval after replay creates no replacement actor, life or pending request")) return -1;
|
||||
if (!Check(m_Logic.ERR_LocalProbeRefuseController(m_Connection), "live helper rejects invalid controller argument through production context guard")) return -1;
|
||||
// The guard has synchronously entered the real terminal transition and
|
||||
// stopped services. No second callback, login, UUID or actor is introduced.
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.DEFEAT && m_Mode.EndReason().Contains("offline player context changed") && m_Logic.ERR_LocalProbeClosed() && !m_Helper.Validate() && Unchanged(), "production failure closes admission and ends round while preserving original used life and controlled actor")) return -1;
|
||||
Stop(); m_Step = 2; return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,321 @@
|
||||
#ifdef WORKBENCH
|
||||
// New native-input feasibility experiment. No original Escapists flight code.
|
||||
// Only the exact fixture-owned AI pilot and helicopter receive these hooks.
|
||||
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.PreparePilot(this, owner, dt);
|
||||
}
|
||||
}
|
||||
modded class SCR_HelicopterControllerComponent
|
||||
{
|
||||
protected override event void OnPrepareControls()
|
||||
{
|
||||
super.OnPrepareControls();
|
||||
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
|
||||
if (probe) probe.PrepareHelicopter(this);
|
||||
}
|
||||
}
|
||||
class ERR_PoweredFlightRules
|
||||
{
|
||||
static bool Authority(bool server, bool enabled, bool stopped, bool ownedPilot, bool ownedSeat, bool playerPresent, bool alive, bool fueled)
|
||||
{
|
||||
return server && enabled && !stopped && ownedPilot && ownedSeat && !playerPresent && alive && fueled;
|
||||
}
|
||||
static float ClampInput(float value, float limit = 1)
|
||||
{
|
||||
return Math.Clamp(value, -limit, limit);
|
||||
}
|
||||
static float DesiredVertical(float height, bool landing)
|
||||
{
|
||||
float target = 8;
|
||||
if (landing) target = 0;
|
||||
return Math.Clamp((target - height) * 0.6, -0.8, 1.5);
|
||||
}
|
||||
static bool Hover(float height, float speed, float vertical, float upright, float drift)
|
||||
{
|
||||
return height >= 7 && height <= 9 && speed < 1.5 && Math.AbsFloat(vertical) < 0.5 && upright > 0.98 && drift < 8;
|
||||
}
|
||||
static bool Landed(bool contact, float height, float speed, float angular, float upright)
|
||||
{
|
||||
return contact && height < 1 && height > -1 && speed < 0.5 && angular < 0.1 && upright > 0.98;
|
||||
}
|
||||
}
|
||||
class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
protected static ERR_NativePoweredFlightProbe s_Current;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected IEntity m_Pilot;
|
||||
protected HelicopterControllerComponent m_Controller;
|
||||
protected CharacterControllerComponent m_Character;
|
||||
protected CharacterInputContext m_Input;
|
||||
protected VehicleHelicopterSimulation m_Simulation;
|
||||
protected FuelManagerComponent m_Fuel;
|
||||
protected PilotCompartmentSlot m_Seat;
|
||||
protected CollectiveMode m_OriginalMode;
|
||||
protected vector m_Home;
|
||||
protected float m_Heading;
|
||||
protected float m_Deadline;
|
||||
protected float m_StageDeadline;
|
||||
protected float m_Stable = -1;
|
||||
protected float m_PreviousTick = -1;
|
||||
protected float m_NextLog;
|
||||
protected float m_InitialFuel;
|
||||
protected float m_Collective = -1;
|
||||
protected float m_LastControl;
|
||||
protected int m_Stage;
|
||||
protected int m_Wakes;
|
||||
protected int m_StartRequests;
|
||||
protected int m_ControlUpdates;
|
||||
protected int m_HelicopterUpdates;
|
||||
protected bool m_Bound;
|
||||
protected bool m_ModeSaved;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
protected bool m_ReadbackOK = true;
|
||||
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
|
||||
static const ResourceName GROUP = "{8DE0C0830FE0C33D}Prefabs/Groups/OPFOR/Group_USSR_Base.et";
|
||||
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "powered-flight";
|
||||
}
|
||||
static ERR_NativePoweredFlightProbe Current() { return s_Current; }
|
||||
void ERR_NativePoweredFlightProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 240000;
|
||||
if (Enabled() && !s_Current) s_Current = this;
|
||||
else m_Stopped = true;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: powered-flight " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: powered-flight " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool PlayerPresent()
|
||||
{
|
||||
if (!m_Aircraft || !m_Group || m_Group.GetPlayerCount(true) > 0) return true;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (actor && (actor == m_Pilot || CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool OwnPilot()
|
||||
{
|
||||
return m_Pilot && m_Group && ERR_Actors.Group(m_Pilot) == m_Group && !ERR_Actors.Player(m_Pilot);
|
||||
}
|
||||
protected bool PoweredAuthority()
|
||||
{
|
||||
bool occupied = m_Seat && m_Seat.GetOccupant() == m_Pilot && m_Controller && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
bool alive = ERR_Actors.Alive(m_Aircraft) && ERR_Actors.CanFight(m_Pilot);
|
||||
SCR_VehicleDamageManagerComponent damage;
|
||||
if (m_Aircraft) damage = SCR_VehicleDamageManagerComponent.Cast(m_Aircraft.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
alive = alive && damage && damage.GetEngineFunctional() && damage.GetMovementDamage() < 1 && !damage.IsOnFire();
|
||||
if (m_Simulation)
|
||||
for (int rotor = 0; rotor < m_Simulation.RotorCount(); rotor++) if (m_Simulation.RotorGetState(rotor) == RotorState.DESTROYED) alive = false;
|
||||
return ERR_PoweredFlightRules.Authority(Replication.IsServer(), Enabled(), m_Stopped, OwnPilot(), occupied, PlayerPresent(), alive, m_Fuel && m_Fuel.HasFuel());
|
||||
}
|
||||
void PrepareHelicopter(HelicopterControllerComponent controller)
|
||||
{
|
||||
if (!m_Bound || controller != m_Controller || !PoweredAuthority()) return;
|
||||
// Native SCR controller normally reapplies device preferences every update.
|
||||
// Override only this owned fixture controller, never global/user settings.
|
||||
controller.SetCollectiveMode(CollectiveMode.Direct); m_HelicopterUpdates++;
|
||||
}
|
||||
void PreparePilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
{
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority() || !m_Input || !m_Simulation || dt <= 0 || dt > 0.2) return;
|
||||
vector transform[4]; m_Simulation.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;
|
||||
if (m_Stage >= 4 && m_Stage <= 6 && m_Controller.IsEngineOn())
|
||||
{
|
||||
float desiredVertical = ERR_PoweredFlightRules.DesiredVertical(height, m_Stage == 6);
|
||||
float error = desiredVertical - velocity[1];
|
||||
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;
|
||||
// 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];
|
||||
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;
|
||||
}
|
||||
m_Input.SetVehicleThrottle(1);
|
||||
m_Input.SetVehicleCollective(collective);
|
||||
m_Input.SetVehicleCyclicForward(forward);
|
||||
m_Input.SetVehicleCyclicAside(aside);
|
||||
m_Input.SetVehicleAntiTorque(torque);
|
||||
m_ReadbackOK = m_ReadbackOK && Math.AbsFloat(m_Input.GetVehicleCollective() - collective) < 0.001 && Math.AbsFloat(m_Input.GetVehicleCyclicForward() - forward) < 0.001 && Math.AbsFloat(m_Input.GetVehicleCyclicAside() - aside) < 0.001 && Math.AbsFloat(m_Input.GetVehicleAntiTorque() - torque) < 0.001;
|
||||
m_ControlUpdates++; m_LastControl = GetGame().GetWorld().GetWorldTime();
|
||||
}
|
||||
protected bool FindPad(out vector result)
|
||||
{
|
||||
vector anchor = m_Mode.ExtractionVehicle().GetOrigin();
|
||||
for (int index = 0; index < 12; index++)
|
||||
{
|
||||
float angle = index * Math.PI / 6;
|
||||
vector candidate = ERR_Actors.Ground(anchor + Vector(Math.Cos(angle) * 120, 0, Math.Sin(angle) * 120));
|
||||
if (candidate[1] < 2) continue;
|
||||
bool flat = true;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector point = candidate + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
if (Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]) > 0.8) flat = false;
|
||||
}
|
||||
if (!flat || !SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 15, 4, TraceFlags.ENTS)) continue;
|
||||
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 35 0";
|
||||
clearance.Mins = "-15 -1 -15"; clearance.Maxs = "15 1 15"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(clearance, null) < 1) continue;
|
||||
result = candidate; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Diagnostic(float now)
|
||||
{
|
||||
if (!m_Simulation || !m_Aircraft || now < m_NextLog) return;
|
||||
m_NextLog = now + 2000;
|
||||
vector transform[4]; m_Simulation.GetWorldTransform(transform);
|
||||
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());
|
||||
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));
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
// Input cleanup is confined to the same unclaimed test pilot/aircraft.
|
||||
bool ours = m_Bound && OwnPilot() && !PlayerPresent();
|
||||
m_Stopped = true; m_Bound = false;
|
||||
if (s_Current == this) s_Current = null;
|
||||
if (ours && 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 (m_Group && m_Group.GetPlayerCount(true) == 0) m_Group.SetPermanentLOD(-1);
|
||||
// Keep real airborne/failed test actors for diagnosis and mode-owned cleanup.
|
||||
// Stop never repairs, stabilizes, teleports, refuels or replaces them.
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
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_PreviousTick = now;
|
||||
if (m_Stage == 0)
|
||||
{
|
||||
if (!m_Mode.ExtractionVehicle()) return 0;
|
||||
vector pad;
|
||||
if (!Check(FindPad(pad), "separate clear flat test pad found; extraction vehicle untouched")) return -1;
|
||||
m_Aircraft = Vehicle.Cast(ERR_Actors.Spawn(AIRCRAFT, pad));
|
||||
m_Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(GROUP, pad + "20 0 0"));
|
||||
m_Mode.Own(m_Aircraft); m_Mode.Own(m_Group);
|
||||
if (!Check(m_Aircraft && m_Group, "one stock unarmed airframe and own empty group created once")) return -1;
|
||||
m_Group.SetPermanentLOD(0); m_Group.ActivateAI();
|
||||
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
if (m_Controller) { m_Simulation = m_Controller.GetSimulation(); m_Seat = m_Controller.GetPilotCompartmentSlot(); }
|
||||
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
if (!Check(m_Simulation && m_Seat && !m_Seat.GetOccupant() && m_Fuel && m_Fuel.HasFuel(), "native flight simulation, actual active pilot seat and finite fuel present")) return -1;
|
||||
AIGroup nativeGroup = m_Group;
|
||||
m_Pilot = m_Seat.SpawnCharacterInCompartment(PILOT, nativeGroup, GROUP);
|
||||
if (!Check(m_Pilot && nativeGroup == m_Group, "one existing vanilla character spawned through native pilot-seat mechanism")) return -1;
|
||||
m_Mode.Own(m_Pilot);
|
||||
m_Stage = 1; m_StageDeadline = now + 30000; return 0;
|
||||
}
|
||||
if (m_Stage == 1)
|
||||
{
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
if (physics && physics.IsDynamic() && m_Simulation.IsValid() && m_Wakes == 0) { m_Wakes = 1; physics.SetActive(ActiveState.ACTIVE); m_Stable = -1; return 0; }
|
||||
if (now >= m_StageDeadline) { Check(false, "initial native seating and ground settling complete in 30 seconds"); return -1; }
|
||||
if (!m_Wakes || !OwnPilot() || m_Seat.GetOccupant() != m_Pilot || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 1000) return 0;
|
||||
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_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 (m_Stage == 2)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "native AI input and helicopter preparation hooks execute before engine start"); return -1; }
|
||||
if (m_ControlUpdates < 3 || m_HelicopterUpdates < 3) return 0;
|
||||
if (!Check(m_Controller.GetCollectiveMode() == CollectiveMode.Direct && m_Input.GetVehicleCollective() == -1, "owned native input hooks apply minimum collective in Direct mode")) return -1;
|
||||
m_StartRequests = 1;
|
||||
bool started = m_Controller.StartEngine();
|
||||
PrintFormat("ERR POWERED ENGINE REQUEST: StartEngine returned=%1; normal procedure, no force-start or simulation EngineStart", started);
|
||||
m_Stage = 3; m_StageDeadline = now + 65000; return 0;
|
||||
}
|
||||
if (now - m_LastControl > 1000) { Check(false, "native control callback remains live during test flight"); return -1; }
|
||||
vector transform[4]; m_Simulation.GetWorldTransform(transform);
|
||||
float height = transform[3][1] - m_Home[1];
|
||||
float speed = m_Simulation.GetVelocity().Length();
|
||||
float drift = vector.DistanceXZ(transform[3], m_Home);
|
||||
if (height > 18 || height < -3 || drift > 22 || transform[1][1] < 0.85) { Check(false, "physical test stays inside height, drift and tilt safety envelope"); return -1; }
|
||||
if (m_Stage == 3)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "normal engine startup and native rotor RPM complete within 65 seconds"); return -1; }
|
||||
float targetRPM = m_Simulation.RotorGetRPMTarget(0);
|
||||
if (!m_Controller.IsEngineOn() || targetRPM <= 0 || m_Simulation.RotorGetRPM(0) < targetRPM * 0.9 || m_Fuel.GetTotalFuel() >= m_InitialFuel) return 0;
|
||||
if (!Check(m_StartRequests == 1 && m_Fuel.GetTotalFuel() < m_InitialFuel, "single normal engine start spins physical rotor and consumes native fuel")) return -1;
|
||||
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
|
||||
}
|
||||
if (m_Stage >= 4 && m_Stage <= 6 && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
|
||||
if (m_Stage == 4 || m_Stage == 5)
|
||||
{
|
||||
bool hover = !m_Simulation.HasAnyGroundContact() && ERR_PoweredFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1], drift);
|
||||
if (!hover) m_Stable = -1;
|
||||
if (hover && m_Stable < 0) m_Stable = now;
|
||||
if (now >= m_StageDeadline) { Check(false, "actual powered takeoff and five-second stable hover reached in 65 seconds"); return -1; }
|
||||
if (m_Stable >= 0 && now - m_Stable >= 5000)
|
||||
{
|
||||
if (!Check(m_ControlUpdates > 100 && m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05, "actual 7–9 metre powered hover stable five seconds with native fuel consumption")) return -1;
|
||||
m_Stage = 6; m_StageDeadline = now + 65000; m_Stable = -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (m_Stage == 6)
|
||||
{
|
||||
bool landed = ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
|
||||
if (!landed) m_Stable = -1;
|
||||
if (landed && m_Stable < 0) m_Stable = now;
|
||||
if (now >= m_StageDeadline) { Check(false, "actual native contact landing reached within 65 seconds"); return -1; }
|
||||
if (m_Stable < 0 || now - m_Stable < 2000) return 0;
|
||||
if (!Check(drift < 8 && m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05 && m_Wakes == 1 && m_StartRequests == 1, "same physical aircraft and living AI pilot land with stable ground contact after powered hover")) return -1;
|
||||
m_Controller.StopEngine(false);
|
||||
m_Stage = 7; m_StageDeadline = now + 15000; return 0;
|
||||
}
|
||||
if (m_Stage == 7)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "normal engine shutdown completed on actual ground"); return -1; }
|
||||
if (m_Controller.IsEngineOn()) return 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;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -67,7 +67,7 @@ modded class SCR_BaseItemSupportStationComponent
|
||||
ownerName = owner.GetName();
|
||||
if (owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
|
||||
}
|
||||
SCR_EntityCatalogManagerComponent catalog = SCR_EntityCatalogManagerComponent.GetInstance();
|
||||
bool globalCatalogPresent = SCR_EntityCatalogManagerComponent.GetInstance() != null;
|
||||
SCR_BaseGameMode baseMode;
|
||||
if (GetGame()) baseMode = SCR_BaseGameMode.Cast(GetGame().GetGameMode());
|
||||
int gameState = -1;
|
||||
@@ -80,7 +80,7 @@ modded class SCR_BaseItemSupportStationComponent
|
||||
ERR_GameMode mode = ERR_GameMode.Cast(baseMode);
|
||||
if (mode) phase = mode.Phase();
|
||||
}
|
||||
string detail = string.Format("ERR STATION TEARDOWN DIAG: n=%1 owner=%2 prefab=%3 cachedCatalog=%4 globalCatalog=%5", s_ERRStationTeardownLines, ownerName, prefab, m_EntityCatalogManager != null, catalog != null);
|
||||
string detail = string.Format("ERR STATION TEARDOWN DIAG: n=%1 owner=%2 prefab=%3 cachedCatalog=%4 globalCatalog=%5", s_ERRStationTeardownLines, ownerName, prefab, m_EntityCatalogManager != null, globalCatalogPresent);
|
||||
detail += string.Format(" modePresent=%1 gameState=%2 errPhase=%3 endData=%4 call=OnDelete/InitValidSetup", baseMode != null, gameState, phase, hasEndData);
|
||||
detail += string.Format(" initSeen=%1 initMode=%2 initMainWorld=%3 initSiteOwned=%4 initInInventory=%5 initParentPrefab=%6", m_ERRStationInitSeen, m_ERRStationInitModePresent, m_ERRStationInitMainWorld, m_ERRStationInitSiteOwned, m_ERRStationInitInInventory, m_ERRStationInitParentPrefab);
|
||||
bool deleteMainWorld;
|
||||
|
||||
@@ -111,4 +111,20 @@ modded class ArmaReforgerScripted
|
||||
super.OnBeforeWorldCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Observe normal finalized admission; this never creates a body or identity.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
override void PlayerSpawned(int id, IEntity actor)
|
||||
{
|
||||
super.PlayerSpawned(id, actor);
|
||||
string project;
|
||||
if (!System.GetCLIParam("errOrdinaryScenario", project) || (project != "core" && project != "chat")) return;
|
||||
PlayerController local = GetGame().GetPlayerController();
|
||||
if (!local || local.GetPlayerId() != id || local.GetControlledEntity() != actor || !ERR_Actors.Alive(actor)) return;
|
||||
if (!Players().CurrentActor(actor) || Phase() != ERR_Phase.PRISON) return;
|
||||
PrintFormat("ERR ORDINARY: local participant verified project=%1 phase=prison", project);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,6 +10,8 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
protected ref array<ref ERR_SpawnTicket> m_SpawnTickets = {};
|
||||
protected ERR_GameMode m_LifecycleMode;
|
||||
protected bool m_AdmissionsStopped;
|
||||
protected ref ERR_LocalAdmission m_LocalAdmission;
|
||||
protected bool m_LocalAdmissionFailed;
|
||||
|
||||
protected void BindLifecycle(ERR_GameMode mode)
|
||||
{
|
||||
@@ -18,11 +20,17 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
m_LifecycleMode.OnPhaseChanged().Remove(AdmissionPhaseChanged);
|
||||
m_LifecycleMode.OnStop().Remove(StopAdmissions);
|
||||
m_LifecycleMode.OnTick().Remove(CheckLocalAdmissionTick);
|
||||
}
|
||||
if (m_LocalAdmission && m_LocalAdmission.OfflineMode()) { StopAdmissions(); m_Identities.Clear(); }
|
||||
if (m_LocalAdmission) m_LocalAdmission.Stop();
|
||||
m_LocalAdmission = new ERR_LocalAdmission(mode);
|
||||
m_LocalAdmissionFailed = false;
|
||||
m_AdmissionsStopped = false;
|
||||
m_LifecycleMode = mode;
|
||||
mode.OnPhaseChanged().Insert(AdmissionPhaseChanged);
|
||||
mode.OnStop().Insert(StopAdmissions);
|
||||
mode.OnTick().Insert(CheckLocalAdmissionTick);
|
||||
}
|
||||
|
||||
override void OnPlayerAuditSuccess_S(int playerId)
|
||||
@@ -40,20 +48,47 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
BindLifecycle(mode);
|
||||
SCR_PlayerController resultController = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (resultController) resultController.ERR_ResetEscapeResult();
|
||||
if (resultController && (!m_LocalAdmission || !m_LocalAdmission.OfflineMode())) resultController.ERR_ResetEscapeResult();
|
||||
#ifndef WORKBENCH
|
||||
string diagnosticProject;
|
||||
if (System.GetCLIParam("errOrdinaryScenario", diagnosticProject))
|
||||
if (System.GetCLIParam("errOrdinaryScenario", diagnosticProject) && (diagnosticProject == "core" || diagnosticProject == "chat"))
|
||||
{
|
||||
PrintFormat("ERR ORDINARY ADMISSION: server=%1 sessionMode=%2 modePresent=%3 controllerPresent=%4 identityPresent=%5 nameLength=%6", Replication.IsServer(), RplSession.Mode(), mode != null, resultController != null, !identity.IsNull(), GetGame().GetPlayerManager().GetPlayerName(playerId).Length());
|
||||
// Read cached native identity metadata only. Never print identity/name bytes,
|
||||
// override cached backend identity or change its native utility.
|
||||
string rawIdentity;
|
||||
int rawLength = -1;
|
||||
BackendApi backend = GetGame().GetBackendApi();
|
||||
if (backend)
|
||||
{
|
||||
rawIdentity = backend.GetPlayerIdentityId(playerId);
|
||||
rawLength = rawIdentity.Length();
|
||||
}
|
||||
UUID rawUUID = rawIdentity;
|
||||
PrintFormat("ERR ORDINARY IDENTITY: rawLength=%1 rawEmpty=%2 rawNull=%3 rawFormatValid=%4 rawNullSentinel=%5 helperLength=%6 helperFormatValid=%7 dedicated=%8", rawLength, rawIdentity.IsEmpty(), rawUUID.IsNull(), UUID.IsUUID(rawIdentity), rawIdentity == UUID.NULL_UUID, identity.Length(), UUID.IsUUID(identity), RplSession.Mode() == RplMode.Dedicated);
|
||||
}
|
||||
#endif
|
||||
if (m_LocalAdmission) identity = m_LocalAdmission.Resolve(mode, playerId, resultController, identity);
|
||||
if (!CheckLocalAdmissionContext()) return;
|
||||
// Repeated real callbacks retain the existing attempt/body/life and result.
|
||||
if (m_LocalAdmission && m_LocalAdmission.Selected() && m_Identities.Contains(playerId))
|
||||
{
|
||||
if (m_Identities.Get(playerId) != identity) { m_LocalAdmission.Disconnected(playerId); CheckLocalAdmissionContext(); return; }
|
||||
if (resultController) resultController.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
return;
|
||||
}
|
||||
if (!mode || !mode.Players().Bind(playerId, identity))
|
||||
{
|
||||
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
if (resultController) resultController.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
if (resultController)
|
||||
{
|
||||
if (m_LocalAdmission && m_LocalAdmission.OfflineMode()) resultController.ERR_ResetEscapeResult();
|
||||
resultController.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
}
|
||||
m_Identities.Set(playerId, identity);
|
||||
if (ResolveReconnection(playerId))
|
||||
if ((!m_LocalAdmission || !m_LocalAdmission.LocalKey()) && ResolveReconnection(playerId))
|
||||
{
|
||||
mode.PlayerSpawned(playerId, GetGame().GetPlayerManager().GetPlayerControlledEntity(playerId));
|
||||
SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId)).ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
@@ -68,6 +103,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (!controller || !mode || m_AdmissionsStopped || !m_Identities.Contains(playerId) || !m_Attempts.Contains(playerId) || m_Pending.Contains(playerId)) return;
|
||||
if (!CheckLocalAdmissionContext()) return;
|
||||
if (controller.GetControlledEntity()) return;
|
||||
if (ERR_MissionRules.Terminal(mode.Phase())) return;
|
||||
if (!mode.PreparationError().IsEmpty()) { Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE); return; }
|
||||
@@ -150,6 +186,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || m_AdmissionsStopped || !mode || !ticket || ticket.Mode != mode || !ticket.Actor || !ticket.Request || !ticket.Controller) return false;
|
||||
if (!CheckLocalAdmissionContext()) return false;
|
||||
ERR_PlayerLife life = mode.Players().Get(ticket.ConnectionId);
|
||||
if (!life || !m_Identities.Contains(ticket.ConnectionId)) return false;
|
||||
PlayerController controller = GetGame().GetPlayerManager().GetPlayerController(ticket.ConnectionId);
|
||||
@@ -197,10 +234,21 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
CancelAdmission(playerId, reason);
|
||||
}
|
||||
}
|
||||
protected bool CheckLocalAdmissionContext()
|
||||
{
|
||||
if (!m_LocalAdmission || m_LocalAdmission.Validate()) return true;
|
||||
if (m_LocalAdmissionFailed || m_AdmissionsStopped) return false;
|
||||
m_LocalAdmissionFailed = true;
|
||||
StopAdmissions();
|
||||
if (m_LifecycleMode) m_LifecycleMode.ERR_FailLocalAdmission();
|
||||
return false;
|
||||
}
|
||||
protected void CheckLocalAdmissionTick(float now) { CheckLocalAdmissionContext(); }
|
||||
protected void StopAdmissions()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_AdmissionsStopped = true;
|
||||
if (m_LocalAdmission) m_LocalAdmission.Stop();
|
||||
array<int> pending = {};
|
||||
foreach (int id, vector position : m_AuthorizedPositions) pending.Insert(id);
|
||||
foreach (ERR_SpawnTicket ticket : m_SpawnTickets)
|
||||
@@ -277,6 +325,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
}
|
||||
override void OnPlayerDisconnected_S(int playerId, KickCauseCode cause, int timeout)
|
||||
{
|
||||
if (m_LocalAdmission && m_LocalAdmission.Disconnected(playerId)) CheckLocalAdmissionContext();
|
||||
CancelAdmission(playerId, ERR_ObserverReason.NONE);
|
||||
m_Identities.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
@@ -287,6 +336,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || m_AdmissionsStopped || !mode || !data || !m_Identities.Contains(playerId) || !mode.Players().CanEnter(playerId, mode.Phase())) return false;
|
||||
if (!CheckLocalAdmissionContext()) return false;
|
||||
ERR_PlayerLife life = mode.Players().Get(playerId);
|
||||
if (!life || life.AccountId != m_Identities.Get(playerId)) return false;
|
||||
if (!m_Pending.Contains(playerId) || !m_AuthorizedPositions.Contains(playerId) || data.GetPrefab() != mode.Factions().PrisonerPrefab) return false;
|
||||
@@ -300,6 +350,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
m_LifecycleMode.OnPhaseChanged().Remove(AdmissionPhaseChanged);
|
||||
m_LifecycleMode.OnStop().Remove(StopAdmissions);
|
||||
m_LifecycleMode.OnTick().Remove(CheckLocalAdmissionTick);
|
||||
}
|
||||
GetGame().GetCallqueue().Remove(RetrySpawn);
|
||||
GetGame().GetCallqueue().Remove(ObserveAfterLoss);
|
||||
|
||||
Reference in New Issue
Block a user