Integrate radio-delivered convoy cover and conservative squad ammunition reports

This commit is contained in:
2026-09-20 04:23:11 +02:00
parent 9b6596eb3e
commit 9205e2129a
88 changed files with 13934 additions and 28 deletions
@@ -0,0 +1,40 @@
// HQ creates these snapshots from delivered sightings and sends each command
// through the original group's physical radio downlink before execution.
class ERR_ConvoyCombatOrder
{
int CommandId;
int ReportId;
int VehicleIndex;
string SourceId;
SCR_AIGroup Group;
bool Hold;
float ObservedAt = -1;
float IssuedAt = -1;
float ExpiresAt = -1;
}
class ERR_ConvoyCombatState
{
bool Held;
int LastCommandId;
int ReportId;
float ObservedAt = -1;
}
class ERR_ConvoyCombatRules
{
static bool Accept(ERR_ConvoyCombatState state, ERR_ConvoyCombatOrder order, float now)
{
if (!state || !order || order.CommandId <= state.LastCommandId || order.CommandId <= 0 || order.ReportId <= 0) return false;
if (!ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(order.ObservedAt) || !ERR_ForceRules.Finite(order.IssuedAt) || !ERR_ForceRules.Finite(order.ExpiresAt)) return false;
if (order.ObservedAt < 0 || order.IssuedAt < order.ObservedAt || now < order.IssuedAt || now >= order.ExpiresAt || order.ExpiresAt <= order.IssuedAt || order.ExpiresAt - order.IssuedAt > 30000) return false;
if (order.Hold)
{
if (now - order.ObservedAt > 60000 || order.ObservedAt < state.ObservedAt) return false;
if (state.ReportId == order.ReportId && state.ObservedAt != order.ObservedAt) return false;
if (order.ReportId < state.ReportId) return false;
return true;
}
return state.Held && order.ReportId == state.ReportId && order.ObservedAt == state.ObservedAt && order.IssuedAt - state.ObservedAt >= 60000;
}
}
+36 -1
View File
@@ -112,6 +112,7 @@ class ERR_ConvoyVehicle
int RouteIndex;
bool Released;
bool Registered;
ref ERR_ConvoyCombatState Combat = new ERR_ConvoyCombatState();
bool Arrived;
float LegDeadline;
float NextReadinessNote;
@@ -126,6 +127,7 @@ class ERR_ConvoyVehicle
{
if (Released) return;
Released = true;
Combat.Held = false;
ClearOrder();
if (mode && mode.Forces())
{
@@ -151,7 +153,7 @@ class ERR_ConvoyVehicle
}
bool Order(ERR_ForceRoute route, float now)
{
if (Released || Arrived || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
if (Released || Arrived || Combat.Held || !Group || !Entity || !route || RouteIndex >= route.Count()) return false;
if (Waypoint) return true;
Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), route.At(RouteIndex)));
if (!Waypoint) return false;
@@ -164,6 +166,7 @@ class ERR_ConvoyVehicle
{
if (Released || Arrived) return;
if (!CrewValid(mode)) { Release(mode); return; }
if (Combat.Held) return;
// Native completion measures the seated group, not the vehicle origin.
// Keep the same predicate after native removal of the completed waypoint.
if (Waypoint && Waypoint.IsWithinCompletionRadius(Group))
@@ -344,6 +347,9 @@ class ERR_ConvoyService
m_Mode.RegisterGroup(unit.Group);
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
if (hq) hq.ReserveGroup(unit.Group);
// Native helper idempotently registers deletion/damage callbacks and mounted
// combat activity; raw vehicle-manager admission alone omits those hooks.
utility.AddUsableVehicle(usage);
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
}
@@ -389,6 +395,35 @@ class ERR_ConvoyService
if (!pending) task.Stop(m_Mode, "finite authored road route ended or all vehicle orders released");
}
}
bool ApplyCombatOrder(ERR_ConvoyTask task, ERR_ConvoyCombatOrder order, float now)
{
if (!Current() || !task || !order || !m_Tasks.Contains(task) || task.Stopped || !task.Ready || !task.Receipt || !task.Receipt.Consumed) return false;
if (order.VehicleIndex < 0 || order.VehicleIndex >= task.Vehicles.Count() || order.SourceId != task.Receipt.SourceId) return false;
ERR_ConvoyVehicle unit = task.Vehicles[order.VehicleIndex];
if (!unit || !unit.Registered || unit.Released || unit.Arrived || unit.Index != order.VehicleIndex || unit.Group != order.Group || !unit.CrewValid(m_Mode)) return false;
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group) || unit.Index >= task.Receipt.Records.Count()) return false;
if (!ERR_ConvoyCombatRules.Accept(unit.Combat, order, now) || !task.Route || unit.RouteIndex >= task.Route.Count()) return false;
ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
if (!record || !record.Physical || record.Eliminated || record.Kind != ERR_ForceKind.CONVOY || record.Id != task.Receipt.Records[unit.Index]) return false;
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
if (agents.Count() != unit.Members.Count()) return false;
foreach (AIAgent agent : agents)
if (!agent || !unit.Members.Contains(agent.GetControlledEntity())) return false;
// No caller can replay even a command whose native waypoint recreation fails.
unit.Combat.LastCommandId = order.CommandId;
if (order.Hold)
{
if (!unit.Combat.Held) unit.ClearOrder();
unit.Combat.Held = true; unit.Combat.ReportId = order.ReportId; unit.Combat.ObservedAt = order.ObservedAt;
}
else
{
unit.Combat.Held = false;
if (!unit.Order(task.Route, now)) { unit.Release(m_Mode); return false; }
}
ERR_HQ.Event("CONVOY COVER", string.Format("command M%1 report M%2 vehicle=%3 held=%4 routeIndex=%5", order.CommandId, order.ReportId, unit.Index, unit.Combat.Held, unit.RouteIndex));
return true;
}
int Count() { return m_Tasks.Count(); }
string Status()
{
+92 -3
View File
@@ -28,6 +28,7 @@ class ERR_HQUnit
bool ReservedForSupport;
int LogisticsReservation;
ref ERR_MortarSupplyStatus PendingSupplyStatus;
ref ERR_ConvoyCombatOrder PendingConvoyOrder;
bool Eligible(ERR_GameMode mode)
{
@@ -47,6 +48,7 @@ class ERR_HQUnit
void CancelPending()
{
PendingOrder = null;
PendingConvoyOrder = null;
PendingCheckin = false;
PendingTask = null;
Downlink = null;
@@ -429,6 +431,9 @@ class ERR_HQ : ERR_CommandService
if (!report) return;
route.MessageId = report.MessageId;
route.Payload = string.Format("status %1: %2 living, %3 fit, %4 confirmed losses; sampled=%5", report.SourceId, report.Count, report.Fit, report.ConfirmedLosses, report.ObservedAt);
string ammunition = "unknown";
if (report.AmmoState && report.AmmoState.Complete) ammunition = string.Format("%1 magazine rounds, %2 chambered", report.AmmoState.SmallArmsRounds, report.AmmoState.SmallArmsChambers);
route.Payload += "; small-arms ammunition=" + ammunition;
unit.PendingStatus = report; unit.Uplink = route; unit.NextStatus = now + 180000;
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
}
@@ -514,6 +519,7 @@ class ERR_HQ : ERR_CommandService
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.ForceSourceId == entry.SourceId || !unit.Radio || !unit.PlanEligible || unit.PlanFighting || unit.Assignment || unit.PendingOrder || unit.Downlink) continue;
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
if (unit.HomeGuard && HomeAvailable(unit) <= 1) continue;
if (!ERR_HQPursuitRules.BearingAllowed(ERR_HQPursuitRules.Bearing(unit.PlanPosition, entry.Position), first, second)) continue;
float candidate = vector.DistanceXZ(unit.PlanPosition, entry.Position);
@@ -610,6 +616,7 @@ class ERR_HQ : ERR_CommandService
else unit.CancelPending();
return;
}
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) { LostOrder(unit, now); return; }
if (task.Investigation())
{
if (!WatchTaskValid(task)) { LostOrder(unit, now); return; }
@@ -718,7 +725,8 @@ class ERR_HQ : ERR_CommandService
{
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Assignment || !unit.Task || unit.Task.Investigation() || unit.PendingOrder || !unit.Radio || unit.PlanFighting || !unit.PlanEligible) continue;
if (!unit.Assignment || !unit.Task || unit.Task.Investigation() || unit.PendingOrder || unit.Downlink || !unit.Radio || unit.PlanFighting || !unit.PlanEligible) continue;
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
ERR_HQBelief belief = m_Memory.GetById(unit.Task.BeliefId);
if (!belief || belief.BeliefRevision == unit.Task.BeliefRevision || now - belief.ObservedAt > 45000) continue;
// Nearby updates refine memory without churning native movement orders.
@@ -753,6 +761,85 @@ class ERR_HQ : ERR_CommandService
if (hunt) hunt.NextHunt = Math.Max(hunt.NextHunt, task.ExpiresAt + 180000);
}
}
// Convoy planning reads delivered own-position status and delivered sightings.
// A silent convoy keeps its last order; no target or vehicle position is queried.
protected bool ConvoyPositionKnown(ERR_HQUnit unit, float now)
{
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
return entry && !entry.Closed && entry.Count > 0 && entry.ObservedAt >= 0
&& ERR_HQPursuitRules.Finite(now) && now >= entry.ObservedAt
&& now - entry.ObservedAt <= 180000;
}
protected ERR_HQReport ConvoyContact(ERR_HQUnit unit, float now)
{
if (!ConvoyPositionKnown(unit, now)) return null;
ERR_HQWatchEntry entry = m_Watch.Find(unit.ForceSourceId);
ERR_HQReport best;
foreach (ERR_HQReport report : m_Contacts)
{
if (report.ObservedAt < 0 || now < report.ObservedAt || now - report.ObservedAt >= 60000
|| vector.DistanceXZ(entry.Position, report.Position) > 400) continue;
if (!best || report.ObservedAt > best.ObservedAt
|| (report.ObservedAt == best.ObservedAt && report.MessageId > best.MessageId)) best = report;
}
return best;
}
protected void PlanConvoy(float now)
{
if (!m_ConvoyTask || m_ConvoyTask.Stopped || !m_ConvoyTask.Ready) return;
foreach (ERR_ConvoyVehicle vehicle : m_ConvoyTask.Vehicles)
{
if (vehicle.Released || vehicle.Arrived || !vehicle.Registered || !vehicle.Group) continue;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != vehicle.Group || !unit.ReservedForSupport || unit.Downlink || !unit.Radio) continue;
if (!ConvoyPositionKnown(unit, now)) break;
ERR_HQReport contact = ConvoyContact(unit, now);
if (contact)
{
if (vehicle.Combat.Held && contact.MessageId == vehicle.Combat.ReportId) break;
if (contact.ObservedAt < vehicle.Combat.ObservedAt) break;
}
else if (!vehicle.Combat.Held || now - vehicle.Combat.ObservedAt < 60000) break;
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
if (!route) break;
ERR_ConvoyCombatOrder order = new ERR_ConvoyCombatOrder();
order.CommandId = m_Journal.Message(); order.SourceId = m_ConvoyTask.Receipt.SourceId;
order.VehicleIndex = vehicle.Index; order.Group = unit.Group;
order.Hold = contact != null; order.IssuedAt = now; order.ExpiresAt = now + 30000;
order.ReportId = vehicle.Combat.ReportId; order.ObservedAt = vehicle.Combat.ObservedAt;
string action = "resume authored route";
if (contact)
{
order.ReportId = contact.MessageId; order.ObservedAt = contact.ObservedAt;
action = "suspend travel for mounted cover";
}
route.MessageId = order.CommandId;
route.Payload = string.Format("convoy vehicle %1: %2; delivered sighting M%3", vehicle.Index, action, order.ReportId);
unit.PendingConvoyOrder = order; unit.Downlink = route;
m_Journal.Write("CONVOY ORDER QUEUED", route.Payload, route.MessageId);
break;
}
}
}
protected void DeliverConvoyOrder(ERR_HQUnit unit, float now)
{
ERR_ConvoyCombatOrder order = unit.PendingConvoyOrder;
bool accepted;
if (order && order.Group == unit.Group && ConvoyPositionKnown(unit, now))
{
ERR_HQReport contact = ConvoyContact(unit, now);
// A newer nearby sighting still justifies HOLD: frequent reports must not
// starve a slower radio downlink. New threat evidence always invalidates RESUME.
bool current = !order.Hold && !contact;
if (order.Hold && contact) current = contact.ObservedAt >= order.ObservedAt;
if (current) accepted = m_Convoys.ApplyCombatOrder(m_ConvoyTask, order, now);
}
if (accepted) m_Journal.Write("CONVOY ORDER DELIVERED", "original crew applied radio travel command", order.CommandId);
else if (order) m_Journal.Write("CONVOY ORDER REJECTED", "stale evidence or unavailable original crew; current order retained", order.CommandId);
unit.CancelPending();
}
protected void Assign(float now)
{
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Get("supportSquadLimit"));
@@ -832,6 +919,7 @@ class ERR_HQ : ERR_CommandService
int bestBearing = -1;
foreach (ERR_HQUnit unit : m_Units)
{
if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
bool busy = unit.Assignment || unit.PendingOrder || unit.Downlink || unit.PlanFighting;
if (!unit.Radio || !ERR_HQPursuitRules.CanCommit(unit.Group != null, unit.PlanEligible, busy, unit.HomeGuard, HomeAvailable(unit), assigned, limit)) continue;
vector position = unit.PlanPosition;
@@ -895,7 +983,8 @@ class ERR_HQ : ERR_CommandService
int delivered = unit.Downlink.Advance(now, m_Journal);
if (delivered == 1)
{
if (unit.PendingCheckin) { unit.NextStatus = now; unit.CancelPending(); }
if (unit.PendingConvoyOrder) DeliverConvoyOrder(unit, now);
else if (unit.PendingCheckin) { unit.NextStatus = now; unit.CancelPending(); }
else DeliverOrder(unit, now);
}
else if (delivered < 0) LostOrder(unit, now);
@@ -903,7 +992,7 @@ class ERR_HQ : ERR_CommandService
if (!unit.Group && !unit.Uplink) { ReleaseUnit(unit, now); m_Units.Remove(i); continue; }
QueueReports(unit, now);
}
if (now >= m_NextAssignment) { Assign(now); m_NextAssignment = now + 5000; }
if (now >= m_NextAssignment) { PlanConvoy(now); Assign(now); m_NextAssignment = now + 5000; }
m_Logistics.Tick(now);
m_Mortars.Tick(now);
m_Journal.Flush();
@@ -25,8 +25,29 @@ class ERR_HQAdminProvider : ERR_AdminProvider
m_HQ.Journal().Read(after, lines, cursor, limit);
return true;
}
// A saved chance changes only future individual startup rolls. It cannot
// reroll registered soldiers or remove/replenish an issued physical launcher.
protected void StingerSetting(ERR_GameMode mode, array<string> parts, array<string> replies)
{
string key = "stingerChanceOneIn";
string effect = "Only eligible actors which have not rolled yet use this chance; no reroll, removal or replenishment of existing equipment.";
string range = "0 off, 1 every eligible soldier, 2..10000 one in N";
if (parts.Count() == 3 && parts[2] == "show")
{ replies.Insert(key + "=" + mode.Settings().Get(key).ToString() + ". " + effect); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && mode.Settings().Set(key, value))
replies.Insert(key + "=" + value.ToString() + " saved. " + effect);
else if (parts.Count() == 3 && parts[2] == "help") replies.Insert(range + ". " + effect);
else replies.Insert("Invalid value or settings could not be saved; previous setting retained. " + range + ". " + effect);
}
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
{
if (arguments.IndexOf("hq stinger ") == 0)
{
if (!m_HQ) { replies.Insert("HQ unavailable."); return true; }
array<string> parts = {}; arguments.Split(" ", parts, true);
StingerSetting(mode, parts, replies); return true;
}
if (arguments != "status" && arguments != "hq show" && arguments != "hq log" && arguments != "hq memory" && arguments != "hq convoy show" && arguments != "hq convoy start") return false;
if (!m_HQ) { replies.Insert("HQ unavailable."); return true; }
if (arguments == "hq convoy start")
@@ -0,0 +1,193 @@
// Detached counts of compatible small-arms magazines/chambers and separately
// supported launcher magazines. Unknown inventories are never reported empty.
class ERR_HQAmmoSnapshot
{
bool Complete;
bool LauncherComplete;
int Members;
int SmallArmsMuzzles;
int SmallArmsRounds;
int SmallArmsMagazines;
int SmallArmsChambers;
int LauncherRounds;
int LauncherMagazines;
int LauncherChambers;
bool KnownSmallArmsEmpty()
{
return Complete && Members > 0 && SmallArmsMuzzles > 0 && SmallArmsRounds == 0 && SmallArmsChambers == 0;
}
ERR_HQAmmoSnapshot Snapshot()
{
ERR_HQAmmoSnapshot copy = new ERR_HQAmmoSnapshot();
copy.Complete = Complete; copy.LauncherComplete = LauncherComplete; copy.Members = Members;
copy.SmallArmsMuzzles = SmallArmsMuzzles; copy.SmallArmsRounds = SmallArmsRounds;
copy.SmallArmsMagazines = SmallArmsMagazines; copy.SmallArmsChambers = SmallArmsChambers;
copy.LauncherRounds = LauncherRounds; copy.LauncherMagazines = LauncherMagazines;
copy.LauncherChambers = LauncherChambers;
return copy;
}
bool Valid(int living)
{
if (Members < 0 || Members > living || living > 32 || (Complete && Members != living)) return false;
if (SmallArmsMuzzles < 0 || SmallArmsMuzzles > 1024) return false;
if (SmallArmsRounds < 0 || SmallArmsRounds > 4096000) return false;
if (SmallArmsMagazines < 0 || SmallArmsMagazines > 4096) return false;
if (SmallArmsChambers < 0 || SmallArmsChambers > 4096) return false;
if (LauncherRounds < 0 || LauncherRounds > 4096000) return false;
if (LauncherMagazines < 0 || LauncherMagazines > 4096) return false;
return LauncherChambers >= 0 && LauncherChambers <= 4096;
}
}
class ERR_HQAmmoSampler
{
protected static int Category(BaseWeaponComponent weapon, BaseMuzzleComponent muzzle)
{
if (!weapon || !muzzle) return 0;
EWeaponType kind = weapon.GetWeaponType();
EMuzzleType muzzleKind = muzzle.GetMuzzleType();
if (muzzleKind == EMuzzleType.MT_RPGMuzzle || muzzleKind == EMuzzleType.MT_UGLMuzzle
|| kind == EWeaponType.WT_ROCKETLAUNCHER || kind == EWeaponType.WT_GRENADELAUNCHER) return 2;
if (kind == EWeaponType.WT_RIFLE || kind == EWeaponType.WT_SNIPERRIFLE
|| kind == EWeaponType.WT_MACHINEGUN || kind == EWeaponType.WT_HANDGUN) return 1;
return 0;
}
protected static bool Compatible(BaseMagazineComponent magazine, array<BaseMuzzleComponent> muzzles)
{
if (!magazine || !magazine.GetMagazineWell()) return false;
foreach (BaseMuzzleComponent muzzle : muzzles)
if (muzzle.GetMagazineWell() && muzzle.GetMagazineWell().Type() == magazine.GetMagazineWell().Type()) return true;
return false;
}
// Native magazine search permits derived wells, while the reload UI uses exact
// types. Keep exact counts, but never infer empty from uncertain derived stock.
protected static bool InheritedCandidate(BaseMagazineComponent magazine, array<BaseMuzzleComponent> muzzles)
{
if (!magazine || !magazine.GetMagazineWell()) return false;
foreach (BaseMuzzleComponent muzzle : muzzles)
{
if (!muzzle.GetMagazineWell()) continue;
if (magazine.GetMagazineWell().Type() == muzzle.GetMagazineWell().Type()) continue;
if (magazine.GetMagazineWell().IsInherited(muzzle.GetMagazineWell().Type())) return true;
}
return false;
}
// Caller must enumerate the immutable own roster. No group discovery, position,
// spawning, ammunition mutation, vehicle weapons or virtual inventory work.
static void Inspect(IEntity actor, SCR_AIGroup group, ERR_HQAmmoSnapshot result)
{
if (!result) return;
if (!Replication.IsServer() || !group || group.IsSlave() || group.GetPlayerCount(true) > 0
|| !ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group)
{ result.Complete = false; result.LauncherComplete = false; return; }
if (result.Members >= 32) { result.Complete = false; result.LauncherComplete = false; return; }
result.Members++;
InventoryStorageManagerComponent inventory;
inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
BaseWeaponManagerComponent manager;
manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
if (!inventory || !manager) { result.Complete = false; result.LauncherComplete = false; return; }
array<IEntity> raw = {}; inventory.GetItems(raw);
array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);
if (raw.Count() > 128 || weapons.Count() > 8)
{ result.Complete = false; result.LauncherComplete = false; return; }
array<IEntity> items = {};
foreach (IEntity item : raw)
{
if (!item) { result.Complete = false; result.LauncherComplete = false; continue; }
if (!items.Contains(item)) items.Insert(item);
}
array<BaseMuzzleComponent> small = {};
array<BaseMuzzleComponent> launchers = {};
array<IEntity> seenWeapons = {};
foreach (IEntity weaponEntity : weapons)
{
if (!weaponEntity) { result.Complete = false; result.LauncherComplete = false; continue; }
if (seenWeapons.Contains(weaponEntity)) continue;
seenWeapons.Insert(weaponEntity);
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(weaponEntity.FindComponent(BaseWeaponComponent));
if (!weapon) { result.Complete = false; result.LauncherComplete = false; continue; }
// Throwables have no relevant magazine well; smoke/grenades do not pretend to be rifle ammunition.
if (weapon.GetWeaponType() == EWeaponType.WT_FRAGGRENADE
|| weapon.GetWeaponType() == EWeaponType.WT_SMOKEGRENADE) continue;
bool launcherWeapon = weapon.GetWeaponType() == EWeaponType.WT_ROCKETLAUNCHER
|| weapon.GetWeaponType() == EWeaponType.WT_GRENADELAUNCHER;
array<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
if (muzzles.IsEmpty() || muzzles.Count() > 4)
{
if (!launcherWeapon) result.Complete = false;
result.LauncherComplete = false;
continue;
}
foreach (BaseMuzzleComponent muzzle : muzzles)
{
if (!muzzle)
{
if (!launcherWeapon) result.Complete = false;
result.LauncherComplete = false;
continue;
}
if (small.Contains(muzzle) || launchers.Contains(muzzle)) continue;
int category = Category(weapon, muzzle);
if (category == 0) { result.Complete = false; result.LauncherComplete = false; continue; }
if (category == 1) { small.Insert(muzzle); result.SmallArmsMuzzles++; }
else launchers.Insert(muzzle);
if (!muzzle.GetMagazineWell())
{
if (category == 1) result.Complete = false;
else result.LauncherComplete = false; // loose launcher projectiles are not compatible-magazine counts
}
int barrels = muzzle.GetBarrelsCount();
if (barrels < 1 || barrels > 4)
{
if (category == 1) result.Complete = false;
else result.LauncherComplete = false;
continue;
}
for (int barrel = 0; barrel < barrels; barrel++)
{
if (!muzzle.IsBarrelChambered(barrel)) continue;
if (category == 1) result.SmallArmsChambers++;
else result.LauncherChambers++;
}
BaseMagazineComponent loaded = muzzle.GetMagazine();
if (loaded && (!loaded.GetOwner() || !loaded.GetMagazineWell()))
{
if (category == 1) result.Complete = false;
else result.LauncherComplete = false;
}
if (loaded && loaded.GetOwner() && !items.Contains(loaded.GetOwner())) items.Insert(loaded.GetOwner());
}
}
// Physical identity deduplicates loaded/carried enumeration and shared compatible weapon wells.
if (items.Count() > 128) { result.Complete = false; result.LauncherComplete = false; return; }
foreach (IEntity item : items)
{
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent));
if (!magazine) continue;
if (!magazine.GetMagazineWell())
{
// Stock reload and magazine-search predicates reject a candidate with no well.
// It cannot replenish our fully inspected well-bearing small arms. A loaded
// no-well small-arms magazine was already rejected in its muzzle above.
result.LauncherComplete = false;
continue;
}
if (InheritedCandidate(magazine, small)) result.Complete = false;
if (InheritedCandidate(magazine, launchers)) result.LauncherComplete = false;
bool rifle = Compatible(magazine, small);
bool launcher = Compatible(magazine, launchers);
if (!rifle && !launcher) continue;
int rounds = magazine.GetAmmoCount();
int capacity = magazine.GetMaxAmmoCount();
if ((rifle && launcher) || rounds < 0 || capacity < 1 || capacity > 1000 || rounds > capacity)
{
if (rifle) result.Complete = false;
if (launcher) result.LauncherComplete = false;
continue;
}
if (rifle) { result.SmallArmsRounds += rounds; result.SmallArmsMagazines++; }
else { result.LauncherRounds += rounds; result.LauncherMagazines++; }
}
}
}
@@ -103,6 +103,8 @@ class ERR_HQStatusSampler
vector position = m_Group.GetOrigin();
if (!ERR_HQPursuitRules.PositionValid(position)) return null;
ERR_HQStatusReport report = new ERR_HQStatusReport();
report.AmmoState = new ERR_HQAmmoSnapshot();
report.AmmoState.Complete = true; report.AmmoState.LauncherComplete = true;
report.SourceId = m_SourceId; report.MessageId = messageId; report.Position = position;
report.Strength = m_Strength; report.ConfirmedLosses = m_Deaths; report.ObservedAt = now;
for (int slot = 0; slot < m_Dead.Count(); slot++)
@@ -112,8 +114,10 @@ class ERR_HQStatusSampler
if (m_Dead[i] || m_Released[i]) continue;
IEntity actor = m_Members[i];
if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Group
|| SCR_FactionManager.SGetFaction(actor) != m_Faction) continue;
|| SCR_FactionManager.SGetFaction(actor) != m_Faction)
{ report.AmmoState.Complete = false; report.AmmoState.LauncherComplete = false; continue; }
report.Count++;
ERR_HQAmmoSampler.Inspect(actor, m_Group, report.AmmoState);
if (ERR_Actors.CanFight(actor)) report.Fit++;
}
m_LastSampleAt = now;
+14
View File
@@ -11,6 +11,7 @@ class ERR_HQStatusReport
int Fit;
int ConfirmedLosses;
ref array<int> DeathSlots = {};
ref ERR_HQAmmoSnapshot AmmoState;
float ObservedAt = -1;
}
@@ -23,6 +24,7 @@ class ERR_HQWatchRules
if (!ERR_HQPursuitRules.PositionValid(report.Position) || !ERR_HQPursuitRules.Finite(report.ObservedAt)
|| !ERR_HQPursuitRules.Finite(now) || report.ObservedAt < 0 || now < report.ObservedAt
|| now - report.ObservedAt > 120000) return false;
if (report.AmmoState && !report.AmmoState.Valid(report.Count)) return false;
return report.Strength > 0 && report.Strength <= 32 && report.Count >= 0
&& report.Count <= report.Strength && report.Fit >= 0 && report.Fit <= report.Count
&& report.ConfirmedLosses >= 0 && report.ConfirmedLosses <= report.Strength - report.Count;
@@ -62,6 +64,7 @@ class ERR_HQWatchEntry
int Count = -1;
int Fit = -1;
int ConfirmedLosses;
ref ERR_HQAmmoSnapshot AmmoState;
ref ERR_HQDeliveredCasualties Casualties = new ERR_HQDeliveredCasualties();
float LastBodyDeliveredAt = -1;
float ObservedAt = -1;
@@ -87,6 +90,15 @@ class ERR_HQWatch
}
int ConfirmedLosses() { return m_ConfirmedLosses; }
int SquadLimit(int baseline) { return ERR_HQWatchRules.SquadLimit(baseline, m_ConfirmedLosses); }
// Conservative new-task veto, never a live inventory query or an active-order cancellation.
// A fresh complete own-roster check-in is required; silence or new bodily losses make no new ammo claims.
bool ArmedSearchBlocked(string sourceId, float now)
{
ERR_HQWatchEntry entry = Find(sourceId);
return entry && !entry.Closed && entry.AmmoState && entry.AmmoState.Members == entry.Count
&& ERR_HQPursuitRules.Finite(now) && entry.ObservedAt >= 0 && now >= entry.ObservedAt
&& now - entry.ObservedAt <= 180000 && entry.AmmoState.KnownSmallArmsEmpty();
}
protected ERR_HQWatchEntry Create(string sourceId, int strength)
{
// Closed identities remain tombstones: replays cannot recreate a formation.
@@ -130,6 +142,8 @@ class ERR_HQWatch
if (!entry.Casualties.AcceptStatus(report.DeathSlots, report.ConfirmedLosses, report.Count)) return false;
entry.MessageId = report.MessageId; entry.Position = report.Position;
entry.Count = report.Count; entry.Fit = report.Fit;
entry.AmmoState = null;
if (report.AmmoState) entry.AmmoState = report.AmmoState.Snapshot();
entry.ObservedAt = report.ObservedAt; entry.LastDeliveredAt = now;
entry.AskedAt = -1; entry.NextAttempt = 0; entry.Investigating = false;
ApplyLosses(entry);