Advance standalone admission, finite artillery and native validation

This commit is contained in:
2026-09-19 09:02:31 +02:00
parent af5146a317
commit 40a474934c
532 changed files with 56456 additions and 199 deletions
+87 -73
View File
@@ -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;