Reconcile radio-delivered body and status casualties by original roster identity
This commit is contained in:
@@ -296,7 +296,7 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!record || !record.Physical || record.Eliminated) return;
|
||||
unit.ForceSourceId = record.SourceId;
|
||||
if (!unit.StatusSampler.Bound()) unit.StatusSampler.Bind(unit.Group, record.SourceId, record.InitialStrength, m_Mode.EnemyFaction());
|
||||
m_Bodies.RegisterGroup(unit.Group);
|
||||
if (unit.StatusSampler.Bound()) m_Bodies.RegisterGroup(unit.Group, unit.StatusSampler);
|
||||
}
|
||||
override void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
@@ -541,9 +541,12 @@ class ERR_HQ : ERR_CommandService
|
||||
protected void Accept(ERR_HQReport report, float now)
|
||||
{
|
||||
if (!report || !ERR_MissionRules.ContactFresh(report.ObservedAt, now)) return;
|
||||
// Delivered scalar body evidence survives native corpse cleanup during radio transit.
|
||||
// Memory still applies its own entity-identity rules; casualty accounting needs no live target.
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.BODY && m_Watch.IngestBody(report, now))
|
||||
m_Bodies.ConfirmDelivered(report.Target);
|
||||
if (!m_Memory.Ingest(report, now)) return;
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.SIGHTING) m_Coverage.Forget(report.Position);
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.BODY) m_Bodies.ConfirmDelivered(report.Target);
|
||||
m_Journal.Write("BELIEF", string.Format("delivered report M%1 retained; tracks=%2", report.MessageId, m_Memory.Count()), report.MessageId);
|
||||
// A reported corpse or distress call can justify investigation, never fire.
|
||||
if (ERR_HQMemoryRules.Kind(report) != ERR_HQMemoryKind.SIGHTING) return;
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
class ERR_HQBodyCandidate
|
||||
{
|
||||
IEntity Corpse;
|
||||
ref ERR_HQStatusSampler Roster;
|
||||
string SourceId;
|
||||
int Slot = -1;
|
||||
int Strength;
|
||||
bool Confirmed;
|
||||
float ReportAfter;
|
||||
}
|
||||
@@ -67,9 +71,9 @@ class ERR_HQBodyObserver
|
||||
for (int i = m_Groups.Count() - 1; i >= 0; i--)
|
||||
if (!m_Groups[i].Group) m_Groups.Remove(i);
|
||||
}
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
void RegisterGroup(SCR_AIGroup group, ERR_HQStatusSampler sampler)
|
||||
{
|
||||
if (!Replication.IsServer() || !OwnedPhysical(group)) return;
|
||||
if (!Replication.IsServer() || !OwnedPhysical(group) || !sampler || !sampler.OwnsRoster(group)) return;
|
||||
Prune();
|
||||
bool known;
|
||||
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) known = true;
|
||||
@@ -79,17 +83,18 @@ class ERR_HQBodyObserver
|
||||
ERR_HQBodyScan created = new ERR_HQBodyScan(); created.Group = group;
|
||||
m_Groups.Insert(created);
|
||||
}
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
for (int member = 0; member < agents.Count() && member < 32; member++)
|
||||
// Slot identity comes only from the immutable sampler bind, never current agent order.
|
||||
for (int member = 0; member < sampler.InitialStrength(); member++)
|
||||
{
|
||||
AIAgent agent = agents[member];
|
||||
if (!agent || !agent.GetControlledEntity()) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
IEntity actor = sampler.InitialMember(member);
|
||||
if (!sampler.CanAttribute(member, actor)) continue;
|
||||
bool found;
|
||||
foreach (ERR_HQBodyCandidate entry : m_Roster) if (entry.Corpse == actor) found = true;
|
||||
if (found) continue;
|
||||
if (m_Roster.Count() >= 128) { Cap(); return; }
|
||||
ERR_HQBodyCandidate candidate = new ERR_HQBodyCandidate(); candidate.Corpse = actor;
|
||||
candidate.Roster = sampler; candidate.SourceId = sampler.InitialSourceId();
|
||||
candidate.Slot = member; candidate.Strength = sampler.InitialStrength();
|
||||
m_Roster.Insert(candidate);
|
||||
}
|
||||
}
|
||||
@@ -171,6 +176,7 @@ class ERR_HQBodyObserver
|
||||
if (state.Cursor >= count) state.Cursor = 0;
|
||||
ERR_HQBodyCandidate candidate = m_Bodies[state.Cursor++];
|
||||
if (!candidate.Corpse || candidate.Confirmed || now < candidate.ReportAfter || !Dead(candidate.Corpse)) continue;
|
||||
if (!candidate.Roster || !candidate.Roster.CanAttribute(candidate.Slot, candidate.Corpse)) continue;
|
||||
for (int member = 0; member < agents.Count() && member < 32; member++)
|
||||
{
|
||||
if (m_Traces <= 0) return false;
|
||||
@@ -181,6 +187,8 @@ class ERR_HQBodyObserver
|
||||
report = new ERR_HQReport();
|
||||
report.Target = candidate.Corpse; report.Source = group;
|
||||
report.Position = position; report.ObservedAt = now; report.TravelMode = "body";
|
||||
report.CasualtySourceId = candidate.SourceId; report.CasualtySlot = candidate.Slot;
|
||||
report.CasualtyStrength = candidate.Strength;
|
||||
// Local throttling only. A lost radio call can be retried after fresh LOS.
|
||||
candidate.ReportAfter = now + 15000;
|
||||
return true;
|
||||
@@ -188,7 +196,7 @@ class ERR_HQBodyObserver
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Call only after a radio-delivered body snapshot was accepted by HQ memory.
|
||||
// Call only after the radio-delivered immutable body attribution was accepted by HQ.
|
||||
void ConfirmDelivered(IEntity body)
|
||||
{
|
||||
if (!Replication.IsServer() || !body) return;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Delivered initial-roster slot identities only. No native actor/position access.
|
||||
// One instance belongs to one immutable formation SourceId in the watch roster.
|
||||
class ERR_HQDeliveredCasualties
|
||||
{
|
||||
protected int m_Strength;
|
||||
protected ref array<int> m_StatusSlots = {};
|
||||
protected ref array<int> m_KnownSlots = {};
|
||||
bool Bind(int strength)
|
||||
{
|
||||
if (strength < 1 || strength > 32) return false;
|
||||
if (m_Strength != 0) return m_Strength == strength;
|
||||
m_Strength = strength;
|
||||
return true;
|
||||
}
|
||||
int Count() { return m_KnownSlots.Count(); }
|
||||
int StatusCount() { return m_StatusSlots.Count(); }
|
||||
bool Closed() { return m_Strength > 0 && Count() == m_Strength; }
|
||||
bool Known(int slot) { return m_KnownSlots.Contains(slot); }
|
||||
protected bool ValidSlot(int slot) { return m_Strength > 0 && slot >= 0 && slot < m_Strength; }
|
||||
bool AcceptBody(int slot)
|
||||
{
|
||||
if (!ValidSlot(slot)) return false;
|
||||
if (!m_KnownSlots.Contains(slot)) m_KnownSlots.Insert(slot);
|
||||
return true; // A repeated valid report is idempotent, including a status-known casualty.
|
||||
}
|
||||
bool AcceptStatus(array<int> slots, int reportedLosses, int reportedLiving)
|
||||
{
|
||||
if (m_Strength <= 0 || !slots || reportedLosses < 0 || reportedLosses > m_Strength
|
||||
|| reportedLiving < 0 || reportedLiving > m_Strength - reportedLosses || slots.Count() != reportedLosses)
|
||||
return false;
|
||||
array<int> unique = {};
|
||||
foreach (int slot : slots)
|
||||
{
|
||||
if (!ValidSlot(slot) || unique.Contains(slot)) return false;
|
||||
unique.Insert(slot);
|
||||
}
|
||||
// A later local cumulative status cannot resurrect a previously status-confirmed member.
|
||||
// Body-only knowledge is intentionally not required in an uninformed caller's status.
|
||||
foreach (int previous : m_StatusSlots) if (!unique.Contains(previous)) return false;
|
||||
m_StatusSlots.Clear();
|
||||
foreach (int slot : unique)
|
||||
{
|
||||
m_StatusSlots.Insert(slot);
|
||||
if (!m_KnownSlots.Contains(slot)) m_KnownSlots.Insert(slot);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
int LivingUpperBound(int lastReportedLiving)
|
||||
{
|
||||
if (Closed()) return 0;
|
||||
if (lastReportedLiving < 0 || lastReportedLiving > m_Strength) return -1;
|
||||
return Math.Min(lastReportedLiving, m_Strength - Count());
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,10 @@ class ERR_HQReport
|
||||
bool Distress;
|
||||
int SeenEscapists = -1;
|
||||
string TravelMode = "unknown";
|
||||
// Original formation identity captured by the local observer, only used after radio delivery.
|
||||
string CasualtySourceId;
|
||||
int CasualtySlot = -1;
|
||||
int CasualtyStrength;
|
||||
string SeenWeapons = "unknown";
|
||||
float DetailsObservedAt = -1;
|
||||
|
||||
@@ -68,6 +72,9 @@ class ERR_HQReport
|
||||
copy.HasVelocity = HasVelocity;
|
||||
copy.SeenEscapists = SeenEscapists;
|
||||
copy.TravelMode = TravelMode;
|
||||
copy.CasualtySourceId = CasualtySourceId;
|
||||
copy.CasualtySlot = CasualtySlot;
|
||||
copy.CasualtyStrength = CasualtyStrength;
|
||||
copy.SeenWeapons = SeenWeapons;
|
||||
copy.DetailsObservedAt = DetailsObservedAt;
|
||||
return copy;
|
||||
|
||||
@@ -14,6 +14,30 @@ class ERR_HQStatusSampler
|
||||
protected ref array<bool> m_Released = {};
|
||||
protected ref array<AIAgent> m_Agents = {};
|
||||
bool Bound() { return m_Bound; }
|
||||
string InitialSourceId() { return m_SourceId; }
|
||||
int InitialStrength() { return m_Strength; }
|
||||
bool OwnsRoster(SCR_AIGroup group) { return m_Bound && group && group == m_Group; }
|
||||
IEntity InitialMember(int slot)
|
||||
{
|
||||
if (!m_Bound || slot < 0 || slot >= m_Members.Count()) return null;
|
||||
return m_Members[slot];
|
||||
}
|
||||
// Attribution retains the original slot even when native death detaches its agent.
|
||||
// A known transfer/player conversion permanently releases that slot, never rebinds it.
|
||||
bool CanAttribute(int slot, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || InitialMember(slot) != actor || m_Released[slot]) return false;
|
||||
if (ERR_Actors.Player(actor) || SCR_FactionManager.SGetFaction(actor) != m_Faction)
|
||||
{ m_Released[slot] = true; return false; }
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
if (!damage) return false;
|
||||
SCR_AIGroup actorGroup = ERR_Actors.Group(actor);
|
||||
// A retained foreign group proves transfer even if death preceded this tick.
|
||||
// A null group on a destroyed actor remains normal native corpse detachment.
|
||||
if ((actorGroup && actorGroup != m_Group) || (!damage.IsDestroyed() && actorGroup != m_Group))
|
||||
{ m_Released[slot] = true; return false; }
|
||||
return true;
|
||||
}
|
||||
protected bool Eligible()
|
||||
{
|
||||
return Replication.IsServer() && m_Bound && m_Group && m_Faction
|
||||
@@ -59,10 +83,12 @@ class ERR_HQStatusSampler
|
||||
{ m_Released[i] = true; continue; }
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
if (!damage) continue;
|
||||
SCR_AIGroup actorGroup = ERR_Actors.Group(actor);
|
||||
if (actorGroup && actorGroup != m_Group) { m_Released[i] = true; continue; }
|
||||
if (damage.IsDestroyed()) { m_Dead[i] = true; m_Deaths++; continue; }
|
||||
// A living member who left this group is no longer ours to account for.
|
||||
// Native death may detach a corpse, so group identity is checked after death.
|
||||
if (ERR_Actors.Group(actor) != m_Group) m_Released[i] = true;
|
||||
// A living detached member is no longer ours. A destroyed detached actor
|
||||
// was handled above without mistaking native corpse cleanup for transfer.
|
||||
if (actorGroup != m_Group) m_Released[i] = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -79,6 +105,8 @@ class ERR_HQStatusSampler
|
||||
ERR_HQStatusReport report = new ERR_HQStatusReport();
|
||||
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++)
|
||||
if (m_Dead[slot]) report.DeathSlots.Insert(slot);
|
||||
for (int i = 0; i < m_Members.Count(); i++)
|
||||
{
|
||||
if (m_Dead[i] || m_Released[i]) continue;
|
||||
|
||||
@@ -10,6 +10,7 @@ class ERR_HQStatusReport
|
||||
int Count;
|
||||
int Fit;
|
||||
int ConfirmedLosses;
|
||||
ref array<int> DeathSlots = {};
|
||||
float ObservedAt = -1;
|
||||
}
|
||||
|
||||
@@ -58,9 +59,11 @@ class ERR_HQWatchEntry
|
||||
int MessageId;
|
||||
vector Position;
|
||||
int Strength;
|
||||
int Count;
|
||||
int Fit;
|
||||
int Count = -1;
|
||||
int Fit = -1;
|
||||
int ConfirmedLosses;
|
||||
ref ERR_HQDeliveredCasualties Casualties = new ERR_HQDeliveredCasualties();
|
||||
float LastBodyDeliveredAt = -1;
|
||||
float ObservedAt = -1;
|
||||
float LastDeliveredAt = -1;
|
||||
float AskedAt = -1;
|
||||
@@ -84,44 +87,71 @@ class ERR_HQWatch
|
||||
}
|
||||
int ConfirmedLosses() { return m_ConfirmedLosses; }
|
||||
int SquadLimit(int baseline) { return ERR_HQWatchRules.SquadLimit(baseline, m_ConfirmedLosses); }
|
||||
protected ERR_HQWatchEntry Create(string sourceId, int strength)
|
||||
{
|
||||
// Closed identities remain tombstones: replays cannot recreate a formation.
|
||||
if (m_Entries.Count() >= 128)
|
||||
{
|
||||
if (!m_CapLogged) Print("Rereintroduced [watch]: 128-formation check-in cap reached.");
|
||||
m_CapLogged = true;
|
||||
return null;
|
||||
}
|
||||
ERR_HQWatchEntry entry = new ERR_HQWatchEntry();
|
||||
entry.SourceId = sourceId; entry.Strength = strength;
|
||||
entry.Casualties.Bind(strength);
|
||||
m_Entries.Insert(entry); m_BySource.Set(sourceId, entry);
|
||||
return entry;
|
||||
}
|
||||
protected void ApplyLosses(ERR_HQWatchEntry entry)
|
||||
{
|
||||
int known = entry.Casualties.Count();
|
||||
m_ConfirmedLosses += known - entry.ConfirmedLosses;
|
||||
entry.ConfirmedLosses = known;
|
||||
entry.Count = entry.Casualties.LivingUpperBound(entry.Count);
|
||||
if (entry.Count >= 0) entry.Fit = Math.Min(entry.Fit, entry.Count);
|
||||
entry.Closed = entry.Casualties.Closed();
|
||||
if (entry.Closed) entry.Fit = 0;
|
||||
}
|
||||
bool Ingest(ERR_HQStatusReport report, float now)
|
||||
{
|
||||
if (!ERR_HQWatchRules.Valid(report, now)) return false;
|
||||
ERR_HQWatchEntry entry = Find(report.SourceId);
|
||||
if (entry)
|
||||
if (entry && (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|
||||
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength)) return false;
|
||||
if (!entry)
|
||||
{
|
||||
if (report.MessageId <= entry.MessageId || report.ObservedAt <= entry.ObservedAt
|
||||
|| now < entry.LastDeliveredAt || report.Strength != entry.Strength
|
||||
|| report.ConfirmedLosses < entry.ConfirmedLosses) return false;
|
||||
// Validate slot payload before allocation: malformed snapshots cannot consume the cap.
|
||||
ERR_HQDeliveredCasualties validation = new ERR_HQDeliveredCasualties();
|
||||
if (!validation.Bind(report.Strength)
|
||||
|| !validation.AcceptStatus(report.DeathSlots, report.ConfirmedLosses, report.Count)) return false;
|
||||
entry = Create(report.SourceId, report.Strength);
|
||||
if (!entry) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep closed identities as well: replay cannot resurrect a retired patrol
|
||||
// or count the same cumulative casualty total again under its old source.
|
||||
if (m_Entries.Count() >= 128)
|
||||
{
|
||||
if (!m_CapLogged) Print("Rereintroduced [watch]: 128-formation check-in cap reached.");
|
||||
m_CapLogged = true;
|
||||
return false;
|
||||
}
|
||||
entry = new ERR_HQWatchEntry();
|
||||
entry.SourceId = report.SourceId;
|
||||
m_Entries.Insert(entry);
|
||||
m_BySource.Set(entry.SourceId, entry);
|
||||
}
|
||||
m_ConfirmedLosses += report.ConfirmedLosses - entry.ConfirmedLosses;
|
||||
entry.MessageId = report.MessageId;
|
||||
entry.Position = report.Position;
|
||||
entry.Strength = report.Strength;
|
||||
entry.Count = report.Count;
|
||||
entry.Fit = report.Fit;
|
||||
entry.ConfirmedLosses = report.ConfirmedLosses;
|
||||
entry.ObservedAt = report.ObservedAt;
|
||||
entry.LastDeliveredAt = now;
|
||||
entry.AskedAt = -1;
|
||||
entry.NextAttempt = 0;
|
||||
entry.Investigating = false;
|
||||
entry.Closed = entry.ConfirmedLosses == entry.Strength;
|
||||
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.ObservedAt = report.ObservedAt; entry.LastDeliveredAt = now;
|
||||
entry.AskedAt = -1; entry.NextAttempt = 0; entry.Investigating = false;
|
||||
ApplyLosses(entry);
|
||||
return true;
|
||||
}
|
||||
// Called only from delivered BODY intake; scalar evidence survives native target cleanup.
|
||||
// Corpse coordinates and body message IDs never replace a formation status/check-in.
|
||||
bool IngestBody(ERR_HQReport report, float now)
|
||||
{
|
||||
if (!report || report.TravelMode != "body" || report.Distress || report.MessageId <= 0
|
||||
|| report.CasualtySourceId.IsEmpty() || report.CasualtySourceId.Length() > 96
|
||||
|| report.CasualtyStrength < 1 || report.CasualtyStrength > 32
|
||||
|| report.CasualtySlot < 0 || report.CasualtySlot >= report.CasualtyStrength
|
||||
|| !ERR_HQPursuitRules.Finite(now) || !ERR_HQPursuitRules.Finite(report.ObservedAt)
|
||||
|| !ERR_HQPursuitRules.PositionValid(report.Position) || report.ObservedAt < 0
|
||||
|| now < report.ObservedAt || now - report.ObservedAt > 120000) return false;
|
||||
ERR_HQWatchEntry entry = Find(report.CasualtySourceId);
|
||||
if (entry && (entry.Strength != report.CasualtyStrength || now < entry.LastBodyDeliveredAt)) return false;
|
||||
if (!entry) entry = Create(report.CasualtySourceId, report.CasualtyStrength);
|
||||
if (!entry || !entry.Casualties.AcceptBody(report.CasualtySlot)) return false;
|
||||
entry.LastBodyDeliveredAt = now;
|
||||
ApplyLosses(entry);
|
||||
return true;
|
||||
}
|
||||
bool RequestDue(string sourceId, float now)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#ifdef WORKBENCH
|
||||
// Controlled scene setup only: reuse two already admitted formations, relocate
|
||||
// and kill one original non-radio member. Production LOS, snapshots and radio
|
||||
// delivery must establish knowledge; this is not organic patrol-discovery proof.
|
||||
class ERR_NativeHQBodySetup
|
||||
{
|
||||
ref ERR_HQUnit VictimUnit;
|
||||
ref ERR_HQUnit ObserverUnit;
|
||||
IEntity Actor;
|
||||
string SourceId;
|
||||
int InitialSlot;
|
||||
int InitialStrength;
|
||||
float SavedNextStatus;
|
||||
int StatusMessageId;
|
||||
float StatusObservedAt = -1;
|
||||
vector StatusPosition;
|
||||
}
|
||||
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected bool ERR_BodyFixtureHealthy(ERR_HQUnit unit)
|
||||
{
|
||||
if (!unit || !unit.Eligible(m_Mode) || !unit.StatusSampler.Bound() || !unit.Radio || unit.Uplink
|
||||
|| unit.PendingReport || unit.PendingStatus || unit.PendingSupplyStatus || unit.PendingSweep) return false;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!route || route.Nodes.IsEmpty()) return false;
|
||||
for (int i = 0; i < route.Nodes.Count(); i++)
|
||||
if (!route.Nodes[i] || !route.Nodes[i].Valid(i == route.Nodes.Count() - 1)) return false;
|
||||
return true;
|
||||
}
|
||||
ERR_NativeHQBodySetup ERR_BodyFixturePrepare(float now)
|
||||
{
|
||||
if (!ERR_NativeHQBodyProbe.Enabled()) return null;
|
||||
foreach (ERR_HQUnit observer : m_Units)
|
||||
{
|
||||
if (!observer.HomeGuard || !ERR_BodyFixtureHealthy(observer)) continue;
|
||||
bool originalObserver;
|
||||
for (int observerSlot = 0; observerSlot < observer.StatusSampler.InitialStrength(); observerSlot++)
|
||||
if (observer.StatusSampler.InitialMember(observerSlot) == observer.Radio
|
||||
&& observer.StatusSampler.CanAttribute(observerSlot, observer.Radio)) originalObserver = true;
|
||||
if (!originalObserver) continue;
|
||||
foreach (ERR_HQUnit victim : m_Units)
|
||||
{
|
||||
if (victim == observer || victim.StatusSampler.InitialStrength() < 2 || !ERR_BodyFixtureHealthy(victim))
|
||||
continue;
|
||||
string sourceId = victim.StatusSampler.InitialSourceId();
|
||||
ERR_HQWatchEntry status = m_Watch.Find(sourceId);
|
||||
if (status && status.ConfirmedLosses != 0) continue;
|
||||
for (int slot = 0; slot < victim.StatusSampler.InitialStrength(); slot++)
|
||||
{
|
||||
IEntity actor = victim.StatusSampler.InitialMember(slot);
|
||||
if (!actor || actor == victim.Radio || !ERR_Actors.CanFight(actor)
|
||||
|| !victim.StatusSampler.CanAttribute(slot, actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
|
||||
ERR_NativeHQBodySetup fixture = new ERR_NativeHQBodySetup();
|
||||
fixture.VictimUnit = victim; fixture.ObserverUnit = observer; fixture.Actor = actor;
|
||||
fixture.SourceId = sourceId; fixture.InitialSlot = slot;
|
||||
fixture.InitialStrength = victim.StatusSampler.InitialStrength();
|
||||
fixture.SavedNextStatus = victim.NextStatus;
|
||||
// Isolate the BODY uplink from the deceased member's own group check-in.
|
||||
// No report/route is discarded: selection required that group to be idle.
|
||||
victim.NextStatus = now + 180000;
|
||||
if (status)
|
||||
{
|
||||
fixture.StatusMessageId = status.MessageId; fixture.StatusObservedAt = status.ObservedAt;
|
||||
fixture.StatusPosition = status.Position;
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
ERR_HQWatchEntry ERR_BodyFixtureWatch(string sourceId)
|
||||
{
|
||||
if (!ERR_NativeHQBodyProbe.Enabled()) return null;
|
||||
return m_Watch.Find(sourceId);
|
||||
}
|
||||
bool ERR_BodyFixtureContact(IEntity actor)
|
||||
{
|
||||
foreach (ERR_HQReport report : m_Contacts) if (report.Target == actor) return true;
|
||||
return false;
|
||||
}
|
||||
// Invoke the real production observer/queue method. It must do its own native LOS
|
||||
// and physical route lookup. The fixture does not construct or enqueue a report.
|
||||
ERR_HQUnit ERR_BodyFixturePending(ERR_NativeHQBodySetup fixture, float now)
|
||||
{
|
||||
if (!ERR_NativeHQBodyProbe.Enabled() || !fixture) return null;
|
||||
ObserveBody(fixture.ObserverUnit, now);
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.PendingReport && unit.Uplink && unit.PendingReport.Target == fixture.Actor
|
||||
&& unit.PendingReport.TravelMode == "body") return unit;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeHQBodyProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_NativeHQBodySetup m_Setup;
|
||||
protected ref ERR_HQReport m_Report;
|
||||
protected ref ERR_HQRoute m_Route;
|
||||
protected ref ERR_HQStatusReport m_Status;
|
||||
protected ref ERR_HQRoute m_StatusRoute;
|
||||
protected float m_Deadline;
|
||||
protected float m_StableUntil;
|
||||
protected int m_Step;
|
||||
protected bool m_Failed;
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-body";
|
||||
}
|
||||
void ERR_NativeHQBodyProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: HQ body " + detail); return true; }
|
||||
m_Failed = true; Print("ERR SMOKE FAIL: HQ body " + detail, LogLevel.ERROR); return false;
|
||||
}
|
||||
// Get(target) intentionally exposes SIGHTING only. Inspect the bounded detached
|
||||
// evaluations to locate this exact BODY report without changing memory semantics.
|
||||
protected ERR_HQBelief BodyBelief(ERR_HQ hq, IEntity actor, int messageId)
|
||||
{
|
||||
for (int i = 0; i < hq.Memory().Count(); i++)
|
||||
{
|
||||
ERR_HQBelief belief = hq.Memory().At(i);
|
||||
if (belief && belief.Kind == ERR_HQMemoryKind.BODY && belief.Target == actor
|
||||
&& belief.MessageId == messageId) return belief;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected bool NoLoss(ERR_HQ hq)
|
||||
{
|
||||
ERR_HQWatchEntry status = hq.ERR_BodyFixtureWatch(m_Setup.SourceId);
|
||||
return !status || status.ConfirmedLosses == 0;
|
||||
}
|
||||
int Tick(ERR_HQ hq, float now)
|
||||
{
|
||||
if (!Enabled() || !hq || !m_Mode || m_Failed) return -1;
|
||||
if (m_Deadline == 0) m_Deadline = now + 150000;
|
||||
if (now > m_Deadline) { Check(false, "bounded native LOS/radio fixture deadline"); return -1; }
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
||||
m_Setup = hq.ERR_BodyFixturePrepare(now);
|
||||
if (!m_Setup) return 0;
|
||||
if (!Check(m_Setup.VictimUnit.Group != m_Setup.ObserverUnit.Group
|
||||
&& ERR_Actors.Group(m_Setup.Actor) == m_Setup.VictimUnit.Group,
|
||||
"two distinct existing physical formations and immutable victim slot selected")) return -1;
|
||||
// Explicit fixture arrangement. One native corpse, no new actor/group/item.
|
||||
vector position = ERR_Actors.Ground(m_Setup.ObserverUnit.Radio.GetOrigin() + "0 0 5");
|
||||
m_Setup.Actor.SetOrigin(position);
|
||||
SCR_CharacterDamageManagerComponent damage;
|
||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_Setup.Actor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!Check(damage != null, "original fixture actor has native character damage")) return -1;
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.Kill(Instigator.CreateInstigator(m_Setup.Actor));
|
||||
if (!Check(NoLoss(hq), "local fixture death has not informed HQ")) return -1;
|
||||
m_Step = 1; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!NoLoss(hq)) { Check(false, "uncaptured delivery preceded pending-payload inspection"); return -1; }
|
||||
ERR_HQUnit reporter = hq.ERR_BodyFixturePending(m_Setup, now);
|
||||
if (!reporter) return 0;
|
||||
m_Report = reporter.PendingReport.Snapshot(); m_Route = reporter.Uplink;
|
||||
if (!Check(m_Report.MessageId > 0 && m_Report.Source == reporter.Group
|
||||
&& m_Report.Target == m_Setup.Actor && m_Report.CasualtySourceId == m_Setup.SourceId
|
||||
&& m_Report.CasualtySlot == m_Setup.InitialSlot && m_Report.CasualtyStrength == m_Setup.InitialStrength,
|
||||
"actual native LOS produced body snapshot with original formation/slot attribution")) return -1;
|
||||
if (!Check(m_Route.MessageId == m_Report.MessageId && !m_Route.Nodes.IsEmpty()
|
||||
&& m_Route.Nodes[0].Carrier == reporter.Radio && ERR_Actors.CanFight(reporter.Radio)
|
||||
&& !m_Report.HasVelocity && !m_Report.Distress && m_Report.TravelMode == "body",
|
||||
"living observer formation queued its actual radio route without target-motion privilege")) return -1;
|
||||
if (!Check(NoLoss(hq), "pending radio payload still gives HQ no casualty knowledge")) return -1;
|
||||
m_Step = 2; return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (m_Route.Failed) { Check(false, "actual body uplink failed before delivery"); return -1; }
|
||||
if (m_Route.Cursor < m_Route.Nodes.Count() || m_Route.Sending)
|
||||
{
|
||||
if (!NoLoss(hq)) { Check(false, "in-transit body report created early knowledge"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
ERR_HQWatchEntry status = hq.ERR_BodyFixtureWatch(m_Setup.SourceId);
|
||||
ERR_HQBelief belief = BodyBelief(hq, m_Setup.Actor, m_Report.MessageId);
|
||||
if (!Check(status && status.ConfirmedLosses == 1 && !status.Closed,
|
||||
"completed native radio uplink records exactly one original casualty")) return -1;
|
||||
if (!Check(status.MessageId == m_Setup.StatusMessageId && status.ObservedAt == m_Setup.StatusObservedAt
|
||||
&& status.Position == m_Setup.StatusPosition,
|
||||
"body delivery preserves the formation's independent status chronology and position")) return -1;
|
||||
if (!Check(belief && belief.Kind == ERR_HQMemoryKind.BODY && belief.MessageId == m_Report.MessageId
|
||||
&& !belief.HasVelocity && !hq.ERR_BodyFixtureContact(m_Setup.Actor),
|
||||
"delivered body is uncertain investigation memory without a firing contact")) return -1;
|
||||
// Resume a real surviving caller's own cumulative status for overlap proof.
|
||||
m_Setup.VictimUnit.NextStatus = now;
|
||||
m_Step = 3; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
ERR_HQUnit victim = m_Setup.VictimUnit;
|
||||
if (!victim.PendingStatus || !victim.Uplink) return 0;
|
||||
m_Status = victim.PendingStatus; m_StatusRoute = victim.Uplink;
|
||||
if (!Check(m_Status.SourceId == m_Setup.SourceId && m_Status.ConfirmedLosses == 1
|
||||
&& m_Status.DeathSlots.Count() == 1 && m_Status.DeathSlots[0] == m_Setup.InitialSlot,
|
||||
"real surviving caller independently reports the same original casualty slot")) return -1;
|
||||
m_Step = 4; return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (m_StatusRoute.Failed) { Check(false, "actual overlapping status uplink failed"); return -1; }
|
||||
if (m_StatusRoute.Cursor < m_StatusRoute.Nodes.Count() || m_StatusRoute.Sending) return 0;
|
||||
ERR_HQWatchEntry status = hq.ERR_BodyFixtureWatch(m_Setup.SourceId);
|
||||
if (!Check(status && status.MessageId == m_Status.MessageId && status.ConfirmedLosses == 1
|
||||
&& status.Count == m_Setup.InitialStrength - 1,
|
||||
"delivered cumulative status overlaps body evidence without double-counting")) return -1;
|
||||
m_StableUntil = now + 20000; m_Step = 5; return 0;
|
||||
}
|
||||
ERR_HQWatchEntry stable = hq.ERR_BodyFixtureWatch(m_Setup.SourceId);
|
||||
if (!stable || stable.ConfirmedLosses != 1 || hq.ERR_BodyFixtureContact(m_Setup.Actor))
|
||||
{ Check(false, "confirmed corpse lost idempotence or became a firing contact"); return -1; }
|
||||
if (now < m_StableUntil) return 0;
|
||||
Check(true, "confirmed corpse remains one loss with no firing contact after twenty further seconds");
|
||||
return 1;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Setup && m_Setup.VictimUnit && m_Step < 3)
|
||||
m_Setup.VictimUnit.NextStatus = m_Setup.SavedNextStatus;
|
||||
}
|
||||
void ~ERR_NativeHQBodyProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
@@ -17,6 +17,8 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestRadioNetwork;
|
||||
protected bool m_ERRTestHQResponse;
|
||||
protected bool m_ERRTestHQMemory;
|
||||
protected bool m_ERRTestHQBody;
|
||||
protected ref ERR_NativeHQBodyProbe m_ERRHQBodyProbe;
|
||||
protected bool m_ERRTestConvoy;
|
||||
protected ref ERR_NativeConvoyProbe m_ERRConvoyProbe;
|
||||
protected ref ERR_NativeHQMemoryProbe m_ERRHQMemoryProbe;
|
||||
@@ -59,6 +61,7 @@ modded class ERR_GameMode
|
||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestHQMemory = outcome == "hq-memory";
|
||||
m_ERRTestHQBody = outcome == "hq-body";
|
||||
m_ERRTestConvoy = outcome == "convoy";
|
||||
m_ERRTestMortar = outcome == "mortar-inventory";
|
||||
m_ERRTestMapOwnership = outcome == "map-ownership";
|
||||
@@ -72,10 +75,10 @@ modded class ERR_GameMode
|
||||
m_ERRTestMortarSalvo = outcome == "mortar-salvo" || outcome == "mortar-refresh";
|
||||
m_ERRTestMortarTransfer = outcome == "mortar-transfer";
|
||||
m_ERRTestPatrolHQ = outcome == "patrol-hq";
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestConvoy;
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
if (m_ERRTestConvoy) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 540000;
|
||||
if (m_ERRTestHQMemory) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
|
||||
if (m_ERRTestHQMemory || m_ERRTestHQBody) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;
|
||||
if (m_ERRTestAircraftControl || m_ERRTestAircraftRoute || m_ERRTestAircraftField) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
|
||||
if (m_ERRTestMortarTransfer) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
@@ -514,6 +517,13 @@ modded class ERR_GameMode
|
||||
m_ERRSmokeStep = 106;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestHQBody)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRHQBodyProbe = new ERR_NativeHQBodyProbe(this);
|
||||
m_ERRSmokeStep = 107;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestHQMemory)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
@@ -694,6 +704,19 @@ modded class ERR_GameMode
|
||||
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
||||
m_ERRSmokeWait = now + 4000;
|
||||
}
|
||||
else if (m_ERRSmokeStep == 107)
|
||||
{
|
||||
int result = m_ERRHQBodyProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||
if (result == 0) return;
|
||||
m_ERRHQBodyProbe.Stop();
|
||||
if (!ERR_Check(result == 1, "native visual body discovery, delivered casualty and status overlap")) return;
|
||||
m_ERRSmokeStep = 9;
|
||||
SCR_CharacterDamageManagerComponent damage;
|
||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
||||
m_ERRSmokeWait = now + 4000;
|
||||
}
|
||||
else if (m_ERRSmokeStep == 106)
|
||||
{
|
||||
int result = m_ERRConvoyProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||
|
||||
Reference in New Issue
Block a user