146 lines
5.7 KiB
C
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(); }
|
|
}
|