Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_HQCoverage.c
T

146 lines
5.7 KiB
C

// Negative sightings have the same ownership as visual reports: HQ may ingest
// this snapshot only after its physical source's uplink completes successfully.
class ERR_HQSearchReport
{
string WatchSourceId;
int WatchMessageId;
int WatchRequestId;
int SourceId;
int MessageId;
int BeliefId;
int BeliefRevision;
vector Position;
float ObservedAt = -1;
bool SweepCompleted;
bool ContactFound;
}
class ERR_HQCoverageRules
{
static bool PositionValid(vector position)
{
return ERR_HQPursuitRules.PositionValid(position)
&& Math.AbsFloat(position[0]) < 1000000 && Math.AbsFloat(position[2]) < 1000000;
}
static int Cell(float coordinate) { return Math.Floor(coordinate / 500); }
// Bounded coordinates make this integer grid key collision-free, including
// negative positions. No per-candidate string or collection allocation.
static int Key(vector position)
{
return (Cell(position[0]) + 2000) * 4096 + Cell(position[2]) + 2000;
}
static bool Recent(float searchedAt, float now)
{
return ERR_HQPursuitRules.Finite(searchedAt) && ERR_HQPursuitRules.Finite(now)
&& searchedAt >= 0 && now >= searchedAt && now - searchedAt <= 900000;
}
static bool ValidReport(int sourceId, int messageId, int beliefId, int revision,
bool completed, bool found, float observedAt, float now)
{
return sourceId > 0 && messageId > 0 && beliefId > 0 && revision > 0 && completed && !found
&& Recent(observedAt, now) && now - observedAt <= 120000;
}
static bool Point(int x, int z, vector center, float radius, out vector point)
{
point = Vector(x * 500 + 250, center[1], z * 500 + 250);
if (vector.DistanceXZ(point, center) > radius)
{
// Small uncertainty circles need not include a grid center. Choose a point
// in their intersecting cell; inset edges keep its key in the selected cell.
point[0] = Math.Clamp(center[0], x * 500, x * 500 + 499.99);
point[2] = Math.Clamp(center[2], z * 500, z * 500 + 499.99);
}
return PositionValid(point) && vector.DistanceXZ(point, center) <= radius;
}
}
// A cell records a reported sweep visit, not proof that every building or hiding
// place in its 500m square was visible. New positive sightings invalidate that
// coarse exclusion; replay watermarks survive so old negatives cannot restore it.
class ERR_HQCoverage
{
protected ref map<int, float> m_Cells = new map<int, float>();
protected ref map<int, int> m_Messages = new map<int, int>();
int Count() { return m_Cells.Count(); }
void Forget(vector position)
{
if (ERR_HQCoverageRules.PositionValid(position)) m_Cells.Remove(ERR_HQCoverageRules.Key(position));
}
bool Recent(vector position, float now)
{
if (!ERR_HQCoverageRules.PositionValid(position)) return false;
int key = ERR_HQCoverageRules.Key(position);
return m_Cells.Contains(key) && ERR_HQCoverageRules.Recent(m_Cells.Get(key), now);
}
bool Ingest(ERR_HQSearchReport report, float now)
{
if (!report || !ERR_HQCoverageRules.PositionValid(report.Position)) return false;
if (!ERR_HQCoverageRules.ValidReport(report.SourceId, report.MessageId, report.BeliefId,
report.BeliefRevision, report.SweepCompleted, report.ContactFound, report.ObservedAt, now)) return false;
if (m_Messages.Contains(report.SourceId) && report.MessageId <= m_Messages.Get(report.SourceId)) return false;
// One uplink per source gives monotonic message order. Never evict replay
// watermarks; the mission already caps its total physical formations at 128.
if (!m_Messages.Contains(report.SourceId) && m_Messages.Count() >= 128) return false;
m_Messages.Set(report.SourceId, report.MessageId);
int key = ERR_HQCoverageRules.Key(report.Position);
if (m_Cells.Contains(key) && m_Cells.Get(key) >= report.ObservedAt) return false;
if (!m_Cells.Contains(key) && m_Cells.Count() >= 128)
{
int oldestKey;
float oldest = 1e20;
foreach (int known, float searched : m_Cells)
{
if (searched >= oldest) continue;
oldest = searched;
oldestKey = known;
}
if (oldest >= report.ObservedAt) return false;
m_Cells.Remove(oldestKey);
}
// Store the observation time, not delivery time: radio delay never makes a
// searched area newer than the physical sweep that justified it.
m_Cells.Set(key, report.ObservedAt);
return true;
}
protected bool Reserved(vector point, array<vector> reserved)
{
if (!reserved) return false;
int key = ERR_HQCoverageRules.Key(point);
foreach (vector position : reserved)
if (ERR_HQCoverageRules.PositionValid(position) && ERR_HQCoverageRules.Key(position) == key) return true;
return false;
}
bool Select(vector center, float radius, vector fromPosition, float now,
out vector destination, array<vector> reserved = null)
{
destination = center;
if (!ERR_HQCoverageRules.PositionValid(center) || !ERR_HQCoverageRules.PositionValid(fromPosition))
return false;
if (!ERR_HQPursuitRules.Finite(now) || now < 0 || !ERR_HQPursuitRules.Finite(radius)
|| radius < 0 || radius > 2000 || (reserved && reserved.Count() > 8)) return false;
int lowX = ERR_HQCoverageRules.Cell(center[0] - radius);
int highX = ERR_HQCoverageRules.Cell(center[0] + radius);
int lowZ = ERR_HQCoverageRules.Cell(center[2] - radius);
int highZ = ERR_HQCoverageRules.Cell(center[2] + radius);
float best = 1e20;
bool found;
vector point;
// Radius is bounded to 2 km: at most nine by nine candidate grid cells.
for (int x = lowX; x <= highX; x++)
{
for (int z = lowZ; z <= highZ; z++)
{
if (!ERR_HQCoverageRules.Point(x, z, center, radius, point)) continue;
if (Recent(point, now) || Reserved(point, reserved)) continue;
float distance = vector.DistanceXZ(point, fromPosition);
if (distance >= best) continue;
best = distance;
destination = point;
found = true;
}
}
return found;
}
void Clear() { m_Cells.Clear(); m_Messages.Clear(); }
}