Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

151 lines
7.9 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -21,7 +21,6 @@
ref ERR_HQTask Task;
ref ERR_HQTask PendingTask;
ref ERR_EngineerMine EngineerMine;
- bool EngineerMineAttempted;
ref ERR_HQSearchReport PendingSweep;
ref ERR_HQStatusReport PendingStatus;
ref ERR_HQStatusSampler StatusSampler = new ERR_HQStatusSampler();
@@ -133,6 +132,7 @@
protected ref array<ref ERR_HQHuntWindow> m_Hunts = {};
protected ref array<IEntity> m_EngineerMineAttempts = {};
protected ref array<ref ERR_EngineerMine> m_EngineerMineJobs = {};
+ protected ref ERR_EngineerMinePosts m_EngineerMinePosts = new ERR_EngineerMinePosts();
protected ref array<ref ERR_HQBelief> m_PlanBeliefs = {};
protected ref map<string, int> m_HomeAvailable = new map<string, int>();
protected ref ERR_HQNetwork m_Network = new ERR_HQNetwork();
@@ -1219,7 +1219,7 @@
if (unit.Assignment) ReserveTask(unit.Task, reserved, first, second);
if (unit.PendingOrder && (!unit.Assignment || unit.PendingTask != unit.Task)) ReserveTask(unit.PendingTask, reserved, first, second);
}
- // A delivered road post may use one original carried mine. This is local
+ // Each distinct delivered road post may use one original carried mine. This is local
// execution of its existing HQ order; no separate response or new force.
protected void MaintainEngineerMines(float now)
{
@@ -1232,19 +1232,31 @@
}
foreach (ERR_HQUnit unit : m_Units)
{
- if (active >= 2 || m_EngineerMineJobs.Count() >= 32 || m_EngineerMineAttempts.Count() >= 256) break;
- if (unit.EngineerMineAttempted || !unit.Task || !unit.Assignment || !unit.Task.RoadIntercept) continue;
+ if (active >= 2 || m_EngineerMineJobs.Count() >= 32 || m_EngineerMinePosts.Count() >= 32 || m_EngineerMineAttempts.Count() >= 256) break;
+ if (!unit.Task || !unit.Assignment || !unit.Task.RoadIntercept) continue;
if (unit.PendingTask || unit.PendingOrder || unit.Downlink || now >= unit.Task.ExpiresAt) continue;
if (!unit.Eligible(m_Mode) || Fighting(unit)) continue;
- if (vector.DistanceXZ(unit.Group.GetOrigin(), unit.Task.Destination) > 20) continue;
- // One bounded site query/attempt per original squad, including a refusal.
- // Retained exact-item quarantine also prevents another squad retrying it.
- unit.EngineerMineAttempted = true;
+ vector groupPosition = unit.Group.GetOrigin();
+ float arrivalDistance = vector.DistanceXZ(groupPosition, unit.Task.Destination);
+ if (!ERR_ForceRules.FiniteVector(groupPosition) || !ERR_ForceRules.Finite(arrivalDistance) || arrivalDistance > 20) continue;
+ if (!m_Mode.Forces() || !unit.StatusSampler || !unit.StatusSampler.OwnsRoster(unit.Group)) continue;
+ ERR_ForceRecord record = m_Mode.Forces().FindGroup(unit.Group);
+ if (!record || record.Group != unit.Group || record.InitialStrength != unit.StatusSampler.InitialStrength()) continue;
+ bool blocked;
+ foreach (ERR_EngineerMine previous : m_EngineerMineJobs)
+ if (previous.BlocksNextPost(record)) { blocked = true; break; }
+ if (blocked) continue;
+ // Snapshot old sites before adding this attempt/site. Refusals consume quota;
+ // registering the original group again cannot reset the mission ledger.
+ array<vector> priorSites = {}; m_EngineerMinePosts.PriorSites(priorSites);
+ int attempt = m_EngineerMinePosts.Consume(record, unit.ForceSourceId, unit.Task, unit.Assignment, now);
+ if (attempt < 0) continue;
vector position;
if (!ERR_EngineerMineSite.Select(unit.Task.Destination, position)) break;
+ if (!m_EngineerMinePosts.RecordSite(attempt, position)) break;
unit.EngineerMine = new ERR_EngineerMine(m_EngineerMineAttempts);
m_EngineerMineJobs.Insert(unit.EngineerMine);
- if (unit.EngineerMine.Begin(m_Mode, unit, unit.Task, position, now)) active++;
+ if (unit.EngineerMine.Begin(m_Mode, unit, unit.Task, position, now, priorSites)) active++;
else unit.EngineerMine.Stop();
break; // One new site query/admission per HQ tick, including refusals.
}
--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMinePosts.c
@@ -0,0 +1,81 @@
+// Mission-owned finite attempt history. Registration and assignment release
+// never reset it. Values are copied before native work; no live enemy queries.
+class ERR_EngineerMinePost
+{
+ ref ERR_ForceRecord Record;
+ SCR_AIGroup Group;
+ string Source;
+ int Strength;
+ ref ERR_HQTask Task;
+ ref ERR_HQReport Assignment;
+ int MessageId;
+ int BeliefId;
+ int Revision;
+ float QueuedAt;
+ float ObservedAt;
+ vector Post;
+ bool HasSite;
+ vector Site;
+}
+class ERR_EngineerMinePosts
+{
+ protected ref array<ref ERR_EngineerMinePost> m_Posts = {};
+ int Count() { return m_Posts.Count(); }
+ bool Eligible(ERR_ForceRecord record, string source, ERR_HQTask task, ERR_HQReport assignment, float now)
+ {
+ if (!record || !record.Group || !record.Physical || record.Eliminated || source.IsEmpty() || record.SourceId != source) return false;
+ if (record.InitialStrength < 1 || record.InitialStrength > 32 || !task || !assignment || m_Posts.Count() >= 32) return false;
+ if (!task.RoadIntercept || task.ReleaseOnly || task.Investigation() || !task.SecuritySite.IsEmpty()) return false;
+ if (task.BeliefId <= 0 || task.BeliefRevision <= 0 || assignment.MessageId <= 0) return false;
+ if (!ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(task.QueuedAt) || !ERR_ForceRules.Finite(task.ExpiresAt)
+ || !ERR_ForceRules.Finite(assignment.ObservedAt) || assignment.ObservedAt < 0 || task.QueuedAt < assignment.ObservedAt
+ || task.QueuedAt > now || now >= task.ExpiresAt) return false;
+ if (!ERR_ForceRules.FiniteVector(task.Destination) || !ERR_ForceRules.FiniteVector(assignment.Position)) return false;
+ int used;
+ foreach (ERR_EngineerMinePost previous : m_Posts)
+ {
+ if (!previous) return false;
+ float separation = vector.DistanceXZ(task.Destination, previous.Post);
+ if (!ERR_ForceRules.Finite(separation) || separation <= 160) return false;
+ if (previous.Task == task || previous.Assignment == assignment) return false;
+ // Same source or physical group under another record cannot acquire quota.
+ if (previous.Record != record)
+ {
+ if (previous.Source == source || previous.Group == record.Group) return false;
+ continue;
+ }
+ if (previous.Source != source || previous.Group != record.Group || previous.Strength != record.InitialStrength) return false;
+ used++;
+ if (used >= 4 || task.QueuedAt <= previous.QueuedAt || assignment.ObservedAt <= previous.ObservedAt
+ || assignment.MessageId == previous.MessageId) return false;
+ if (task.BeliefId == previous.BeliefId && task.BeliefRevision <= previous.Revision) return false;
+ }
+ return true;
+ }
+ // Returns immutable entry index or -1; a refused site/item still consumes it.
+ int Consume(ERR_ForceRecord record, string source, ERR_HQTask task, ERR_HQReport assignment, float now)
+ {
+ if (!Eligible(record, source, task, assignment, now)) return -1;
+ ERR_EngineerMinePost entry = new ERR_EngineerMinePost();
+ entry.Record = record; entry.Group = record.Group; entry.Source = source; entry.Strength = record.InitialStrength;
+ entry.Task = task; entry.Assignment = assignment; entry.MessageId = assignment.MessageId;
+ entry.BeliefId = task.BeliefId; entry.Revision = task.BeliefRevision;
+ entry.QueuedAt = task.QueuedAt; entry.ObservedAt = assignment.ObservedAt; entry.Post = task.Destination;
+ m_Posts.Insert(entry);
+ return m_Posts.Count() - 1;
+ }
+ bool RecordSite(int index, vector site)
+ {
+ if (index < 0 || index >= m_Posts.Count() || !ERR_ForceRules.FiniteVector(site)) return false;
+ ERR_EngineerMinePost entry = m_Posts[index];
+ if (!entry || entry.HasSite || !ERR_EngineerMineSite.InBand(entry.Post, site)) return false;
+ entry.Site = site; entry.HasSite = true;
+ return true;
+ }
+ void PriorSites(out array<vector> sites)
+ {
+ sites.Clear();
+ foreach (ERR_EngineerMinePost entry : m_Posts)
+ if (entry.HasSite) sites.Insert(entry.Site);
+ }
+}