// Radio propagation is simulated using actual living carriers and inventory. // A manpack is the HQ uplink; short-range hops are limited to 500 m and three // retransmissions. No virtual relay is fabricated when the network is broken. class ERR_HQRadioNode { IEntity Carrier; SCR_AIGroup Group; int UnitId; int Capability; protected float m_CheckedAt = -1; protected int m_LiveCapability; bool Valid(bool manpack = false) { ERR_GameMode factions = ERR_GameMode.Get(); if (!Carrier || !Group || Group.GetPlayerCount(true) > 0 || !factions || Group.GetFaction() != factions.EnemyFaction() || ERR_Actors.Group(Carrier) != Group) return false; int required = 1; if (manpack) required = 2; float now = GetGame().GetWorld().GetWorldTime(); if (m_CheckedAt < 0 || now < m_CheckedAt || now - m_CheckedAt >= 1000) { m_CheckedAt = now; m_LiveCapability = ERR_HQRadio.Capability(Carrier); } return m_LiveCapability >= required; } string Label() { return "unit " + UnitId.ToString(); } } class ERR_HQRoute { ref array Nodes = {}; bool Downlink; int Cursor; bool Sending; float Due; vector SentFrom; float StartedAt = -1; int MessageId; string Payload; bool Failed; // -1 failed, 0 travelling, 1 delivered. Each hop really gates delivery. int Advance(float now, ERR_HQJournal journal) { if (Failed) return -1; if (StartedAt < 0) { StartedAt = now; if (Downlink) Cursor = -1; // first leg is HQ to the manpack } if (now - StartedAt > 30000) return Fail(journal, "route timed out"); int target = Cursor + 1; if (Sending) { if (now < Due) return 0; if (target < Nodes.Count()) { if (!Nodes[target].Valid(Downlink && target == 0)) return Fail(journal, "receiving carrier lost radio, died or changed ownership"); if (Cursor >= 0 && vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin()) > 500) return Fail(journal, "receiver moved beyond the 500m link"); journal.Write("RECEIVE", Nodes[target].Label() + " received from " + SenderLabel(), MessageId); } else journal.Write("RECEIVE", "HQ received from " + SenderLabel(), MessageId); Cursor++; Sending = false; if ((!Downlink && Cursor >= Nodes.Count()) || (Downlink && Cursor == Nodes.Count() - 1)) return 1; target = Cursor + 1; } if (Nodes.IsEmpty()) return Fail(journal, "no physical radio route"); if (Cursor >= 0 && !Nodes[Cursor].Valid(!Downlink && Cursor == Nodes.Count() - 1)) return Fail(journal, "transmitting carrier lost radio, died or changed ownership"); string recipient = "HQ"; string link = "long-range"; if (target < Nodes.Count()) { if (!Nodes[target].Valid(Downlink && target == 0)) return Fail(journal, "next relay unavailable"); recipient = Nodes[target].Label(); if (Cursor >= 0) { SentFrom = Nodes[Cursor].Carrier.GetOrigin(); float distance = vector.DistanceXZ(SentFrom, Nodes[target].Carrier.GetOrigin()); if (distance > 500) return Fail(journal, "next relay beyond 500m"); link = Math.Round(distance).ToString() + "m short-range"; } } journal.Write("TRANSMIT", SenderLabel() + " -> " + recipient + " (" + link + ")", MessageId); Sending = true; Due = now + 2000; return 0; } protected string SenderLabel() { if (Cursor < 0) return "HQ"; return Nodes[Cursor].Label(); } protected int Fail(ERR_HQJournal journal, string reason) { Failed = true; journal.Write("LOST", reason + "; " + Payload, MessageId); return -1; } } class ERR_HQNetwork { protected ref array m_Nodes = {}; protected ref array m_Parents = {}; protected ref array m_Depths = {}; protected ref map m_Indices = new map(); protected ref map> m_Cells = new map>(); bool Overflow; void Clear() { m_Nodes.Clear(); m_Parents.Clear(); m_Depths.Clear(); m_Indices.Clear(); m_Cells.Clear(); Overflow = false; } protected string Cell(int x, int z) { return string.Format("%1:%2", x, z); } void Add(IEntity carrier, SCR_AIGroup group, int unitId, int capability) { if (m_Indices.Contains(carrier)) return; if (m_Nodes.Count() >= 2048) { Overflow = true; return; } ERR_HQRadioNode node = new ERR_HQRadioNode(); node.Carrier = carrier; node.Group = group; node.UnitId = unitId; node.Capability = capability; int index = m_Nodes.Count(); m_Nodes.Insert(node); m_Indices.Insert(carrier, index); m_Parents.Insert(-2); m_Depths.Insert(99); vector position = carrier.GetOrigin(); string key = Cell(Math.Floor(position[0] / 500), Math.Floor(position[2] / 500)); array cell = m_Cells.Get(key); if (!cell) { cell = {}; m_Cells.Insert(key, cell); } cell.Insert(index); } void Build() { array queue = {}; int checks; for (int root = 0; root < m_Nodes.Count(); root++) { if (m_Nodes[root].Capability == 2) { m_Parents[root] = -1; m_Depths[root] = 0; queue.Insert(root); } } for (int head = 0; head < queue.Count(); head++) { int from = queue[head]; if (m_Depths[from] >= 3) continue; vector position = m_Nodes[from].Carrier.GetOrigin(); int x = Math.Floor(position[0] / 500); int z = Math.Floor(position[2] / 500); for (int dx = -1; dx <= 1; dx++) { for (int dz = -1; dz <= 1; dz++) { array neighbors = m_Cells.Get(Cell(x + dx, z + dz)); if (!neighbors) continue; foreach (int candidate : neighbors) { if (++checks > 100000) { Overflow = true; return; } if (m_Parents[candidate] != -2 || vector.DistanceXZ(position, m_Nodes[candidate].Carrier.GetOrigin()) > 500) continue; m_Parents[candidate] = from; m_Depths[candidate] = m_Depths[from] + 1; queue.Insert(candidate); } } } } } ERR_HQRoute Route(IEntity caller, bool downlink = false) { int index; if (!m_Indices.Find(caller, index) || m_Parents[index] == -2) return null; ERR_HQRoute route = new ERR_HQRoute(); route.Downlink = downlink; while (index >= 0) { if (downlink) route.Nodes.InsertAt(m_Nodes[index], 0); else route.Nodes.Insert(m_Nodes[index]); index = m_Parents[index]; } return route; } }