295 lines
8.6 KiB
C
295 lines
8.6 KiB
C
// 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 (!ERR_Actors.CanFight(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<ref ERR_HQRadioNode> Nodes = {};
|
|
SCR_AIGroup LocalSource;
|
|
IEntity LocalWitness;
|
|
ref ERR_HQStatusSampler LocalRoster;
|
|
bool Downlink;
|
|
bool Originated;
|
|
float OriginDue = -1;
|
|
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");
|
|
// An originating report needs three seconds on its original physical
|
|
// caller before any network transmission. Later completed hops remain
|
|
// independent of that caller, and HQ downlinks do not repeat this call.
|
|
if (!Downlink && !Originated)
|
|
{
|
|
if (Nodes.IsEmpty() || !Nodes[0].Valid(Nodes.Count() == 1))
|
|
return Fail(journal, "originating call lost its living owned radio carrier");
|
|
if (LocalRoster && !ERR_HQLocalHandoff.Valid(LocalSource, LocalRoster, LocalWitness, Nodes[0]))
|
|
return Fail(journal, "local sighting handoff lost original squad member, recipient or 100m reach");
|
|
if (OriginDue < 0)
|
|
{
|
|
OriginDue = now + 3000;
|
|
journal.Write("CALL", Nodes[0].Label() + " originating report; three seconds before transmission", MessageId);
|
|
}
|
|
if (now < OriginDue) return 0;
|
|
Originated = true;
|
|
journal.Write("CALL COMPLETE", Nodes[0].Label() + " completed originating report", MessageId);
|
|
}
|
|
int target = Cursor + 1;
|
|
if (Sending)
|
|
{
|
|
if (now < Due)
|
|
return 0;
|
|
// A transmission takes two seconds. The sender must keep its physical
|
|
// radio throughout that interval, including the final manpack-to-HQ leg.
|
|
if (Cursor >= 0 && !Nodes[Cursor].Valid(!Downlink && Cursor == Nodes.Count() - 1))
|
|
return Fail(journal, "transmitting carrier lost radio during transmission");
|
|
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<ref ERR_HQRadioNode> m_Nodes = {};
|
|
protected ref array<int> m_Parents = {};
|
|
protected ref array<int> m_Depths = {};
|
|
protected ref map<IEntity, int> m_Indices = new map<IEntity, int>();
|
|
protected ref map<string, ref array<int>> m_Cells = new map<string, ref array<int>>();
|
|
bool Overflow;
|
|
protected int m_LocalChecks;
|
|
|
|
void Clear()
|
|
{
|
|
m_Nodes.Clear();
|
|
m_Parents.Clear();
|
|
m_Depths.Clear();
|
|
m_Indices.Clear();
|
|
m_Cells.Clear();
|
|
Overflow = false;
|
|
m_LocalChecks = 0;
|
|
}
|
|
|
|
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<int> cell = m_Cells.Get(key);
|
|
if (!cell)
|
|
{
|
|
cell = {};
|
|
m_Cells.Insert(key, cell);
|
|
}
|
|
cell.Insert(index);
|
|
}
|
|
|
|
void Build()
|
|
{
|
|
array<int> 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<int> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// At most 4096 local pairs across all reports per network rebuild. Indexes
|
|
// provide candidate routes only; current original actors/distance are checked.
|
|
ERR_HQRoute LocalRoute(SCR_AIGroup source, ERR_HQStatusSampler roster, array<IEntity> busy)
|
|
{
|
|
if (Overflow || !source || !roster || !roster.OwnsRoster(source) || !busy || busy.Count() > 256) return null;
|
|
int count = roster.InitialStrength();
|
|
if (count < 1 || count > 32) return null;
|
|
foreach (ERR_HQRadioNode node : m_Nodes)
|
|
{
|
|
if (!node || node.Group == source || busy.Contains(node.Carrier)) continue;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (++m_LocalChecks > 4096) return null;
|
|
IEntity witness = roster.InitialMember(i);
|
|
if (!ERR_HQLocalHandoff.Valid(source, roster, witness, node)) continue;
|
|
ERR_HQRoute route = Route(node.Carrier);
|
|
if (!route) continue;
|
|
route.LocalSource = source; route.LocalWitness = witness; route.LocalRoster = roster;
|
|
return route;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|