Integrate finite physical mortar assets and archive inventory regression
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
SCR_AIGroup : "{8DE0C0830FE0C33D}Prefabs/Groups/OPFOR/Group_USSR_Base.et" {
|
||||
m_aUnitPrefabSlots {
|
||||
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
|
||||
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
|
||||
"{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et"
|
||||
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,11 @@ class ERR_HQUnit
|
||||
ref ERR_HQReport PendingOrder;
|
||||
ref ERR_HQReport Assignment;
|
||||
AIWaypoint Waypoint;
|
||||
bool ReservedForSupport;
|
||||
|
||||
bool Eligible(ERR_GameMode mode)
|
||||
{
|
||||
if (!Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
|
||||
if (ReservedForSupport || !Group || !Utility || Group.GetPlayerCount(true) > 0 || Group.IsSlave() || Group.GetFaction() != mode.EnemyFaction()) return false;
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
bool survivor;
|
||||
@@ -44,6 +45,7 @@ class ERR_HQUnit
|
||||
class ERR_HQ : ERR_CommandService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_MortarSystem m_Mortars;
|
||||
protected ref array<ref ERR_HQUnit> m_Units = {};
|
||||
protected ref array<ref ERR_HQReport> m_Contacts = {};
|
||||
protected ref ERR_HQNetwork m_Network = new ERR_HQNetwork();
|
||||
@@ -52,11 +54,84 @@ class ERR_HQ : ERR_CommandService
|
||||
protected float m_NextNetwork;
|
||||
protected float m_NextAssignment;
|
||||
|
||||
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Mortars = new ERR_MortarSystem(this); }
|
||||
static ERR_HQ Get()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return null;
|
||||
return ERR_HQ.Cast(mode.Command());
|
||||
}
|
||||
static bool MissionActive()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Active();
|
||||
}
|
||||
static void Event(string kind, string message, int messageId = 0)
|
||||
{
|
||||
ERR_HQ hq = Get();
|
||||
if (hq) hq.Journal().Write(kind, message, messageId);
|
||||
}
|
||||
ERR_MortarSystem Mortars() { return m_Mortars; }
|
||||
bool PrepareSupport() { return m_Mortars.Prepare(); }
|
||||
void ReserveGroup(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
unit.Release();
|
||||
unit.ReservedForSupport = true;
|
||||
}
|
||||
}
|
||||
void PhysicalForces(out array<IEntity> entities)
|
||||
{
|
||||
entities.Clear();
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (!unit.Group) continue;
|
||||
array<AIAgent> agents = {};
|
||||
unit.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !agent.GetControlledEntity()) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (!entities.Contains(actor)) entities.Insert(actor);
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(actor);
|
||||
if (vehicle && !entities.Contains(vehicle)) entities.Insert(vehicle);
|
||||
}
|
||||
}
|
||||
m_Mortars.AppendGuns(entities);
|
||||
}
|
||||
bool MortarConnected(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == group && unit.Radio) return m_Network.Route(unit.Radio) != null;
|
||||
return false;
|
||||
}
|
||||
ERR_HQRoute Downlink(SCR_AIGroup group, string kind, string detail, int sourceId)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group || !unit.Radio) continue;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) return null;
|
||||
route.MessageId = m_Journal.Message();
|
||||
route.Payload = string.Format("%1 from report M%2; %3", kind, sourceId, detail);
|
||||
m_Journal.Write("ORDER QUEUED", route.Payload, route.MessageId);
|
||||
return route;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
|
||||
{
|
||||
contacts.Clear();
|
||||
foreach (ERR_HQReport contact : m_Contacts)
|
||||
if (ERR_MortarPrediction.Fresh(contact, now)) contacts.Insert(contact.Snapshot());
|
||||
}
|
||||
|
||||
ERR_HQJournal Journal() { return m_Journal; }
|
||||
string Status()
|
||||
{
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned());
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + "; " + m_Mortars.Status();
|
||||
}
|
||||
|
||||
override void RegisterGroup(SCR_AIGroup group)
|
||||
@@ -167,6 +242,8 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!order || !ERR_MissionRules.ContactFresh(order.ObservedAt, now) || !unit.Eligible(m_Mode)) { unit.Release(); return; }
|
||||
SCR_AIGroupPerception perception = unit.Utility.m_Perception;
|
||||
if (perception && !perception.m_aTargets.IsEmpty()) { unit.Release(); return; }
|
||||
int deliveryId;
|
||||
if (unit.Downlink) deliveryId = unit.Downlink.MessageId;
|
||||
unit.Release();
|
||||
vector position = order.Position;
|
||||
if (order.HasVelocity) position = ERR_HQReport.Predict(position, order.Velocity, (now - order.ObservedAt) / 1000);
|
||||
@@ -175,7 +252,7 @@ class ERR_HQ : ERR_CommandService
|
||||
unit.Waypoint = waypoint;
|
||||
unit.Assignment = order;
|
||||
unit.Group.AddWaypointAt(waypoint, 0);
|
||||
m_Journal.Write("ORDER DELIVERED", string.Format("group %1: %2", unit.Id, order.Description(now)), order.MessageId);
|
||||
m_Journal.Write("ORDER DELIVERED", string.Format("group %1 from report M%2: %3", unit.Id, order.MessageId, order.Description(now)), deliveryId);
|
||||
}
|
||||
|
||||
protected void Assign(float now)
|
||||
@@ -240,11 +317,13 @@ class ERR_HQ : ERR_CommandService
|
||||
Observe(unit, now);
|
||||
}
|
||||
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
|
||||
m_Mortars.Tick(now);
|
||||
m_Journal.Flush();
|
||||
}
|
||||
|
||||
override void Stop()
|
||||
{
|
||||
m_Mortars.Stop();
|
||||
foreach (ERR_HQUnit unit : m_Units) unit.Release();
|
||||
m_Units.Clear();
|
||||
m_Contacts.Clear();
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
// Migrated from the independently authored AI extension; successor-owned physical groups.
|
||||
// Each site/phase has its own repeat limiter. Changed reasons and lifecycle
|
||||
// events are immediate; only identical polling results are summarized.
|
||||
class ERR_MortarTraceState
|
||||
{
|
||||
string LastReason;
|
||||
float NextLog;
|
||||
int Suppressed;
|
||||
|
||||
bool Due(string reason, float now)
|
||||
{
|
||||
if (reason != LastReason || now >= NextLog)
|
||||
{
|
||||
LastReason = reason;
|
||||
NextLog = now + 30000;
|
||||
return true;
|
||||
}
|
||||
Suppressed++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_MortarBattery
|
||||
{
|
||||
static const ResourceName GUN = "{D1FFE458E8AC4BDB}Prefabs/Weapons/Mortars/2B14/Mortar_2B14.et";
|
||||
static const ResourceName SHELL = "{98EC9C526AFBA282}Prefabs/Weapons/Ammo/Ammo_Shell_82mm_HE_O832DU.et";
|
||||
static const ResourceName CREW = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
static const ResourceName RADIO_OPERATOR = "{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et";
|
||||
IEntity RadioOperator;
|
||||
IEntity Gun;
|
||||
SCR_AIGroup Group;
|
||||
ref SCR_AIStaticArtilleryActivity Activity;
|
||||
ref ERR_HQReport Report;
|
||||
ref ERR_HQRoute RadioOrder;
|
||||
ref ERR_HQReport PendingRadioReport;
|
||||
vector Position;
|
||||
vector Impact;
|
||||
vector AimOffset;
|
||||
int AimedShot = -1;
|
||||
float FlightTime;
|
||||
float AimedAt = -1;
|
||||
float AirspaceUntil;
|
||||
vector AirspaceImpact;
|
||||
float AirspaceRadius = 600;
|
||||
float OrderedAt;
|
||||
float NextOrder;
|
||||
float SpawnedAt;
|
||||
bool StartupActivated;
|
||||
bool Released;
|
||||
bool Rejected;
|
||||
bool Claimed; // keeps the world slot occupied after the battery is destroyed
|
||||
int SiteId;
|
||||
int OrderId;
|
||||
int RadioId;
|
||||
int SourceId;
|
||||
int OrderShots;
|
||||
int TotalShots;
|
||||
bool CommandOpen;
|
||||
string LastFailure;
|
||||
string SpawnFailure;
|
||||
vector LastCandidate;
|
||||
ref map<string, int> SearchCounts = new map<string, int>();
|
||||
ref map<string, ref ERR_MortarTraceState> Traces = new map<string, ref ERR_MortarTraceState>();
|
||||
|
||||
void Audit(string kind, string detail)
|
||||
{
|
||||
ERR_HQ.Event("MORTAR " + kind, string.Format("site=%1 order=%2 radio=M%3 source=M%4 position=%5; %6", SiteId, OrderId, RadioId, SourceId, Position, detail), RadioId);
|
||||
}
|
||||
|
||||
void Trace(string phase, string reason, string detail)
|
||||
{
|
||||
ERR_MortarTraceState state;
|
||||
if (!Traces.Find(phase, state))
|
||||
{
|
||||
state = new ERR_MortarTraceState();
|
||||
Traces.Insert(phase, state);
|
||||
}
|
||||
string previous = state.LastReason;
|
||||
if (!state.Due(reason, GetGame().GetWorld().GetWorldTime()))
|
||||
return;
|
||||
Audit(phase, string.Format("reason=%1; %2; previous=%3 repeated polls omitted=%4", reason, detail, previous, state.Suppressed));
|
||||
state.Suppressed = 0;
|
||||
}
|
||||
|
||||
void FlushTraces()
|
||||
{
|
||||
foreach (string phase, ERR_MortarTraceState state : Traces)
|
||||
{
|
||||
if (state.Suppressed > 0)
|
||||
Audit("REPEATS", string.Format("phase=%1 reason=%2 repeated polls omitted=%3", phase, state.LastReason, state.Suppressed));
|
||||
}
|
||||
Traces.Clear();
|
||||
}
|
||||
|
||||
string OperationalReason()
|
||||
{
|
||||
if (!Gun) return "gun missing";
|
||||
if (!SpawnFailure.IsEmpty()) return SpawnFailure;
|
||||
if (!Group || !Group.GetGroupUtilityComponent()) return "crew group missing";
|
||||
if (Group.GetFaction() != ERR_GameMode.Get().EnemyFaction()) return "crew changed faction";
|
||||
if (Group.GetPlayerCount(true) > 0) return "player in crew group";
|
||||
if (Group.GetAgentsCount() == 0) return "crew group empty";
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(Gun.FindComponent(DamageManagerComponent));
|
||||
if (damage && damage.IsDestroyed()) return "gun destroyed";
|
||||
return "";
|
||||
}
|
||||
|
||||
void BeginRequest(ERR_HQReport report, float now)
|
||||
{
|
||||
Cancel("superseded by new HQ order");
|
||||
FlushTraces();
|
||||
OrderId++;
|
||||
RadioId = 0;
|
||||
SourceId = report.MessageId;
|
||||
OrderShots = 0;
|
||||
Released = false;
|
||||
CommandOpen = true;
|
||||
OrderedAt = now;
|
||||
LastFailure = "";
|
||||
Audit("COMMAND", string.Format("requested rounds=%1 HE radius=%2m; %3", ERR_MortarSalvo.ROUNDS, ERR_MortarSalvo.RADIUS_METRES, report.Description(now)));
|
||||
}
|
||||
|
||||
void ObserveProjectile(IEntity projectileEntity, IEntity firingCharacter, bool playerGunner)
|
||||
{
|
||||
if (!projectileEntity)
|
||||
{
|
||||
Audit("FIRE CALLBACK", "native OnWeaponFired had no projectile; no confirmed shot counted");
|
||||
return;
|
||||
}
|
||||
TotalShots++;
|
||||
if (CommandOpen && Released && !playerGunner)
|
||||
{
|
||||
OrderShots++;
|
||||
// Only a physical projectile unlocks the next load. It must be aimed again.
|
||||
Released = false;
|
||||
AimedAt = -1;
|
||||
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
|
||||
Audit("FIRED UNASSIGNED", string.Format("physical projectile=%1 origin=%2 player gunner=%3 order open=%4 committed=%5 total shots=%6; no active HQ order attribution", projectileEntity, projectileEntity.GetOrigin(), playerGunner, CommandOpen, Released, TotalShots));
|
||||
}
|
||||
|
||||
void ReserveShellAirspace(float now)
|
||||
{
|
||||
// Enclose every still-airborne shell, including an earlier, longer flight.
|
||||
// A later round must not move/shrink the existing exclusion or deadline.
|
||||
if (now >= AirspaceUntil)
|
||||
{
|
||||
AirspaceImpact = Impact;
|
||||
AirspaceRadius = 600;
|
||||
}
|
||||
AirspaceRadius = Math.Max(AirspaceRadius, vector.DistanceXZ(AirspaceImpact, Impact) + 600);
|
||||
AirspaceUntil = Math.Max(AirspaceUntil, now + (FlightTime + 10) * 1000);
|
||||
}
|
||||
|
||||
bool SpawnFailed(string reason)
|
||||
{
|
||||
SpawnFailure = reason;
|
||||
Audit("DEPLOY FAILED", string.Format("reason=%1 claimed=%2 gun=%3 group=%4", reason, Claimed, Gun, Group));
|
||||
return false;
|
||||
}
|
||||
|
||||
void SearchResult(vector candidate, string reason)
|
||||
{
|
||||
LastCandidate = candidate;
|
||||
int count;
|
||||
SearchCounts.Find(reason, count);
|
||||
SearchCounts.Set(reason, count + 1);
|
||||
}
|
||||
|
||||
void SiteStatus(float now)
|
||||
{
|
||||
if (!SearchCounts.IsEmpty())
|
||||
{
|
||||
string counts;
|
||||
foreach (string reason, int count : SearchCounts) counts += string.Format("%1=%2; ", reason, count);
|
||||
Audit("SITE SEARCH", string.Format("candidate counts since last summary: %1 last candidate=%2", counts, LastCandidate));
|
||||
SearchCounts.Clear();
|
||||
}
|
||||
int agents, living;
|
||||
int actionState = -1;
|
||||
if (Group)
|
||||
{
|
||||
array<AIAgent> crew = {};
|
||||
Group.GetAgents(crew);
|
||||
agents = crew.Count();
|
||||
foreach (AIAgent agent : crew)
|
||||
{
|
||||
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) living++;
|
||||
}
|
||||
}
|
||||
if (Activity) actionState = Activity.GetActionState();
|
||||
bool connected;
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && Group) connected = hq.MortarConnected(Group);
|
||||
Audit("SITE STATUS", string.Format("claimed=%1 startup=%2 operational=%3 crew agents=%4 fighting=%5 inventory HE=%6 radio connected=%7 cooldown remaining=%8s", Claimed, StartupActivated, Operational(), agents, living, CountShells(), connected, Math.Max(0, NextOrder - now) / 1000));
|
||||
Audit("ORDER STATUS", string.Format("open=%1 route pending=%2 native action=%3 shell committed=%4 order shots=%5 total shots=%6 age=%7s last failure=%8", CommandOpen, RadioOrder != null, actionState, Released, OrderShots, TotalShots, Math.Max(0, now - OrderedAt) / 1000, LastFailure));
|
||||
if (!SpawnFailure.IsEmpty()) Audit("PARTIAL DEPLOYMENT", SpawnFailure);
|
||||
if (!Operational()) Trace("SITE BLOCK", OperationalReason(), "spawn failure=" + SpawnFailure);
|
||||
}
|
||||
|
||||
bool Operational()
|
||||
{
|
||||
return OperationalReason().IsEmpty();
|
||||
}
|
||||
|
||||
int CountShells()
|
||||
{
|
||||
if (!Group)
|
||||
return 0;
|
||||
int count;
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity()))
|
||||
continue;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(agent.GetControlledEntity().FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory)
|
||||
continue;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL)
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void Bind(int siteId, SCR_AIGroup group, IEntity gun, float now)
|
||||
{
|
||||
SiteId = siteId;
|
||||
Group = group;
|
||||
Gun = gun;
|
||||
Position = gun.GetOrigin();
|
||||
SpawnedAt = now;
|
||||
Claimed = true;
|
||||
}
|
||||
|
||||
|
||||
protected int InventoryShells(InventoryStorageManagerComponent inventory)
|
||||
{
|
||||
if (!inventory)
|
||||
return 0;
|
||||
int count;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == SHELL)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
protected void Equip(IEntity soldier)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
bool SupplyGunner()
|
||||
{
|
||||
if (!Operational())
|
||||
return SupplyFailed(OperationalReason());
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Gun.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
IEntity gunner;
|
||||
if (usage && usage.GetTurretCompartmentSlot())
|
||||
gunner = usage.GetTurretCompartmentSlot().GetOccupant();
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
float nearest = float.MAX;
|
||||
if (!gunner)
|
||||
{
|
||||
foreach (AIAgent candidate : agents)
|
||||
{
|
||||
if (!candidate || candidate.GetControlledEntity() == RadioOperator || !ERR_Actors.CanFight(candidate.GetControlledEntity()))
|
||||
continue;
|
||||
float distance = vector.DistanceXZ(Gun.GetOrigin(), candidate.GetControlledEntity().GetOrigin());
|
||||
if (distance < nearest)
|
||||
{
|
||||
nearest = distance;
|
||||
gunner = candidate.GetControlledEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
InventoryStorageManagerComponent destination = InventoryStorageManagerComponent.Cast(gunner.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!destination)
|
||||
return SupplyFailed("gunner inventory missing");
|
||||
if (InventoryShells(destination) > 0)
|
||||
return true;
|
||||
int nearbyDonors, donorShells, storageRejected, moveRejected;
|
||||
foreach (AIAgent bearer : agents)
|
||||
{
|
||||
if (!bearer || !ERR_Actors.CanFight(bearer.GetControlledEntity()))
|
||||
continue;
|
||||
IEntity donor = bearer.GetControlledEntity();
|
||||
if (donor == gunner || vector.DistanceXZ(donor.GetOrigin(), gunner.GetOrigin()) > 12)
|
||||
continue;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(donor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory)
|
||||
continue;
|
||||
nearbyDonors++;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.GetPrefabData() || item.GetPrefabData().GetPrefabName() != SHELL)
|
||||
continue;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
bool SupplyFailed(string reason)
|
||||
{
|
||||
LastFailure = reason;
|
||||
Audit("AMMO FAILED", reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
void Cancel(string reason = "cancel requested")
|
||||
{
|
||||
if (CommandOpen)
|
||||
{
|
||||
FlushTraces();
|
||||
Audit("ORDER END", string.Format("reason=%1 observed shots=%2 shell committed=%3 elapsed=%4s last failure=%5", reason, OrderShots, Released, (GetGame().GetWorld().GetWorldTime() - OrderedAt) / 1000, LastFailure));
|
||||
}
|
||||
CommandOpen = false;
|
||||
RadioOrder = null;
|
||||
PendingRadioReport = null;
|
||||
Report = null;
|
||||
AimedAt = -1;
|
||||
if (Activity)
|
||||
Activity.Complete();
|
||||
Activity = null;
|
||||
}
|
||||
|
||||
// Idle distant batteries still report through HQ's scheduler. Their native
|
||||
// AI needs full detail while firing or when players can interact with them.
|
||||
protected void UpdateCrewDetail(float now, bool firing = false)
|
||||
{
|
||||
if (!Group || Group.GetPlayerCount(true) > 0)
|
||||
return;
|
||||
bool active = firing || Activity != null || ERR_MortarSystem.NearPlayer(Position);
|
||||
int lod = -1;
|
||||
if (active)
|
||||
lod = 0;
|
||||
if (Group.GetPermanentLOD() != lod)
|
||||
Group.SetPermanentLOD(lod);
|
||||
if (active && !Group.IsAIActivated())
|
||||
Group.ActivateAI();
|
||||
array<AIAgent> agents = {};
|
||||
Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent || EntityUtils.IsPlayer(agent.GetControlledEntity()))
|
||||
continue;
|
||||
if (agent.GetPermanentLOD() != lod)
|
||||
agent.SetPermanentLOD(lod);
|
||||
if (active && !agent.IsAIActivated() && ERR_Actors.CanFight(agent.GetControlledEntity()))
|
||||
agent.ActivateAI();
|
||||
}
|
||||
}
|
||||
|
||||
void Update(float 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));
|
||||
if (Activity.GetActionState() == EAIActionState.FAILED) Cancel("native artillery activity FAILED");
|
||||
else if (Activity.GetActionState() == EAIActionState.COMPLETED) Cancel("native artillery activity COMPLETED (shot count is separate)");
|
||||
else if (now - OrderedAt > ERR_MortarSalvo.TIMEOUT_MS) Cancel("120s artillery salvo timeout");
|
||||
}
|
||||
if (StartupActivated)
|
||||
UpdateCrewDetail(now);
|
||||
}
|
||||
|
||||
void Order(ERR_HQReport report, float now)
|
||||
{
|
||||
Report = report.Snapshot();
|
||||
OrderedAt = now;
|
||||
NextOrder = now + 30000;
|
||||
Released = false;
|
||||
UpdateCrewDetail(now, true);
|
||||
Activity = new SCR_AIStaticArtilleryActivity(Group.GetGroupUtilityComponent(), null, report.Position, SCR_EAIArtilleryAmmoType.HIGH_EXPLOSIVE, ERR_MortarSalvo.ROUNDS);
|
||||
Group.GetGroupUtilityComponent().AddAction(Activity);
|
||||
Audit("AIM", string.Format("native activity created; latest source=M%1 observed=%2", report.MessageId, report.Description(now)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// The dedicated manpack operator has no mortar-shell backpack. Preserve the
|
||||
// native chooser and its caller's original index for every eligible gunner.
|
||||
modded class SCR_AIStaticArtilleryActivity
|
||||
{
|
||||
protected override AIAgent FindBestAgentForArtillery(notnull array<AIAgent> agents, notnull IEntity artilleryEntity, out int outIndex)
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
ERR_MortarBattery battery;
|
||||
if (system) battery = system.FindGun(artilleryEntity);
|
||||
if (!battery) return super.FindBestAgentForArtillery(agents, artilleryEntity, outIndex);
|
||||
array<AIAgent> gunners = {};
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (agent && agent.GetControlledEntity() != battery.RadioOperator && ERR_Actors.CanFight(agent.GetControlledEntity()))
|
||||
gunners.Insert(agent);
|
||||
}
|
||||
int filteredIndex;
|
||||
AIAgent selected = super.FindBestAgentForArtillery(gunners, artilleryEntity, filteredIndex);
|
||||
outIndex = -1;
|
||||
if (selected) outIndex = agents.Find(selected);
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
// Leave all other artillery (including player-operated captured guns) native.
|
||||
modded class SCR_AIGetArtilleryAimDistanceCompensation
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
IEntity gun;
|
||||
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
ERR_MortarBattery battery;
|
||||
if (system)
|
||||
battery = system.FindGun(gun);
|
||||
if (!battery)
|
||||
return super.EOnTaskSimulate(owner, dt);
|
||||
battery.AimedAt = -1;
|
||||
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; }
|
||||
if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) { system.Withhold(battery, "aim: salvo complete or order closed"); return ENodeResult.FAIL; }
|
||||
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)));
|
||||
ResourceName ammo;
|
||||
GetVariableIn(PORT_AMMO_PREFAB, ammo);
|
||||
if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return ENodeResult.FAIL; }
|
||||
Resource resource = Resource.Load(ammo);
|
||||
if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return ENodeResult.FAIL; }
|
||||
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||
if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return ENodeResult.FAIL; }
|
||||
array<float> coefficients = {};
|
||||
GetAmmoInitialSpeedCoefficients(ammo, coefficients);
|
||||
if (coefficients.IsEmpty())
|
||||
coefficients.Insert(1.0);
|
||||
float minimum, maximum;
|
||||
GetVerticalAimLimits(gun, minimum, maximum);
|
||||
float flight = 30;
|
||||
float offset;
|
||||
int charge;
|
||||
vector impact;
|
||||
vector salvoOffset = ERR_MortarSalvo.Offset(battery.SiteId, battery.OrderId, battery.OrderShots);
|
||||
bool converged;
|
||||
// Iterate the moving intercept using the native shell's flight time.
|
||||
// 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; }
|
||||
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; }
|
||||
bool reachable;
|
||||
float calculated;
|
||||
for (int i = 0; i < coefficients.Count(); i++)
|
||||
{
|
||||
if (!BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(distance, offset, calculated, source, altitude, coefficients[i]))
|
||||
continue;
|
||||
float tangent = (offset + altitude) / distance;
|
||||
if (calculated <= 0 || calculated > 75 || tangent < Math.Tan(minimum) || tangent > Math.Tan(maximum))
|
||||
continue;
|
||||
charge = i;
|
||||
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; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIShootStaticArtillery
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
IEntity gun;
|
||||
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
ERR_MortarBattery battery;
|
||||
if (system)
|
||||
battery = system.FindGun(gun);
|
||||
if (battery && !system.AllowShot(battery))
|
||||
{
|
||||
battery.Trace("SHOOT NODE", "authorization rejected", battery.LastFailure);
|
||||
return ENodeResult.FAIL;
|
||||
}
|
||||
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
||||
if (battery)
|
||||
battery.Trace("SHOOT NODE", string.Format("native result=%1", result), "authorization passed; node result does not confirm a physical shot");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_MortarMuzzleComponent
|
||||
{
|
||||
protected override void TransferShellToMortar()
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
ERR_MortarBattery battery;
|
||||
if (system && m_CharController && !EntityUtils.IsPlayer(m_CharController.GetOwner()))
|
||||
battery = system.FindGun(GetOwner());
|
||||
if (battery)
|
||||
{
|
||||
// The loading animation takes time. Recheck at the actual transfer,
|
||||
// before native inventory insertion triggers the physical projectile.
|
||||
if (!system.AllowShot(battery))
|
||||
{
|
||||
if (m_ShellComponent)
|
||||
{
|
||||
m_ShellComponent.GetOnShellUsed().Remove(TransferShellToMortar);
|
||||
m_ShellComponent.SetLoadedState(false);
|
||||
}
|
||||
CharacterAnimationComponent animation = m_CharController.GetAnimationComponent();
|
||||
if (animation && animation.GetCommandHandler())
|
||||
animation.GetCommandHandler().FinishItemUse(false);
|
||||
battery.Audit("ABORT", "final shell transfer rejected; " + battery.LastFailure);
|
||||
return;
|
||||
}
|
||||
battery.Released = true;
|
||||
battery.ReserveShellAirspace(GetGame().GetWorld().GetWorldTime());
|
||||
battery.Audit("SHELL COMMIT", string.Format("final safety passed; gunner=%1 predicted impact=%2 flight=%3s; invoking native transfer, awaiting projectile event", m_CharController.GetOwner(), battery.Impact, battery.FlightTime));
|
||||
}
|
||||
super.TransferShellToMortar();
|
||||
}
|
||||
|
||||
override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity)
|
||||
{
|
||||
IEntity firingCharacter;
|
||||
if (m_CharController) firingCharacter = m_CharController.GetOwner();
|
||||
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
ERR_MortarBattery battery;
|
||||
if (system)
|
||||
battery = system.FindGun(GetOwner());
|
||||
if (!battery)
|
||||
return;
|
||||
bool playerGunner;
|
||||
if (firingCharacter) playerGunner = EntityUtils.IsPlayer(firingCharacter);
|
||||
battery.ObserveProjectile(projectileEntity, firingCharacter, playerGunner);
|
||||
}
|
||||
}
|
||||
|
||||
// The native tree normally re-aims only every five shots (and skips the first
|
||||
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
|
||||
modded class SCR_AIStaticArtilleryBehavior
|
||||
{
|
||||
ERR_MortarBattery ERR_GetMortarBattery()
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
if (!system) return null;
|
||||
return system.FindGun(m_ArtilleryEntity.m_Value);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
|
||||
{
|
||||
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
|
||||
{
|
||||
ERR_MortarBattery battery;
|
||||
SCR_AIStaticArtilleryBehavior behavior;
|
||||
if (m_Utility)
|
||||
{
|
||||
behavior = SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction());
|
||||
if (behavior) battery = behavior.ERR_GetMortarBattery();
|
||||
}
|
||||
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;
|
||||
}
|
||||
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,216 @@
|
||||
// Owned and ticked by HQ. Initial assets are authored once; losses are never replaced.
|
||||
class ERR_MortarSystem
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ref array<ref ERR_MortarBattery> m_Batteries = {};
|
||||
protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety();
|
||||
protected bool m_Prepared;
|
||||
protected bool m_Stopped;
|
||||
protected float m_NextDecision;
|
||||
string SafetyReason;
|
||||
string SettlementReason;
|
||||
|
||||
void ERR_MortarSystem(ERR_HQ hq) { m_HQ = hq; }
|
||||
static ERR_MortarSystem Get()
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (!hq || !Replication.IsServer()) return null;
|
||||
return hq.Mortars();
|
||||
}
|
||||
static bool NearPlayer(vector position)
|
||||
{
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
foreach (IEntity player : players)
|
||||
if (vector.DistanceXZ(player.GetOrigin(), position) <= 1200) return true;
|
||||
return false;
|
||||
}
|
||||
bool Prepare()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Prepared || m_Stopped) return false;
|
||||
m_Prepared = true;
|
||||
vector position = ERR_Actors.Ground("6000 0 7000");
|
||||
if (position[1] < 1) return false;
|
||||
IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position);
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
|
||||
if (!gun || !group)
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(gun);
|
||||
ERR_Actors.DeleteSpawned(group);
|
||||
return false;
|
||||
}
|
||||
group.InitFactionKey("USSR");
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(gun.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(ERR_GameMode.Get().EnemyFaction());
|
||||
ERR_MortarBattery battery = new ERR_MortarBattery();
|
||||
battery.Bind(1, group, gun, GetGame().GetWorld().GetWorldTime());
|
||||
m_Batteries.Insert(battery);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
mode.Own(gun);
|
||||
mode.RegisterGroup(group);
|
||||
m_HQ.ReserveGroup(group);
|
||||
battery.Audit("DEPLOY", "authored gun and four-person crew; finite ammunition initialization pending");
|
||||
return true;
|
||||
}
|
||||
static ResourceName MortarGroup() { return "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"; }
|
||||
ERR_MortarBattery FindGun(IEntity gun)
|
||||
{
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
if (gun && battery.Gun == gun) return battery;
|
||||
return null;
|
||||
}
|
||||
ERR_MortarBattery FindGroup(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
if (group && battery.Group == group) return battery;
|
||||
return null;
|
||||
}
|
||||
ERR_HQReport Latest(ERR_MortarBattery battery, float now)
|
||||
{
|
||||
// Only the coordinates actually delivered to this battery are available.
|
||||
// A newer HQ contact cannot update a gun's knowledge without another downlink.
|
||||
if (!battery || !ERR_MortarPrediction.Fresh(battery.Report, now)) return null;
|
||||
return battery.Report;
|
||||
}
|
||||
string ContactFailure(ERR_MortarBattery battery)
|
||||
{
|
||||
if (!battery.Report) return "no delivered target report";
|
||||
return "delivered target report expired or invalid";
|
||||
}
|
||||
bool OutsideSettlements(vector impact)
|
||||
{
|
||||
bool clear = m_Settlements.OutsideSettlements(impact);
|
||||
SettlementReason = m_Settlements.SettlementReason;
|
||||
return clear;
|
||||
}
|
||||
bool Safe(ERR_MortarBattery battery, vector impact, float flight)
|
||||
{
|
||||
SafetyReason = "clear";
|
||||
if (!battery.Operational()) { SafetyReason = battery.OperationalReason(); return false; }
|
||||
if (!OutsideSettlements(impact)) { SafetyReason = SettlementReason; return false; }
|
||||
if (SCR_TerrainHelper.GetTerrainY(impact) < 0) { SafetyReason = "impact below Everon sea level"; return false; }
|
||||
Faction faction = battery.Group.GetFaction();
|
||||
if (!faction) { SafetyReason = "battery faction missing"; return false; }
|
||||
array<IEntity> friendlies = {};
|
||||
m_HQ.PhysicalForces(friendlies);
|
||||
foreach (IEntity entity : friendlies)
|
||||
{
|
||||
Faction friendly = SCR_FactionManager.SGetFaction(entity);
|
||||
if (!friendly || faction.IsFactionEnemy(friendly) || !ERR_Actors.Alive(entity)) continue;
|
||||
vector velocity;
|
||||
IEntity moving = CompartmentAccessComponent.GetVehicleIn(entity);
|
||||
if (!moving) moving = entity;
|
||||
if (moving.GetPhysics()) velocity = moving.GetPhysics().GetVelocity();
|
||||
if (ERR_MortarPrediction.ThreatensFriendly(impact, entity.GetOrigin(), velocity, flight))
|
||||
{
|
||||
SafetyReason = "friendly physical force can enter the impact area";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Withhold(ERR_MortarBattery battery, string reason, string detail = "")
|
||||
{
|
||||
battery.LastFailure = reason + "; " + detail;
|
||||
battery.Trace("WITHHELD", reason, detail);
|
||||
// Finish the activity from the next HQ tick, never from inside its own node.
|
||||
battery.Rejected = true;
|
||||
return false;
|
||||
}
|
||||
bool AllowShot(ERR_MortarBattery battery)
|
||||
{
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (!ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive");
|
||||
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");
|
||||
ERR_HQReport report = Latest(battery, now);
|
||||
if (!report) return Withhold(battery, ContactFailure(battery));
|
||||
vector updated;
|
||||
if (!ERR_MortarPrediction.Predict(report, now, battery.FlightTime, updated)) return Withhold(battery, ERR_MortarPrediction.FailureReason);
|
||||
updated += battery.AimOffset;
|
||||
if (vector.DistanceXZ(updated, battery.Impact) > 60) return Withhold(battery, "predicted target left aimed impact area");
|
||||
if (!OutsideSettlements(updated)) return Withhold(battery, SettlementReason);
|
||||
if (!Safe(battery, battery.Impact, battery.FlightTime)) return Withhold(battery, SafetyReason);
|
||||
return true;
|
||||
}
|
||||
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())
|
||||
{
|
||||
battery.Cancel("radio delivery, report freshness or gunner availability failed");
|
||||
battery.NextOrder = now + 10000;
|
||||
return;
|
||||
}
|
||||
battery.Order(delivered, now);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer()) return;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
{
|
||||
if (battery.Rejected && !battery.Released)
|
||||
{
|
||||
battery.Cancel("native aim or firing authorization rejected");
|
||||
battery.NextOrder = now + 10000;
|
||||
}
|
||||
battery.Rejected = false;
|
||||
battery.Update(now);
|
||||
AdvanceOrder(battery, now);
|
||||
}
|
||||
if (now < m_NextDecision) return;
|
||||
m_NextDecision = now + 10000;
|
||||
array<ref ERR_HQReport> reports = {};
|
||||
m_HQ.SupportContacts(reports, now);
|
||||
ERR_MortarBattery selected;
|
||||
ERR_HQReport target;
|
||||
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;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
{
|
||||
float candidate = vector.DistanceXZ(battery.Position, report.Position);
|
||||
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
|
||||
selected = battery;
|
||||
target = report;
|
||||
distance = candidate;
|
||||
}
|
||||
}
|
||||
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);
|
||||
selected.RadioId = route.MessageId;
|
||||
selected.RadioOrder = route;
|
||||
selected.PendingRadioReport = target.Snapshot();
|
||||
m_NextDecision = now + 20000;
|
||||
}
|
||||
void AppendGuns(array<IEntity> entities)
|
||||
{
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
if (battery.Gun && !entities.Contains(battery.Gun)) entities.Insert(battery.Gun);
|
||||
}
|
||||
string Status()
|
||||
{
|
||||
int ready, shells;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries)
|
||||
{
|
||||
if (battery.StartupActivated && battery.Operational()) ready++;
|
||||
shells += battery.CountShells();
|
||||
}
|
||||
return string.Format("mortars %1/%2; HE shells %3", ready, m_Batteries.Count(), shells);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_MortarBattery battery : m_Batteries) battery.Cancel("HQ stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Vanilla named settlements supply exclusion areas; no original mission location system.
|
||||
// Shared town/village exclusion for mortar impacts, roadblocks and mine sites.
|
||||
// Fixed distance from mapped settlement centres, independent of mission
|
||||
// activation radii. Scattered buildings and props are not settlements.
|
||||
class ERR_SettlementSafety
|
||||
{
|
||||
static const float RADIUS = 200;
|
||||
string SettlementReason;
|
||||
protected vector m_SafeImpact;
|
||||
protected bool m_SettlementNear;
|
||||
protected vector m_LastSettlementImpact;
|
||||
protected float m_LastSettlementCheck = -1;
|
||||
protected bool m_LastSettlementClear;
|
||||
|
||||
bool OutsideSettlements(vector impact)
|
||||
{
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_LastSettlementCheck >= 0 && now >= m_LastSettlementCheck && now - m_LastSettlementCheck < 1000 && vector.DistanceXZ(impact, m_LastSettlementImpact) == 0)
|
||||
return m_LastSettlementClear;
|
||||
m_LastSettlementCheck = now;
|
||||
m_LastSettlementImpact = impact;
|
||||
m_LastSettlementClear = false;
|
||||
m_SafeImpact = impact;
|
||||
m_SettlementNear = false;
|
||||
SettlementReason = "clear";
|
||||
// Also cover named native map settlements, independent of whether the
|
||||
// mission placed a garrison marker there. Generic building entities also
|
||||
// include deer stands, bridge pillars, rubble and electrical cabinets.
|
||||
// Broad vertical bounds keep the 200m distance horizontal on hills.
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(impact - Vector(RADIUS, 5000, RADIUS), impact + Vector(RADIUS, 5000, RADIUS), CheckSettlement, FilterSettlement, EQueryEntitiesFlags.ALL);
|
||||
m_LastSettlementClear = !m_SettlementNear;
|
||||
return m_LastSettlementClear;
|
||||
}
|
||||
|
||||
protected bool FilterSettlement(IEntity entity)
|
||||
{
|
||||
return entity && entity.FindComponent(MapDescriptorComponent);
|
||||
}
|
||||
|
||||
protected bool CheckSettlement(IEntity entity)
|
||||
{
|
||||
float radius;
|
||||
MapDescriptorComponent descriptor = MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent));
|
||||
if (descriptor)
|
||||
{
|
||||
int type = descriptor.GetBaseType();
|
||||
if (type == EMapDescriptorType.MDT_NAME_CITY)
|
||||
radius = RADIUS;
|
||||
else if (type == EMapDescriptorType.MDT_NAME_TOWN)
|
||||
radius = RADIUS;
|
||||
else if (type == EMapDescriptorType.MDT_NAME_VILLAGE || type == EMapDescriptorType.MDT_NAME_SETTLEMENT)
|
||||
radius = RADIUS;
|
||||
}
|
||||
if (radius > 0 && vector.DistanceXZ(m_SafeImpact, entity.GetOrigin()) <= radius)
|
||||
{
|
||||
m_SettlementNear = true;
|
||||
SettlementReason = string.Format("named map settlement entity=%1 position=%2 distance=%3m exclusion=%4m", entity, entity.GetOrigin(), vector.DistanceXZ(m_SafeImpact, entity.GetOrigin()), radius);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
modded class ERR_ScenarioComponent
|
||||
{
|
||||
protected override bool PrepareAdditionalForces()
|
||||
{
|
||||
if (!super.PrepareAdditionalForces()) return false;
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
return hq && hq.PrepareSupport();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user