// Original checkpoint duty, not an invented HQ sighting/order. One carried mine // per job; common engineer custody/callback/withdrawal rules own all mutations. class ERR_RoadblockMine : ERR_EngineerMine { protected AIWaypoint m_Hold; protected ERR_KnownMines m_Known; protected AIAgent m_WorkAgent; protected int m_PreviousLOD; protected bool m_LODLease; protected override bool OrderValid() { if (!m_Hold || !m_Known || m_Unit.Task || m_Unit.Assignment || m_Unit.PendingTask || m_Unit.PendingOrder || m_Unit.Downlink) return false; if (m_Group.GetCurrentWaypoint() != m_Hold || vector.Distance(m_Hold.GetOrigin(), m_Rally) > 0.01) return false; // The mine must stay outside the entire ordinary guard/cover-search area. return vector.DistanceXZ(m_Rally, m_Position) >= m_Hold.GetCompletionRadius() + 20; } override void Stop() { super.Stop(); // Release only this job's temporary simulation lease; preserve GM changes. if (m_LODLease && m_WorkAgent && m_WorkAgent.GetPermanentLOD() == 0) m_WorkAgent.SetPermanentLOD(m_PreviousLOD); m_LODLease = false; } protected override void OnArmed() { m_Known.Add(m_Mine, m_Position); } bool BeginPost(ERR_GameMode mode, ERR_HQUnit unit, AIWaypoint hold, vector position, float now, array priorSites, ERR_KnownMines known) { if (m_Begun || m_Stopped) return false; m_Begun = true; if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return Refuse(); if (!unit || !unit.Group || !unit.StatusSampler || !hold || !known || !mode.Forces()) return Refuse(); if (!ERR_ForceRules.Finite(now) || now < 0 || !ERR_ForceRules.FiniteVector(position)) return Refuse(); if (!m_Attempted || m_Attempted.Count() >= 256 || !priorSites || priorSites.Count() > 32) return Refuse(); m_Mode = mode; m_Unit = unit; m_Group = unit.Group; m_Hold = hold; m_Known = known; m_SourceId = unit.ForceSourceId; m_Strength = unit.StatusSampler.InitialStrength(); m_Record = mode.Forces().FindGroup(m_Group); m_Position = position; m_Rally = hold.GetOrigin(); float distance = vector.DistanceXZ(m_Position, m_Rally); if (!ERR_ForceRules.FiniteVector(m_Rally) || distance < 45 || distance > 60) return Refuse(); foreach (vector prior : priorSites) { if (!ERR_ForceRules.FiniteVector(prior)) return Refuse(); m_PriorSites.Insert(prior); } if (!SelectActor() || !Owned() || !Quiet()) return Refuse(); if (!ERR_EngineerMineRules.CorridorClear(m_Actor.GetOrigin(), m_Position, m_PriorSites) || !ERR_EngineerMineRules.CorridorClear(m_Position, m_Rally, m_PriorSites)) return Refuse(); m_Attempted.Insert(m_Mine); if (!Carried()) return Refuse(); if (m_Mode.MineIntel()) m_Mode.MineIntel().Candidates(m_Actor, m_IntelFolders); m_WorkAgent = SCR_AIUtils.GetAIAgent(m_Actor); if (!m_WorkAgent) return Refuse(); m_PreviousLOD = m_WorkAgent.GetPermanentLOD(); m_LODLease = true; m_WorkAgent.SetPermanentLOD(0); if (!m_WorkAgent.ActivateAI()) return Refuse(); m_Started = now; m_LastTick = now; m_State = 0; Move(m_Position); ERR_HQ.Event("ROADBLOCK MINE", "guard carrying original PMN4 to off-road site; original hold order retained"); return true; } } class ERR_RoadblockMinePost { SCR_AIGroup Group; AIWaypoint Hold; vector Right; int Attempts; float NextAttempt; ref array Sites = {}; ref array Jobs = {}; } class ERR_RoadblockMining { protected ERR_GameMode m_Mode; protected ref array m_Posts = {}; protected ref ERR_KnownMines m_Known; protected bool m_Stopped; void ERR_RoadblockMining(ERR_GameMode mode) { m_Mode = mode; m_Known = new ERR_KnownMines(mode); } void Register(SCR_AIGroup group, AIWaypoint hold, vector right) { if (m_Stopped || !group || !hold || m_Posts.Count() >= 12) return; ERR_RoadblockMinePost post = new ERR_RoadblockMinePost(); post.Group = group; post.Hold = hold; post.Right = right; // Equipment insertion and original roster sampling must settle first. post.NextAttempt = GetGame().GetWorld().GetWorldTime() + 30000; m_Posts.Insert(post); } // Four separated flanks, with open road and a clear inner guard area. Every // nearby road's *edge* is checked, including intersections and narrow tracks. static bool Site(vector post, vector right, int slot, out vector position) { if (slot < 0 || slot >= 4 || !ERR_ForceRules.FiniteVector(post) || !ERR_ForceRules.FiniteVector(right)) return false; vector forward = Vector(-right[2], 0, right[0]); int side = 1, row = 1; if (slot % 2 == 1) side = -1; if (slot >= 2) row = -1; position = ERR_Actors.Ground(post + right * (side * 47) + forward * (row * 20)); if (!ERR_EngineerMineSite.InBand(post, position) || !ERR_HQRoadIntercept.Standable(position)) return false; ERR_SettlementSafety settlements = new ERR_SettlementSafety(); if (!settlements.OutsideSettlements(position)) return false; SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld()); if (!world || !world.GetRoadNetworkManager()) return false; array roads = {}; world.GetRoadNetworkManager().GetRoadsInAABB(position - "100 1000 100", position + "100 1000 100", roads); if (roads.IsEmpty() || roads.Count() > 128) return false; int segments; foreach (BaseRoad road : roads) { if (!road || !ERR_ForceRules.Finite(road.GetWidth()) || road.GetWidth() < 0 || road.GetWidth() > 160) return false; array points = {}; road.GetPoints(points); if (points.Count() < 2 || points.Count() > 1024) return false; segments += points.Count() - 1; if (segments > 4096) return false; for (int i = 1; i < points.Count(); i++) if (!ERR_MineAvoidanceRules.Clear(points[i - 1], points[i], position, road.GetWidth() * 0.5 + 15)) return false; } return true; } void Tick(array units, array attempted, float now) { if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return; int active; foreach (ERR_RoadblockMinePost post : m_Posts) foreach (ERR_RoadblockMine job : post.Jobs) { job.Tick(now); if (!job.Finished()) active++; } if (active >= 2) return; foreach (ERR_RoadblockMinePost post : m_Posts) { if (!post.Group || !post.Hold || post.Attempts >= 4 || now < post.NextAttempt) continue; if (post.Group.GetCurrentWaypoint() != post.Hold) { post.Attempts = 4; continue; } ERR_HQUnit owner; foreach (ERR_HQUnit unit : units) if (unit.Group == post.Group) { owner = unit; break; } if (!owner || !owner.Eligible(m_Mode) || !owner.StatusSampler || !owner.StatusSampler.OwnsRoster(post.Group)) continue; if (owner.Task || owner.Assignment || owner.PendingTask || owner.PendingOrder || owner.Downlink || ERR_HunterCombat.Active(post.Group)) continue; bool blocked; ERR_ForceRecord record = m_Mode.Forces().FindGroup(post.Group); foreach (ERR_RoadblockMine previous : post.Jobs) if (previous.BlocksNextPost(record)) { blocked = true; break; } if (blocked) continue; int slot = post.Attempts++; post.NextAttempt = now + 30000; vector position; if (!Site(post.Hold.GetOrigin(), post.Right, slot, position)) { ERR_HQ.Event("ROADBLOCK MINE", "unsafe off-road site skipped; finite attempt consumed"); return; } ERR_RoadblockMine next = new ERR_RoadblockMine(attempted); post.Jobs.Insert(next); owner.EngineerMine = next; if (next.BeginPost(m_Mode, owner, post.Hold, position, now, post.Sites, m_Known)) post.Sites.Insert(position); else { next.Stop(); ERR_HQ.Event("ROADBLOCK MINE", "placement refused: no eligible carried mine, quiet guard or clear withdrawal route"); } return; // One bounded site scan/admission per HQ tick. } } void Stop() { m_Stopped = true; foreach (ERR_RoadblockMinePost post : m_Posts) foreach (ERR_RoadblockMine job : post.Jobs) job.Stop(); m_Known.Stop(); } }