--- 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 m_Hunts = {}; protected ref array m_EngineerMineAttempts = {}; protected ref array m_EngineerMineJobs = {}; + protected ref ERR_EngineerMinePosts m_EngineerMinePosts = new ERR_EngineerMinePosts(); protected ref array m_PlanBeliefs = {}; protected ref map m_HomeAvailable = new map(); 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 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 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 sites) + { + sites.Clear(); + foreach (ERR_EngineerMinePost entry : m_Posts) + if (entry.HasSite) sites.Insert(entry.Site); + } +}