#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.Player)] [Desc("Manages AI minelayer unit related with " + nameof(Minelayer) + " traits.", "When enemy damage AI's actors, the location of conflict will be recorded,", "If a location is a valid spot, it will add/merge to favorite location for usage later")] public class MinelayerBotModuleInfo : ConditionalTraitInfo { [Desc("Enemy target types to ignore when add the minefield location to conflict location.")] public readonly BitSet IgnoredEnemyTargetTypes = default; [Desc("Victim target types that considering conflict location as enemy location instead of victim location.")] public readonly BitSet UseEnemyLocationTargetTypes = default; [ActorReference(typeof(MinelayerInfo))] [Desc("Actors with " + nameof(Minelayer) + "trait.")] public readonly HashSet MinelayingActorTypes = default; [Desc("Find this amount of suitable actors and lay mine to a location.")] public readonly int MaxPerAssign = 1; [Desc("Scan suitable actors and target in this interval.")] public readonly int ScanTick = 320; [Desc("Radius per mine laying order.")] public readonly int MineFieldRadius = 1; [Desc("Minefield location is cancelled if those whose target type belong to allied nearby.")] public readonly BitSet AwayFromAlliedTargetTypes = default; [Desc("Minefield location is cancelled if those whose target type belong to enemy nearby.")] public readonly BitSet AwayFromEnemyTargetTypes = default; [Desc("Minefield location check distance to AwayFromAlliedTargettype and AwayFromEnemyTargettype.", "In addition, if any emeny actor within this range and minefield location is not cancelled,", "minelayer will try lay mines at the 3/4 path to minefield location")] public readonly int AwayFromCellDistance = 9; [Desc("Merge conflict point minefield position to a favorite minefield position if within this range and closest.", "If favorite minefield positions is at the max of 5, we always merge it to closest regardless of this")] public readonly int FavoritePositionDistance = 6; public override object Create(ActorInitializer init) { return new MinelayerBotModule(init.Self, this); } } public class MinelayerBotModule : ConditionalTrait, IBotTick, IBotRespondToAttack { const int MaxPositionCacheLength = 5; const int RepeatedAltertTicks = 40; readonly World world; readonly Player player; readonly Predicate unitCannotBeOrdered; readonly Predicate unitCannotBeOrderedOrIsBusy; readonly CPos?[] conflictPositionQueue; readonly CPos?[] favoritePositions; int minAssignRoleDelayTicks; int conflictPositionLength; int favoritePositionsLength; int currentFavoritePositionIndex; int alertedTicks; PathFinder pathFinder; public MinelayerBotModule(Actor self, MinelayerBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !a.IsIdle; conflictPositionQueue = new CPos?[MaxPositionCacheLength] { null, null, null, null, null }; favoritePositions = new CPos?[MaxPositionCacheLength] { null, null, null, null, null }; } protected override void TraitEnabled(Actor self) { // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); alertedTicks = 0; conflictPositionLength = 0; favoritePositionsLength = 0; currentFavoritePositionIndex = 0; pathFinder = self.World.WorldActor.Trait(); } void IBotTick.BotTick(IBot bot) { if (alertedTicks > 0) alertedTicks--; if (--minAssignRoleDelayTicks <= 0) { minAssignRoleDelayTicks = Info.ScanTick; var minelayingPosition = CPos.Zero; var useFavoritePosition = false; var layMineOnHalfway = false; while (conflictPositionLength > 0) { minelayingPosition = conflictPositionQueue[0].Value; var (hasInvalidActors, hasEnemyNearby) = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance)); if (hasInvalidActors) DequeueFirstConflictPosition(); else { layMineOnHalfway = hasEnemyNearby; break; } } TraitPair[] minelayers = null; if (conflictPositionLength == 0) { // If enemy turtle themselves at base and we don't have valid position recorded, // we will try find a location that at the middle of pathfinding cells if (favoritePositionsLength == 0) { minelayers = world.ActorsWithTrait().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray(); if (minelayers.Length == 0) return; var enemies = world.Actors.Where(a => IsPreferredEnemyUnit(a)).ToArray(); if (enemies.Length == 0) return; var enemy = enemies.Random(world.LocalRandom); foreach (var minelayer in minelayers) { var cells = pathFinder.FindPathToTargetCell(minelayer.Actor, new[] { minelayer.Actor.Location }, enemy.Location, BlockedByActor.Immovable, laneBias: false); if (cells != null && cells.Count != 0) { AIUtils.BotDebug($"{player}: try find a location to lay mine."); EnqueueConflictPosition(cells[cells.Count / 2]); // We don't do other things in this tick, just find new location and abort return; } } return; } else { while (favoritePositionsLength > 0) { minelayingPosition = favoritePositions[currentFavoritePositionIndex].Value; var (hasInvalidActors, hasEnemyNearby) = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance)); if (hasInvalidActors) { DeleteCurrentFavoritePosition(); if (favoritePositionsLength == 0) return; } else { layMineOnHalfway = hasEnemyNearby; useFavoritePosition = true; break; } } } } minelayers ??= world.ActorsWithTrait().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray(); if (minelayers.Length == 0) return; var orderedActors = new List(); foreach (var minelayer in minelayers) { var cells = pathFinder.FindPathToTargetCell(minelayer.Actor, new[] { minelayer.Actor.Location }, minelayingPosition, BlockedByActor.Immovable, laneBias: false); if (cells != null && cells.Count != 0) { orderedActors.Add(minelayer.Actor); // if there is enemy actor nearby, we will try to lay mine on // 3/4 distance to desired position (the path cell is reversed) if (layMineOnHalfway) { minelayingPosition = cells[cells.Count * 1 / 4]; layMineOnHalfway = false; } if (orderedActors.Count >= Info.MaxPerAssign) break; } } if (orderedActors.Count > 0) { if (useFavoritePosition) { AIUtils.BotDebug($"{player}: Use favorite position {minelayingPosition} at index {currentFavoritePositionIndex}"); NextFavoritePositionIndex(); } else { DequeueFirstConflictPosition(); AddPositionToFavoritePositions(minelayingPosition); AIUtils.BotDebug($"{player}: Use in time conflict position {minelayingPosition}"); } var vec = new CVec(Info.MineFieldRadius, Info.MineFieldRadius); bot.QueueOrder( new Order( "PlaceMinefield", null, Target.FromCell(world, minelayingPosition + vec), false, groupedActors: orderedActors.ToArray()) { ExtraLocation = minelayingPosition - vec }); bot.QueueOrder( new Order( "Move", null, Target.FromCell(world, orderedActors[0].Location), true, groupedActors: orderedActors.ToArray())); } else { if (useFavoritePosition) DeleteCurrentFavoritePosition(); else DequeueFirstConflictPosition(); } } } void DequeueFirstConflictPosition() { for (var i = 1; i < conflictPositionLength; i++) conflictPositionQueue[i - 1] = conflictPositionQueue[i]; conflictPositionQueue[conflictPositionLength - 1] = null; conflictPositionLength--; } void DeleteCurrentFavoritePosition() { for (var i = currentFavoritePositionIndex; i < favoritePositionsLength - 1; i++) favoritePositions[i] = favoritePositions[i + 1]; favoritePositions[favoritePositionsLength - 1] = null; if (--favoritePositionsLength > 0) currentFavoritePositionIndex %= favoritePositionsLength; } void AddPositionToFavoritePositions(CPos cpos) { var favoriteDistSquare = Info.FavoritePositionDistance * Info.FavoritePositionDistance; var closestIndex = 0; var closestDistSquare = int.MaxValue; for (var i = 0; i < favoritePositionsLength; i++) { var lengthsquare = (favoritePositions[i].Value - cpos).LengthSquared; if (lengthsquare < closestDistSquare) { closestIndex = i; closestDistSquare = lengthsquare; } } // Add new if there is space if (closestDistSquare > favoriteDistSquare && favoritePositionsLength < favoritePositions.Length) { favoritePositions[favoritePositionsLength] = cpos; favoritePositionsLength++; } else { var pos = favoritePositions[closestIndex].Value; favoritePositions[closestIndex] = (pos - cpos) / 2 + cpos; } } void NextFavoritePositionIndex() { currentFavoritePositionIndex = (currentFavoritePositionIndex + 1) % favoritePositionsLength; } bool IsPreferredEnemyUnit(Actor actor) { if (actor == null || actor.IsDead || player.RelationshipWith(actor.Owner) != PlayerRelationship.Enemy || actor.Info.HasTraitInfo()) return false; var targetTypes = actor.GetEnabledTargetTypes(); return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes); } (bool HasInvalidActors, bool HasEnemyNearby) HasInvalidActorInCircle(WPos pos, WDist dist) { var hasInvalidActor = false; var hasEnemyActor = false; hasInvalidActor = world.FindActorsInCircle(pos, dist).Any(actor => { if (actor.Owner.RelationshipWith(player) == PlayerRelationship.Ally) { var targetTypes = actor.GetEnabledTargetTypes(); return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromAlliedTargetTypes); } if (actor.Owner.RelationshipWith(player) == PlayerRelationship.Enemy) { hasEnemyActor = true; var targetTypes = actor.GetEnabledTargetTypes(); return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromEnemyTargetTypes); } return false; }); return (hasInvalidActor, hasEnemyActor); } void EnqueueConflictPosition(CPos cell) { if (conflictPositionLength < MaxPositionCacheLength) { conflictPositionQueue[conflictPositionLength] = cell; conflictPositionLength++; } else conflictPositionQueue[MaxPositionCacheLength - 1] = cell; } void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) { if (alertedTicks > 0 || !IsPreferredEnemyUnit(e.Attacker)) return; alertedTicks = RepeatedAltertTicks; var hasInvalidActor = HasInvalidActorInCircle(self.CenterPosition, WDist.FromCells(Info.AwayFromCellDistance)).HasInvalidActors; if (hasInvalidActor) return; var targetTypes = self.GetEnabledTargetTypes(); CPos pos; if (!targetTypes.IsEmpty && targetTypes.Overlaps(Info.UseEnemyLocationTargetTypes)) pos = e.Attacker.Location; else pos = self.Location; EnqueueConflictPosition(pos); } } }