285 lines
15 KiB
C
285 lines
15 KiB
C
#ifdef WORKBENCH
|
|
// Read-only evidence hooks. Every production method delegates unchanged.
|
|
modded class ERR_HQ
|
|
{
|
|
void ERR_ResponseUnits(out array<ERR_HQUnit> units)
|
|
{
|
|
units.Clear();
|
|
if (!ERR_NativeHQResponseProof.Enabled()) return;
|
|
foreach (ERR_HQUnit unit : m_Units) units.Insert(unit);
|
|
}
|
|
protected override void Observe(ERR_HQUnit unit, float now)
|
|
{
|
|
ERR_HQReport previous = unit.PendingReport;
|
|
super.Observe(unit, now);
|
|
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
|
if (proof && unit.PendingReport && unit.PendingReport != previous) proof.Observed(this, unit, now);
|
|
}
|
|
protected override void Accept(ERR_HQReport report, float now)
|
|
{
|
|
super.Accept(report, now);
|
|
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
|
if (!proof || !report) return;
|
|
foreach (ERR_HQReport accepted : m_Contacts)
|
|
if (accepted.MessageId == report.MessageId) { proof.Accepted(this, accepted, now); break; }
|
|
}
|
|
protected override void DeliverOrder(ERR_HQUnit unit, float now)
|
|
{
|
|
ERR_HQReport order = unit.PendingOrder;
|
|
ERR_HQRoute route = unit.Downlink;
|
|
super.DeliverOrder(unit, now);
|
|
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
|
if (proof && order && route && unit.Assignment == order && unit.Waypoint) proof.Delivered(this, unit, route, now);
|
|
}
|
|
}
|
|
modded class ERR_HQJournal
|
|
{
|
|
override void Write(string kind, string message, int messageId = 0)
|
|
{
|
|
super.Write(kind, message, messageId);
|
|
ERR_NativeHQResponseProof proof = ERR_NativeHQResponseProof.Active();
|
|
if (proof) proof.JournalEvent(this, kind, message, messageId);
|
|
}
|
|
}
|
|
modded class SCR_AIGroupUtilityComponent
|
|
{
|
|
IEntity ERR_ResponseVisualObserver(ERR_HQReport report, float now)
|
|
{
|
|
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
|
|
{
|
|
if (!info || !info.m_Perception) continue;
|
|
AIAgent agent = AIAgent.Cast(info.GetOwner());
|
|
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) continue;
|
|
array<BaseTarget> targets = {}; info.m_Perception.GetTargetsList(targets, ETargetCategory.ENEMY);
|
|
foreach (BaseTarget target : targets)
|
|
{
|
|
ERR_HQVisualContact sample = ERR_HQObservation.Capture(target, now);
|
|
if (sample && sample.Entity == report.Target && sample.Escapist && Math.AbsFloat(sample.ObservedAt - report.ObservedAt) < 1 && vector.Distance(sample.Position, report.Position) < 0.1) return agent.GetControlledEntity();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
class ERR_NativeResponseUnit
|
|
{
|
|
ERR_HQUnit Unit;
|
|
SCR_AIGroup Group;
|
|
IEntity Radio;
|
|
ref array<IEntity> Actors = {};
|
|
}
|
|
class ERR_NativeResponseJournal
|
|
{
|
|
bool Report;
|
|
bool Transmit;
|
|
bool HQReceive;
|
|
bool HQTransmit;
|
|
bool UnitReceive;
|
|
bool Delivered;
|
|
}
|
|
class ERR_NativeResponseLineage
|
|
{
|
|
ref ERR_HQReport Report;
|
|
ERR_HQUnit Source;
|
|
IEntity Observer;
|
|
IEntity SourceRadio;
|
|
ref ERR_HQRoute Uplink;
|
|
bool Accepted;
|
|
float AcceptedAt;
|
|
ERR_HQUnit Recipient;
|
|
IEntity RecipientRadio;
|
|
int DeliveryId;
|
|
AIWaypoint Waypoint;
|
|
IEntity MovingActor;
|
|
vector DeliveredOrigin;
|
|
float DeliveredAt;
|
|
}
|
|
class ERR_NativeHQResponseProof
|
|
{
|
|
protected static ERR_NativeHQResponseProof s_Active;
|
|
protected ERR_GameMode m_Mode;
|
|
protected ERR_HQ m_HQ;
|
|
protected IEntity m_Player;
|
|
protected SCR_PlayerController m_Controller;
|
|
protected ERR_PlayerLife m_Life;
|
|
protected ERR_HQUnit m_Source;
|
|
protected DamageManagerComponent m_Damage;
|
|
protected bool m_DamageWasEnabled;
|
|
protected bool m_Protected;
|
|
protected bool m_Started;
|
|
protected bool m_Stopped;
|
|
protected bool m_Failed;
|
|
protected string m_Failure;
|
|
protected float m_Deadline;
|
|
protected float m_NextDiagnostic;
|
|
protected ref array<ref ERR_NativeResponseUnit> m_Units = {};
|
|
protected ref array<IEntity> m_Forces = {};
|
|
protected ref array<ref ERR_NativeResponseLineage> m_Reports = {};
|
|
protected ref map<int, ref ERR_NativeResponseJournal> m_Journal = new map<int, ref ERR_NativeResponseJournal>();
|
|
static bool Enabled()
|
|
{
|
|
string enabled, outcome;
|
|
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-response";
|
|
}
|
|
static ERR_NativeHQResponseProof Active() { return s_Active; }
|
|
void ERR_NativeHQResponseProof(ERR_GameMode mode, IEntity player)
|
|
{
|
|
m_Mode = mode; m_Player = player; m_HQ = ERR_HQ.Get(); m_Deadline = GetGame().GetWorld().GetWorldTime() + 140000;
|
|
if (Enabled() && !s_Active) s_Active = this;
|
|
}
|
|
protected bool Fail(string reason) { m_Failed = true; m_Failure = reason; return false; }
|
|
protected bool Check(bool condition, string detail)
|
|
{
|
|
if (condition) { Print("ERR SMOKE PASS: HQ response " + detail); return true; }
|
|
Print("ERR SMOKE FAIL: HQ response " + detail, LogLevel.ERROR); Stop(); return false;
|
|
}
|
|
protected ERR_NativeResponseUnit Original(ERR_HQUnit unit)
|
|
{
|
|
foreach (ERR_NativeResponseUnit original : m_Units) if (original.Unit == unit && original.Group == unit.Group) return original;
|
|
return null;
|
|
}
|
|
protected ERR_NativeResponseLineage Report(int id)
|
|
{
|
|
foreach (ERR_NativeResponseLineage report : m_Reports) if (report.Report.MessageId == id) return report;
|
|
return null;
|
|
}
|
|
protected bool Canonical()
|
|
{
|
|
return m_Player && m_Controller && m_Life && m_Mode.Players().CurrentActor(m_Player) == m_Life && m_Controller == GetGame().GetPlayerController() && m_Controller.GetControlledEntity() == m_Player && m_Life.Actor == m_Player && m_Life.Connected && m_Life.UsedLife;
|
|
}
|
|
protected bool UnchangedForces()
|
|
{
|
|
array<ERR_HQUnit> current = {}; m_HQ.ERR_ResponseUnits(current);
|
|
if (current.Count() != m_Units.Count()) return false;
|
|
foreach (ERR_HQUnit unit : current)
|
|
{
|
|
ERR_NativeResponseUnit original = Original(unit);
|
|
if (!original) return false;
|
|
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
|
if (agents.Count() != original.Actors.Count()) return false;
|
|
foreach (AIAgent agent : agents) if (!agent || !original.Actors.Contains(agent.GetControlledEntity())) return false;
|
|
}
|
|
array<IEntity> forces = {}; m_HQ.PhysicalForces(forces);
|
|
if (forces.Count() != m_Forces.Count()) return false;
|
|
foreach (IEntity entity : forces) if (!m_Forces.Contains(entity)) return false;
|
|
return true;
|
|
}
|
|
protected bool Arrange()
|
|
{
|
|
array<ERR_HQUnit> units = {}; m_HQ.ERR_ResponseUnits(units);
|
|
foreach (ERR_HQUnit unit : units)
|
|
{
|
|
if (!unit.Group || !unit.Utility || !unit.Radio || ERR_HQRadio.Capability(unit.Radio) != 2) return false;
|
|
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
|
if (agents.IsEmpty() || !unit.Group.IsExpandComplete()) return false;
|
|
ERR_NativeResponseUnit original = new ERR_NativeResponseUnit(); original.Unit = unit; original.Group = unit.Group; original.Radio = unit.Radio;
|
|
foreach (AIAgent agent : agents)
|
|
{
|
|
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
|
original.Actors.Insert(agent.GetControlledEntity());
|
|
}
|
|
m_Units.Insert(original);
|
|
if (!m_Source && !unit.ReservedForSupport) m_Source = unit;
|
|
}
|
|
if (!m_Source || m_Units.Count() < 2) return false;
|
|
m_HQ.PhysicalForces(m_Forces);
|
|
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); m_Life = m_Mode.Players().CurrentActor(m_Player);
|
|
if (!Canonical()) return false;
|
|
m_Damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
|
if (!m_Damage) return false;
|
|
m_DamageWasEnabled = m_Damage.IsDamageHandlingEnabled(); m_Damage.EnableDamageHandling(false); m_Protected = true;
|
|
// Preserve the established fixture arrangement: move only the real prisoner.
|
|
// Native perception must discover him; neither targets nor reports are seeded.
|
|
vector position = ERR_Actors.Ground(m_Source.Group.GetOrigin() + "0 0 20");
|
|
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = position;
|
|
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
|
m_Started = true;
|
|
PrintFormat("ERR HQ RESPONSE ARRANGEMENT: canonical prisoner moved to=%1 near original source group=%2; temporary player damage protection=%3; no AI/report/radio cadence changes", position, m_Source.Id, m_Protected);
|
|
return true;
|
|
}
|
|
void JournalEvent(ERR_HQJournal journal, string kind, string message, int messageId)
|
|
{
|
|
if (!m_Started || m_Stopped || !m_HQ || journal != m_HQ.Journal() || messageId <= 0) return;
|
|
ERR_NativeResponseJournal entry = m_Journal.Get(messageId);
|
|
if (!entry)
|
|
{
|
|
if (m_Journal.Count() >= 128) { Fail("bounded journal evidence map exhausted"); return; }
|
|
entry = new ERR_NativeResponseJournal(); m_Journal.Insert(messageId, entry);
|
|
}
|
|
if (kind == "REPORT") entry.Report = true;
|
|
if (kind == "TRANSMIT") { entry.Transmit = true; if (message.StartsWith("HQ -> ")) entry.HQTransmit = true; }
|
|
if (kind == "RECEIVE") { if (message.StartsWith("HQ received from ")) entry.HQReceive = true; else if (message.StartsWith("unit ")) entry.UnitReceive = true; }
|
|
if (kind == "ORDER DELIVERED") entry.Delivered = true;
|
|
}
|
|
void Observed(ERR_HQ hq, ERR_HQUnit unit, float now)
|
|
{
|
|
if (!m_Started || m_Stopped || hq != m_HQ || unit != m_Source || unit.PendingReport.Target != m_Player) return;
|
|
ERR_HQReport report = unit.PendingReport;
|
|
ERR_NativeResponseUnit original = Original(unit);
|
|
IEntity observer = unit.Utility.ERR_ResponseVisualObserver(report, now);
|
|
if (!Canonical() || !original || !original.Actors.Contains(observer) || !unit.Uplink || unit.Uplink.Nodes.IsEmpty() || unit.Uplink.Nodes[0].Carrier != unit.Radio || unit.Uplink.MessageId != report.MessageId || unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || report.Source != original.Group || report.ObservedAt > now || Report(report.MessageId)) { Fail("real observed report lacks canonical original observer/radio/source lineage"); return; }
|
|
if (m_Reports.Count() >= 32) { Fail("bounded report evidence exhausted"); return; }
|
|
ERR_NativeResponseLineage proof = new ERR_NativeResponseLineage(); proof.Report = report.Snapshot(); proof.Source = unit; proof.Observer = observer; proof.SourceRadio = unit.Radio; proof.Uplink = unit.Uplink; m_Reports.Insert(proof);
|
|
PrintFormat("ERR HQ RESPONSE SIGHTING: source=%1 report=M%2 observer=%3 radio=%4 target=%5 observedAt=%6; actual native visual sample", unit.Id, report.MessageId, observer, unit.Radio, m_Player, report.ObservedAt);
|
|
}
|
|
void Accepted(ERR_HQ hq, ERR_HQReport report, float now)
|
|
{
|
|
if (!m_Started || m_Stopped || hq != m_HQ) return;
|
|
ERR_NativeResponseLineage proof = Report(report.MessageId); if (!proof) return;
|
|
if (report.Target != m_Player || report.Source != proof.Report.Source || report.ObservedAt != proof.Report.ObservedAt || vector.Distance(report.Position, proof.Report.Position) > 0.01 || !proof.Uplink || proof.Uplink.Failed || proof.Uplink.Sending || proof.Uplink.Cursor < proof.Uplink.Nodes.Count()) { Fail("accepted report lacks completed actual uplink or immutable observed time"); return; }
|
|
proof.Accepted = true; proof.AcceptedAt = now;
|
|
}
|
|
void Delivered(ERR_HQ hq, ERR_HQUnit unit, ERR_HQRoute route, float now)
|
|
{
|
|
if (!m_Started || m_Stopped || hq != m_HQ || !unit.Assignment) return;
|
|
ERR_NativeResponseLineage proof = Report(unit.Assignment.MessageId); if (!proof || proof.Recipient) return;
|
|
ERR_NativeResponseUnit original = Original(unit);
|
|
if (!proof.Accepted || !original || unit == proof.Source || !unit.Assignment || unit.Assignment.Target != m_Player || unit.Assignment.Source != proof.Report.Source || unit.Assignment.ObservedAt != proof.Report.ObservedAt || vector.Distance(unit.Assignment.Position, proof.Report.Position) > 0.01 || unit.Radio != original.Radio || !original.Actors.Contains(unit.Radio) || !route.Downlink || route.Nodes.IsEmpty() || route.Nodes[route.Nodes.Count() - 1].Carrier != unit.Radio || route.Failed || route.Sending || route.Cursor != route.Nodes.Count() - 1 || !unit.Waypoint) { Fail("delivered response lacks immutable accepted report and original physical recipient/downlink"); return; }
|
|
proof.Recipient = unit; proof.RecipientRadio = unit.Radio; proof.DeliveryId = route.MessageId; proof.Waypoint = unit.Waypoint; proof.DeliveredAt = now;
|
|
foreach (IEntity actor : original.Actors) if (ERR_Actors.CanFight(actor)) { proof.MovingActor = actor; break; }
|
|
if (proof.MovingActor) proof.DeliveredOrigin = proof.MovingActor.GetOrigin();
|
|
PrintFormat("ERR HQ RESPONSE DELIVERED: report=M%1 downlink=M%2 source=%3 recipient=%4 radio=%5 waypoint=%6 observedAt=%7", proof.Report.MessageId, route.MessageId, proof.Source.Id, unit.Id, unit.Radio, unit.Waypoint, unit.Assignment.ObservedAt);
|
|
}
|
|
int Tick(float now)
|
|
{
|
|
if (m_Stopped) return -1;
|
|
if (!Enabled() || s_Active != this || !m_Mode || !m_Mode.Active() || !m_HQ || now > m_Deadline || m_Failed) { Check(false, "bounded active evidence context: " + m_Failure); return -1; }
|
|
if (!m_Started)
|
|
{
|
|
if (!m_HQ.ERR_ProbeSquadsReady()) return 0;
|
|
array<IEntity> guns = {}; m_HQ.Mortars().AppendGuns(guns);
|
|
foreach (IEntity gun : guns) { ERR_MortarBattery battery = m_HQ.Mortars().FindGun(gun); if (!battery || !battery.StartupActivated || !battery.Operational() || !m_HQ.MortarConnected(battery.Group)) return 0; }
|
|
if (!Check(Arrange(), "original physical force, RTOs and canonical prisoner captured before arranged sighting")) return -1;
|
|
return 0;
|
|
}
|
|
if (!Canonical() || !UnchangedForces()) { Check(false, "canonical player and all original physical group/actor identities retained without added force"); return -1; }
|
|
if (now >= m_NextDiagnostic)
|
|
{
|
|
PrintFormat("ERR HQ RESPONSE PROOF: observed source reports=%1; %2", m_Reports.Count(), m_HQ.ERR_ProbePerception()); m_NextDiagnostic = now + 10000;
|
|
}
|
|
foreach (ERR_NativeResponseLineage proof : m_Reports)
|
|
{
|
|
if (!proof.Recipient || !proof.MovingActor) continue;
|
|
if (proof.Recipient.Waypoint != proof.Waypoint || !proof.Recipient.Assignment || proof.Recipient.Assignment.MessageId != proof.Report.MessageId) continue;
|
|
ERR_NativeResponseJournal uplink = m_Journal.Get(proof.Report.MessageId);
|
|
ERR_NativeResponseJournal downlink = m_Journal.Get(proof.DeliveryId);
|
|
if (!uplink || !downlink || !uplink.Report || !uplink.Transmit || !uplink.HQReceive || !downlink.HQTransmit || !downlink.UnitReceive || !downlink.Delivered) continue;
|
|
if (proof.Recipient.Group.GetCurrentWaypoint() != proof.Waypoint || vector.DistanceXZ(proof.MovingActor.GetOrigin(), proof.DeliveredOrigin) < 3) continue;
|
|
if (vector.DistanceXZ(proof.MovingActor.GetOrigin(), proof.Report.Position) >= vector.DistanceXZ(proof.DeliveredOrigin, proof.Report.Position) - 1) continue;
|
|
if (!Check(proof.SourceRadio && proof.RecipientRadio && m_HQ.ERR_ProbeUniqueAssignments(), "actual sighting/report/uplink/HQ receipt/downlink journal lineage reaches distinct original infantry with bounded unique waypoints")) return -1;
|
|
if (!Check(UnchangedForces(), "original recipient physically moves toward its native Search waypoint with no replacement or additional force")) return -1;
|
|
Stop(); return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
void Stop()
|
|
{
|
|
if (m_Stopped) return;
|
|
m_Stopped = true;
|
|
if (m_Protected && m_Damage) m_Damage.EnableDamageHandling(m_DamageWasEnabled);
|
|
m_Protected = false;
|
|
if (s_Active == this) s_Active = null;
|
|
}
|
|
void ~ERR_NativeHQResponseProof() { Stop(); }
|
|
}
|
|
#endif
|