#region Copyright & License Information /* * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) * 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.Mods.Common.Traits.BotModules.Squads; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Manages AI squads.")] public class SquadManagerBotModuleInfo : ConditionalTraitInfo { [Desc("Actor types that are valid for naval squads.")] public readonly HashSet NavalUnitsTypes = new HashSet(); [Desc("Actor types that should generally be excluded from attack squads.")] public readonly HashSet ExcludeFromSquadsTypes = new HashSet(); [Desc("Actor types that are considered construction yards (base builders).")] public readonly HashSet ConstructionYardTypes = new HashSet(); [Desc("Enemy building types around which to scan for targets for naval squads.")] public readonly HashSet NavalProductionTypes = new HashSet(); [Desc("Minimum number of units AI must have before attacking.")] public readonly int SquadSize = 8; [Desc("Random number of up to this many units is added to squad size when creating an attack squad.")] public readonly int SquadSizeRandomBonus = 30; [Desc("Delay (in ticks) between giving out orders to units.")] public readonly int AssignRolesInterval = 50; [Desc("Delay (in ticks) between attempting rush attacks.")] public readonly int RushInterval = 600; [Desc("Delay (in ticks) between updating squads.")] public readonly int AttackForceInterval = 75; [Desc("Minimum delay (in ticks) between creating squads.")] public readonly int MinimumAttackForceDelay = 0; [Desc("Radius in cells around enemy BaseBuilder (Construction Yard) where AI scans for targets to rush.")] public readonly int RushAttackScanRadius = 15; [Desc("Radius in cells around the base that should be scanned for units to be protected.")] public readonly int ProtectUnitScanRadius = 15; [Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.", "Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")] public readonly int MaxBaseRadius = 20; [Desc("Radius in cells that squads should scan for enemies around their position while idle.")] public readonly int IdleScanRadius = 10; [Desc("Radius in cells that squads should scan for danger around their position to make flee decisions.")] public readonly int DangerScanRadius = 10; [Desc("Radius in cells that attack squads should scan for enemies around their position when trying to attack.")] public readonly int AttackScanRadius = 12; [Desc("Radius in cells that protecting squads should scan for enemies around their position.")] public readonly int ProtectionScanRadius = 8; [Desc("Enemy target types to never target.")] public readonly BitSet IgnoredEnemyTargetTypes = default(BitSet); public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { base.RulesetLoaded(rules, ai); if (DangerScanRadius <= 0) throw new YamlException("DangerScanRadius must be greater than zero."); } public override object Create(ActorInitializer init) { return new SquadManagerBotModule(init.Self, this); } } public class SquadManagerBotModule : ConditionalTrait, IBotEnabled, IBotTick, IBotRespondToAttack, IBotPositionsUpdated, IGameSaveTraitData { public CPos GetRandomBaseCenter() { var randomConstructionYard = World.Actors.Where(a => a.Owner == Player && Info.ConstructionYardTypes.Contains(a.Info.Name)) .RandomOrDefault(World.LocalRandom); return randomConstructionYard?.Location ?? initialBaseCenter; } public readonly World World; public readonly Player Player; readonly Predicate unitCannotBeOrdered; readonly List unitsHangingAroundTheBase = new List(); // Units that the bot already knows about. Any unit not on this list needs to be given a role. readonly List activeUnits = new List(); public List Squads = new List(); IBot bot; IBotPositionsUpdated[] notifyPositionsUpdated; IBotNotifyIdleBaseUnits[] notifyIdleBaseUnits; CPos initialBaseCenter; int rushTicks; int assignRolesTicks; int attackForceTicks; int minAttackForceDelayTicks; public SquadManagerBotModule(Actor self, SquadManagerBotModuleInfo info) : base(info) { World = self.World; Player = self.Owner; unitCannotBeOrdered = a => a == null || a.Owner != Player || a.IsDead || !a.IsInWorld; } // Use for proactive targeting. public bool IsPreferredEnemyUnit(Actor a) { if (a == null || a.IsDead || Player.RelationshipWith(a.Owner) != PlayerRelationship.Enemy || a.Info.HasTraitInfo() || a.Info.HasTraitInfo()) return false; var targetTypes = a.GetEnabledTargetTypes(); return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes); } public bool IsNotHiddenUnit(Actor a) { var hasModifier = false; var visModifiers = a.TraitsImplementing(); foreach (var v in visModifiers) { if (v.IsVisible(a, Player)) return true; hasModifier = true; } return !hasModifier; } protected override void Created(Actor self) { notifyPositionsUpdated = self.Owner.PlayerActor.TraitsImplementing().ToArray(); notifyIdleBaseUnits = self.Owner.PlayerActor.TraitsImplementing().ToArray(); } protected override void TraitEnabled(Actor self) { // Avoid all AIs trying to rush in the same tick, randomize their initial rush a little. var smallFractionOfRushInterval = Info.RushInterval / 20; rushTicks = World.LocalRandom.Next(Info.RushInterval - smallFractionOfRushInterval, Info.RushInterval + smallFractionOfRushInterval); // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. assignRolesTicks = World.LocalRandom.Next(0, Info.AssignRolesInterval); attackForceTicks = World.LocalRandom.Next(0, Info.AttackForceInterval); minAttackForceDelayTicks = World.LocalRandom.Next(0, Info.MinimumAttackForceDelay); } void IBotEnabled.BotEnabled(IBot bot) { this.bot = bot; } void IBotTick.BotTick(IBot bot) { AssignRolesToIdleUnits(bot); } internal Actor FindClosestEnemy(WPos pos) { var units = World.Actors.Where(IsPreferredEnemyUnit); return units.Where(IsNotHiddenUnit).ClosestTo(pos) ?? units.ClosestTo(pos); } internal Actor FindClosestEnemy(WPos pos, WDist radius) { return World.FindActorsInCircle(pos, radius).Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a)).ClosestTo(pos); } void CleanSquads() { Squads.RemoveAll(s => !s.IsValid); foreach (var s in Squads) s.Units.RemoveAll(unitCannotBeOrdered); } // HACK: Use of this function requires that there is one squad of this type. Squad GetSquadOfType(SquadType type) { return Squads.FirstOrDefault(s => s.Type == type); } Squad RegisterNewSquad(IBot bot, SquadType type, Actor target = null) { var ret = new Squad(bot, this, type, target); Squads.Add(ret); return ret; } void AssignRolesToIdleUnits(IBot bot) { CleanSquads(); activeUnits.RemoveAll(unitCannotBeOrdered); unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); if (--rushTicks <= 0) { rushTicks = Info.RushInterval; TryToRushAttack(bot); } if (--attackForceTicks <= 0) { attackForceTicks = Info.AttackForceInterval; foreach (var s in Squads) s.Update(); } if (--assignRolesTicks <= 0) { assignRolesTicks = Info.AssignRolesInterval; FindNewUnits(bot); } if (--minAttackForceDelayTicks <= 0) { minAttackForceDelayTicks = Info.MinimumAttackForceDelay; CreateAttackForce(bot); } } void FindNewUnits(IBot bot) { var newUnits = World.ActorsHavingTrait() .Where(a => a.Owner == Player && !Info.ExcludeFromSquadsTypes.Contains(a.Info.Name) && !activeUnits.Contains(a)); foreach (var a in newUnits) { if (a.Info.HasTraitInfo() && a.Info.HasTraitInfo()) { var air = GetSquadOfType(SquadType.Air); if (air == null) air = RegisterNewSquad(bot, SquadType.Air); air.Units.Add(a); } else if (Info.NavalUnitsTypes.Contains(a.Info.Name)) { var ships = GetSquadOfType(SquadType.Naval); if (ships == null) ships = RegisterNewSquad(bot, SquadType.Naval); ships.Units.Add(a); } else unitsHangingAroundTheBase.Add(a); activeUnits.Add(a); } // Notifying here rather than inside the loop, should be fine and saves a bunch of notification calls foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); } void CreateAttackForce(IBot bot) { // Create an attack force when we have enough units around our base. // (don't bother leaving any behind for defense) var randomizedSquadSize = Info.SquadSize + World.LocalRandom.Next(Info.SquadSizeRandomBonus); if (unitsHangingAroundTheBase.Count >= randomizedSquadSize) { var attackForce = RegisterNewSquad(bot, SquadType.Assault); foreach (var a in unitsHangingAroundTheBase) attackForce.Units.Add(a); unitsHangingAroundTheBase.Clear(); foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); } } void TryToRushAttack(IBot bot) { var allEnemyBaseBuilder = AIUtils.FindEnemiesByCommonName(Info.ConstructionYardTypes, Player); // TODO: This should use common names & ExcludeFromSquads instead of hardcoding TraitInfo checks var ownUnits = activeUnits .Where(unit => unit.IsIdle && unit.Info.HasTraitInfo() && !unit.Info.HasTraitInfo() && !Info.NavalUnitsTypes.Contains(unit.Info.Name) && !unit.Info.HasTraitInfo()).ToList(); if (!allEnemyBaseBuilder.Any() || ownUnits.Count < Info.SquadSize) return; foreach (var b in allEnemyBaseBuilder) { // Don't rush enemy aircraft! var enemies = World.FindActorsInCircle(b.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) .Where(unit => IsPreferredEnemyUnit(unit) && unit.Info.HasTraitInfo() && !unit.Info.HasTraitInfo() && !Info.NavalUnitsTypes.Contains(unit.Info.Name)).ToList(); if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies)) { var target = enemies.Any() ? enemies.Random(World.LocalRandom) : b; var rush = GetSquadOfType(SquadType.Rush); if (rush == null) rush = RegisterNewSquad(bot, SquadType.Rush, target); foreach (var a3 in ownUnits) rush.Units.Add(a3); return; } } } void ProtectOwn(IBot bot, Actor attacker) { var protectSq = GetSquadOfType(SquadType.Protection); if (protectSq == null) protectSq = RegisterNewSquad(bot, SquadType.Protection, attacker); if (!protectSq.IsTargetValid) protectSq.TargetActor = attacker; if (!protectSq.IsValid) { var ownUnits = World.FindActorsInCircle(World.Map.CenterOfCell(GetRandomBaseCenter()), WDist.FromCells(Info.ProtectUnitScanRadius)) .Where(unit => unit.Owner == Player && !unit.Info.HasTraitInfo() && !unit.Info.HasTraitInfo() && unit.Info.HasTraitInfo()); foreach (var a in ownUnits) protectSq.Units.Add(a); } } void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) { initialBaseCenter = newLocation; } void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) { if (!IsPreferredEnemyUnit(e.Attacker)) return; // Protected priority assets, MCVs, harvesters and buildings // TODO: Use *CommonNames, instead of hard-coding trait(info)s. if (self.Info.HasTraitInfo() || self.Info.HasTraitInfo() || self.Info.HasTraitInfo()) { foreach (var n in notifyPositionsUpdated) n.UpdatedDefenseCenter(e.Attacker.Location); ProtectOwn(bot, e.Attacker); } } List IGameSaveTraitData.IssueTraitData(Actor self) { if (IsTraitDisabled) return null; return new List() { new MiniYamlNode("Squads", "", Squads.Select(s => new MiniYamlNode("Squad", s.Serialize())).ToList()), new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)), new MiniYamlNode("UnitsHangingAroundTheBase", FieldSaver.FormatValue(unitsHangingAroundTheBase .Where(a => !unitCannotBeOrdered(a)) .Select(a => a.ActorID) .ToArray())), new MiniYamlNode("ActiveUnits", FieldSaver.FormatValue(activeUnits .Where(a => !unitCannotBeOrdered(a)) .Select(a => a.ActorID) .ToArray())), new MiniYamlNode("RushTicks", FieldSaver.FormatValue(rushTicks)), new MiniYamlNode("AssignRolesTicks", FieldSaver.FormatValue(assignRolesTicks)), new MiniYamlNode("AttackForceTicks", FieldSaver.FormatValue(attackForceTicks)), new MiniYamlNode("MinAttackForceDelayTicks", FieldSaver.FormatValue(minAttackForceDelayTicks)), }; } void IGameSaveTraitData.ResolveTraitData(Actor self, List data) { if (self.World.IsReplay) return; var initialBaseCenterNode = data.FirstOrDefault(n => n.Key == "InitialBaseCenter"); if (initialBaseCenterNode != null) initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value.Value); var unitsHangingAroundTheBaseNode = data.FirstOrDefault(n => n.Key == "UnitsHangingAroundTheBase"); if (unitsHangingAroundTheBaseNode != null) { unitsHangingAroundTheBase.Clear(); unitsHangingAroundTheBase.AddRange(FieldLoader.GetValue("UnitsHangingAroundTheBase", unitsHangingAroundTheBaseNode.Value.Value) .Select(a => self.World.GetActorById(a)).Where(a => a != null)); } var activeUnitsNode = data.FirstOrDefault(n => n.Key == "ActiveUnits"); if (activeUnitsNode != null) { activeUnits.Clear(); activeUnits.AddRange(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value.Value) .Select(a => self.World.GetActorById(a)).Where(a => a != null)); } var rushTicksNode = data.FirstOrDefault(n => n.Key == "RushTicks"); if (rushTicksNode != null) rushTicks = FieldLoader.GetValue("RushTicks", rushTicksNode.Value.Value); var assignRolesTicksNode = data.FirstOrDefault(n => n.Key == "AssignRolesTicks"); if (assignRolesTicksNode != null) assignRolesTicks = FieldLoader.GetValue("AssignRolesTicks", assignRolesTicksNode.Value.Value); var attackForceTicksNode = data.FirstOrDefault(n => n.Key == "AttackForceTicks"); if (attackForceTicksNode != null) attackForceTicks = FieldLoader.GetValue("AttackForceTicks", attackForceTicksNode.Value.Value); var minAttackForceDelayTicksNode = data.FirstOrDefault(n => n.Key == "MinAttackForceDelayTicks"); if (minAttackForceDelayTicksNode != null) minAttackForceDelayTicks = FieldLoader.GetValue("MinAttackForceDelayTicks", minAttackForceDelayTicksNode.Value.Value); var squadsNode = data.FirstOrDefault(n => n.Key == "Squads"); if (squadsNode != null) { Squads.Clear(); foreach (var n in squadsNode.Value.Nodes) Squads.Add(Squad.Deserialize(bot, this, n.Value)); } } } }