diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 86745a90db..97af209425 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -365,6 +365,7 @@ namespace OpenRA.Traits void Activate(Player p); void QueueOrder(Order order); IBotInfo Info { get; } + Player Player { get; } } [RequireExplicitImplementation] diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs deleted file mode 100644 index 3df5f177ff..0000000000 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ /dev/null @@ -1,682 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2018 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; -using System.Collections.Generic; -using System.Linq; -using OpenRA.Mods.Common.Traits; -using OpenRA.Support; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.AI -{ - public sealed class HackyAIInfo : IBotInfo, ITraitInfo - { - public class UnitCategories - { - public readonly HashSet Mcv = new HashSet(); - public readonly HashSet NavalUnits = new HashSet(); - public readonly HashSet ExcludeFromSquads = new HashSet(); - } - - // TODO: Move this to SquadManagerBotModule later - public class BuildingCategories - { - public readonly HashSet ConstructionYard = new HashSet(); - public readonly HashSet VehiclesFactory = new HashSet(); - public readonly HashSet Refinery = new HashSet(); - public readonly HashSet Power = new HashSet(); - public readonly HashSet Barracks = new HashSet(); - public readonly HashSet Production = new HashSet(); - public readonly HashSet NavalProduction = new HashSet(); - public readonly HashSet Silo = new HashSet(); - } - - [FieldLoader.Require] - [Desc("Internal id for this bot.")] - public readonly string Type = null; - - [Desc("Human-readable name this bot uses.")] - public readonly string Name = "Unnamed Bot"; - - [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 = 20; - - [Desc("Delay (in ticks) between attempting rush attacks.")] - public readonly int RushInterval = 600; - - [Desc("Delay (in ticks) between updating squads.")] - public readonly int AttackForceInterval = 30; - - [Desc("Minimum delay (in ticks) between creating squads.")] - public readonly int MinimumAttackForceDelay = 0; - - [Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). Excess orders remain queued for subsequent ticks.")] - public readonly int MinOrderQuotientPerTick = 5; - - [Desc("Only produce units as long as there are less than this amount of units idling inside the base.")] - public readonly int IdleBaseUnitsMaximum = 12; - - [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("Minimum distance in cells from center of the base when checking for MCV deployment location.")] - public readonly int MinBaseRadius = 2; - - [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("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] - public readonly bool RestrictMCVDeploymentFallbackToBase = true; - - [Desc("Production queues AI uses for producing units.")] - public readonly HashSet UnitQueues = new HashSet { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; - - [Desc("What units to the AI should build.", "What % of the total army must be this type of unit.")] - public readonly Dictionary UnitsToBuild = null; - - [Desc("What units should the AI have a maximum limit to train.")] - public readonly Dictionary UnitLimits = null; - - [Desc("Tells the AI what unit types fall under the same common name. Supported entries are Mcv and ExcludeFromSquads.")] - [FieldLoader.LoadUsing("LoadUnitCategories", true)] - public readonly UnitCategories UnitsCommonNames; - - // TODO: Move this to SquadManagerBotModule later - [Desc("Tells the AI what building types fall under the same common name.", - "Possible keys are ConstructionYard, Power, Refinery, Silo, Barracks, Production, VehiclesFactory, NavalProduction.")] - [FieldLoader.LoadUsing("LoadBuildingCategories", true)] - public readonly BuildingCategories BuildingCommonNames; - - static object LoadUnitCategories(MiniYaml yaml) - { - var categories = yaml.Nodes.First(n => n.Key == "UnitsCommonNames"); - return FieldLoader.Load(categories.Value); - } - - // TODO: Move this to SquadManagerBotModule later - static object LoadBuildingCategories(MiniYaml yaml) - { - var categories = yaml.Nodes.First(n => n.Key == "BuildingCommonNames"); - return FieldLoader.Load(categories.Value); - } - - string IBotInfo.Type { get { return Type; } } - - string IBotInfo.Name { get { return Name; } } - - public object Create(ActorInitializer init) { return new HackyAI(this, init); } - } - - public sealed class HackyAI : ITick, IBot, INotifyDamage, IBotPositionsUpdated - { - // DEPRECATED: Modules should use World.LocalRandom. - public MersenneTwister Random { get; private set; } - public readonly HackyAIInfo Info; - - public CPos GetRandomBaseCenter() - { - var randomConstructionYard = World.Actors.Where(a => a.Owner == Player && - Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)) - .RandomOrDefault(Random); - - return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; - } - - public bool IsEnabled; - public List Squads = new List(); - public Player Player { get; private set; } - - readonly Queue orders = new Queue(); - - readonly Func isEnemyUnit; - readonly Predicate unitCannotBeOrdered; - - IBotTick[] tickModules; - IBotRespondToAttack[] attackResponseModules; - IBotPositionsUpdated[] positionsUpdatedModules; - - CPos initialBaseCenter; - int ticks; - - List unitsHangingAroundTheBase = new List(); - - // Units that the ai already knows about. Any unit not on this list needs to be given a role. - List activeUnits = new List(); - - public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. - - public readonly World World; - public Map Map { get { return World.Map; } } - IBotInfo IBot.Info { get { return Info; } } - - int rushTicks; - int assignRolesTicks; - int attackForceTicks; - int minAttackForceDelayTicks; - - public HackyAI(HackyAIInfo info, ActorInitializer init) - { - Info = info; - World = init.World; - - if (World.Type == WorldType.Editor) - return; - - isEnemyUnit = unit => - Player.Stances[unit.Owner] == Stance.Enemy - && !unit.Info.HasTraitInfo() - && unit.Info.HasTraitInfo(); - - unitCannotBeOrdered = a => a.Owner != Player || a.IsDead || !a.IsInWorld; - } - - // Called by the host's player creation code - public void Activate(Player p) - { - Player = p; - IsEnabled = true; - tickModules = p.PlayerActor.TraitsImplementing().ToArray(); - attackResponseModules = p.PlayerActor.TraitsImplementing().ToArray(); - positionsUpdatedModules = p.PlayerActor.TraitsImplementing().ToArray(); - - Random = new MersenneTwister(Game.CosmeticRandom.Next()); - - // Avoid all AIs trying to rush in the same tick, randomize their initial rush a little. - var smallFractionOfRushInterval = Info.RushInterval / 20; - rushTicks = Random.Next(Info.RushInterval - smallFractionOfRushInterval, Info.RushInterval + smallFractionOfRushInterval); - - // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. - assignRolesTicks = Random.Next(0, Info.AssignRolesInterval); - attackForceTicks = Random.Next(0, Info.AttackForceInterval); - minAttackForceDelayTicks = Random.Next(0, Info.MinimumAttackForceDelay); - } - - void IBot.QueueOrder(Order order) - { - orders.Enqueue(order); - } - - // DEPRECATED: Modules should use IBot.QueueOrder instead - public void QueueOrder(Order order) - { - orders.Enqueue(order); - } - - ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) - { - var buildableThings = queue.BuildableItems(); - if (!buildableThings.Any()) - return null; - - var unit = buildableThings.Random(Random); - return HasAdequateAirUnitReloadBuildings(unit) ? unit : null; - } - - ActorInfo ChooseUnitToBuild(ProductionQueue queue) - { - var buildableThings = queue.BuildableItems(); - if (!buildableThings.Any()) - return null; - - var myUnits = Player.World - .ActorsHavingTrait() - .Where(a => a.Owner == Player) - .Select(a => a.Info.Name).ToList(); - - foreach (var unit in Info.UnitsToBuild.Shuffle(Random)) - if (buildableThings.Any(b => b.Name == unit.Key)) - if (myUnits.Count(a => a == unit.Key) < unit.Value * myUnits.Count) - if (HasAdequateAirUnitReloadBuildings(Map.Rules.Actors[unit.Key])) - return Map.Rules.Actors[unit.Key]; - - return null; - } - - bool HasAdequateConstructionYardCount - { - get - { - // Require at least one construction yard, unless we have no vehicles factory (can't build it). - return AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 || - AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0; - } - } - - bool HasAdequateRefineryCount - { - get - { - // Require at least one refinery, unless we can't build it. - return AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 || - AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 || - AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) == 0; - } - } - - // For mods like RA (number of RearmActors must match the number of aircraft) - bool HasAdequateAirUnitReloadBuildings(ActorInfo actorInfo) - { - var aircraftInfo = actorInfo.TraitInfoOrDefault(); - if (aircraftInfo == null) - return true; - - // If actor isn't Rearmable, it doesn't need a RearmActor to reload - var rearmableInfo = actorInfo.TraitInfoOrDefault(); - if (rearmableInfo == null) - return true; - - var countOwnAir = AIUtils.CountActorsWithTrait(actorInfo.Name, Player); - var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait(b, Player)); - if (countOwnAir >= countBuildings) - return false; - - return true; - } - - CPos? ChooseMcvDeployLocation(string actorType, bool distanceToBaseIsImportant) - { - var actorInfo = World.Map.Rules.Actors[actorType]; - var bi = actorInfo.TraitInfoOrDefault(); - if (bi == null) - return null; - - // Find the buildable cell that is closest to pos and centered around center - Func findPos = (center, target, minRange, maxRange) => - { - var cells = World.Map.FindTilesInAnnulus(center, minRange, maxRange); - - // Sort by distance to target if we have one - if (center != target) - cells = cells.OrderBy(c => (c - target).LengthSquared); - else - cells = cells.Shuffle(World.LocalRandom); - - foreach (var cell in cells) - { - if (!World.CanPlaceBuilding(cell, actorInfo, bi, null)) - continue; - - if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(World, Player, actorInfo, cell)) - continue; - - return cell; - } - - return null; - }; - - var baseCenter = GetRandomBaseCenter(); - - return findPos(baseCenter, baseCenter, Info.MinBaseRadius, - distanceToBaseIsImportant ? Info.MaxBaseRadius : World.Map.Grid.MaximumTileSearchRange); - } - - void ITick.Tick(Actor self) - { - if (!IsEnabled) - return; - - ticks++; - - if (ticks == 1) - InitializeBase(self, false); - - if (ticks % FeedbackTime == 0) - ProductionUnits(self); - - AssignRolesToIdleUnits(self); - - using (new PerfSample("bot_tick")) - { - Sync.RunUnsynced(Game.Settings.Debug.SyncCheckBotModuleCode, World, () => - { - foreach (var t in tickModules) - if (t.IsTraitEnabled()) - t.BotTick(this); - }); - } - - var ordersToIssueThisTick = Math.Min((orders.Count + Info.MinOrderQuotientPerTick - 1) / Info.MinOrderQuotientPerTick, orders.Count); - for (var i = 0; i < ordersToIssueThisTick; i++) - World.IssueOrder(orders.Dequeue()); - } - - internal Actor FindClosestEnemy(WPos pos) - { - return World.Actors.Where(isEnemyUnit).ClosestTo(pos); - } - - internal Actor FindClosestEnemy(WPos pos, WDist radius) - { - return World.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestTo(pos); - } - - void CleanSquads() - { - Squads.RemoveAll(s => !s.IsValid); - foreach (var s in Squads) - s.Units.RemoveAll(unitCannotBeOrdered); - } - - // Use of this function requires that one squad of this type. Hence it is a piece of shit - Squad GetSquadOfType(SquadType type) - { - return Squads.FirstOrDefault(s => s.Type == type); - } - - Squad RegisterNewSquad(SquadType type, Actor target = null) - { - var ret = new Squad(this, type, target); - Squads.Add(ret); - return ret; - } - - void AssignRolesToIdleUnits(Actor self) - { - CleanSquads(); - - activeUnits.RemoveAll(unitCannotBeOrdered); - unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); - - if (--rushTicks <= 0) - { - rushTicks = Info.RushInterval; - TryToRushAttack(); - } - - if (--attackForceTicks <= 0) - { - attackForceTicks = Info.AttackForceInterval; - foreach (var s in Squads) - s.Update(); - } - - if (--assignRolesTicks <= 0) - { - assignRolesTicks = Info.AssignRolesInterval; - FindNewUnits(self); - InitializeBase(self, true); - } - - if (--minAttackForceDelayTicks <= 0) - { - minAttackForceDelayTicks = Info.MinimumAttackForceDelay; - CreateAttackForce(); - } - } - - void FindNewUnits(Actor self) - { - var newUnits = self.World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !activeUnits.Contains(a)); - - foreach (var a in newUnits) - { - if (Info.UnitsCommonNames.Mcv.Contains(a.Info.Name) || Info.UnitsCommonNames.ExcludeFromSquads.Contains(a.Info.Name)) - continue; - - unitsHangingAroundTheBase.Add(a); - - if (a.Info.HasTraitInfo() && a.Info.HasTraitInfo()) - { - var air = GetSquadOfType(SquadType.Air); - if (air == null) - air = RegisterNewSquad(SquadType.Air); - - air.Units.Add(a); - } - else if (Info.UnitsCommonNames.NavalUnits.Contains(a.Info.Name)) - { - var ships = GetSquadOfType(SquadType.Naval); - if (ships == null) - ships = RegisterNewSquad(SquadType.Naval); - - ships.Units.Add(a); - } - - activeUnits.Add(a); - } - } - - void CreateAttackForce() - { - // Create an attack force when we have enough units around our base. - // (don't bother leaving any behind for defense) - var randomizedSquadSize = Info.SquadSize + Random.Next(Info.SquadSizeRandomBonus); - - if (unitsHangingAroundTheBase.Count >= randomizedSquadSize) - { - var attackForce = RegisterNewSquad(SquadType.Assault); - - foreach (var a in unitsHangingAroundTheBase) - if (!a.Info.HasTraitInfo()) - attackForce.Units.Add(a); - - unitsHangingAroundTheBase.Clear(); - } - } - - void TryToRushAttack() - { - var allEnemyBaseBuilder = AIUtils.FindEnemiesByCommonName(Info.BuildingCommonNames.ConstructionYard, Player); - var ownUnits = activeUnits - .Where(unit => unit.IsIdle && unit.Info.HasTraitInfo() - && !unit.Info.HasTraitInfo() && !unit.Info.HasTraitInfo()).ToList(); - - if (!allEnemyBaseBuilder.Any() || (ownUnits.Count < Info.SquadSize)) - return; - - foreach (var b in allEnemyBaseBuilder) - { - var enemies = World.FindActorsInCircle(b.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) - .Where(unit => Player.Stances[unit.Owner] == Stance.Enemy && unit.Info.HasTraitInfo()).ToList(); - - if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies)) - { - var target = enemies.Any() ? enemies.Random(Random) : b; - var rush = GetSquadOfType(SquadType.Rush); - if (rush == null) - rush = RegisterNewSquad(SquadType.Rush, target); - - foreach (var a3 in ownUnits) - rush.Units.Add(a3); - - return; - } - } - } - - void ProtectOwn(Actor attacker) - { - var protectSq = GetSquadOfType(SquadType.Protection); - if (protectSq == null) - protectSq = RegisterNewSquad(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 InitializeBase(Actor self, bool chooseLocation) - { - var mcv = FindAndDeployMcv(self, chooseLocation); - - if (mcv == null) - return; - - Sync.RunUnsynced(Game.Settings.Debug.SyncCheckBotModuleCode, World, () => - { - foreach (var n in positionsUpdatedModules) - { - n.UpdatedBaseCenter(mcv.Location); - n.UpdatedDefenseCenter(mcv.Location); - } - }); - } - - void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) - { - initialBaseCenter = newLocation; - } - - void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } - - // Find any MCV and deploy them at a sensible location. - Actor FindAndDeployMcv(Actor self, bool move) - { - var mcv = self.World.Actors.FirstOrDefault(a => a.Owner == Player && - Info.UnitsCommonNames.Mcv.Contains(a.Info.Name) && a.IsIdle); - - if (mcv == null) - return null; - - // Don't try to move and deploy an undeployable actor - var transformsInfo = mcv.Info.TraitInfoOrDefault(); - if (transformsInfo == null) - return null; - - // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! - var restrictToBase = Info.RestrictMCVDeploymentFallbackToBase && - AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0; - - if (move) - { - var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, restrictToBase); - if (desiredLocation == null) - return null; - - QueueOrder(new Order("Move", mcv, Target.FromCell(World, desiredLocation.Value), true)); - } - - QueueOrder(new Order("DeployTransform", mcv, true)); - - return mcv; - } - - void ProductionUnits(Actor self) - { - // Stop building until economy is restored - if (!HasAdequateRefineryCount) - return; - - // No construction yards - Build a new MCV - if (Info.UnitsCommonNames.Mcv.Any() && HasAdequateConstructionYardCount && - !self.World.Actors.Any(a => a.Owner == Player && Info.UnitsCommonNames.Mcv.Contains(a.Info.Name))) - BuildUnit("Vehicle", AIUtils.GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name); - - foreach (var q in Info.UnitQueues) - BuildUnit(q, unitsHangingAroundTheBase.Count < Info.IdleBaseUnitsMaximum); - } - - void BuildUnit(string category, bool buildRandom) - { - // Pick a free queue - var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any()); - if (queue == null) - return; - - var unit = buildRandom ? - ChooseRandomUnitToBuild(queue) : - ChooseUnitToBuild(queue); - - if (unit == null) - return; - - var name = unit.Name; - - if (Info.UnitsToBuild != null && !Info.UnitsToBuild.ContainsKey(name)) - return; - - if (Info.UnitLimits != null && - Info.UnitLimits.ContainsKey(name) && - World.Actors.Count(a => a.Owner == Player && a.Info.Name == name) >= Info.UnitLimits[name]) - return; - - QueueOrder(Order.StartProduction(queue.Actor, name, 1)); - } - - void BuildUnit(string category, string name) - { - var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any()); - if (queue == null) - return; - - if (Map.Rules.Actors[name] != null) - QueueOrder(Order.StartProduction(queue.Actor, name, 1)); - } - - void INotifyDamage.Damaged(Actor self, AttackInfo e) - { - if (!IsEnabled) - return; - - // TODO: Add an option to include this in CheckSyncUnchanged. - // Checking sync for this is too expensive to include it by default, - // so it should be implemented as separate sub-option checkbox. - using (new PerfSample("bot_attack_response")) - { - Sync.RunUnsynced(Game.Settings.Debug.SyncCheckBotModuleCode, World, () => - { - foreach (var t in attackResponseModules) - if (t.IsTraitEnabled()) - t.RespondToAttack(this, self, e); - }); - } - - if (e.Attacker == null || e.Attacker.Disposed) - return; - - if (e.Attacker.Owner.Stances[self.Owner] != Stance.Enemy) - return; - - if (!e.Attacker.Info.HasTraitInfo()) - 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()) - ProtectOwn(e.Attacker); - } - } -} diff --git a/OpenRA.Mods.Common/AI/AIUtils.cs b/OpenRA.Mods.Common/AIUtils.cs similarity index 93% rename from OpenRA.Mods.Common/AI/AIUtils.cs rename to OpenRA.Mods.Common/AIUtils.cs index 2db7a533e1..d6d86c346b 100644 --- a/OpenRA.Mods.Common/AI/AIUtils.cs +++ b/OpenRA.Mods.Common/AIUtils.cs @@ -14,7 +14,7 @@ using System.Linq; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common { public enum BuildingType { Building, Defense, Refinery } @@ -68,6 +68,12 @@ namespace OpenRA.Mods.Common.AI return GetActorsWithTrait(owner.World).Count(a => a.Owner == owner && a.Info.Name == actorName); } + public static int CountActorByCommonName(HashSet commonNames, Player owner) + { + return owner.World.Actors.Count(a => !a.IsDead && a.Owner == owner && + commonNames.Contains(a.Info.Name)); + } + public static int CountBuildingByCommonName(HashSet buildings, Player owner) { return GetActorsWithTrait(owner.World) diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index af0c473e79..165b618a60 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -119,22 +119,25 @@ - - + + - + - - - - - - - + + + + + + + + + + @@ -811,7 +814,7 @@ - + diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index b87f0f7719..e2be758bf7 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -13,7 +13,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.AI; using OpenRA.Support; using OpenRA.Traits; @@ -123,7 +122,7 @@ namespace OpenRA.Mods.Common.Traits public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); } } - public class BaseBuilderBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IBotRespondToAttack + public class BaseBuilderBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IBotRespondToAttack, IBotRequestPauseUnitProduction { public CPos GetRandomBaseCenter() { @@ -181,6 +180,11 @@ namespace OpenRA.Mods.Common.Traits defenseCenter = newLocation; } + bool IBotRequestPauseUnitProduction.PauseUnitProduction + { + get { return !IsTraitDisabled && !HasAdequateRefineryCount; } + } + void IBotTick.BotTick(IBot bot) { SetRallyPointsForNewProductionBuildings(bot); @@ -200,8 +204,8 @@ namespace OpenRA.Mods.Common.Traits if (!e.Attacker.Info.HasTraitInfo()) return; - // Protected priority assets, MCVs, harvesters and buildings - if (self.Info.HasTraitInfo() || self.Info.HasTraitInfo()) + // Protect buildings + if (self.Info.HasTraitInfo()) foreach (var n in positionsUpdatedModules) n.UpdatedDefenseCenter(e.Attacker.Location); } @@ -240,5 +244,26 @@ namespace OpenRA.Mods.Common.Traits { return info != null && world.IsCellBuildable(x, null, info); } + + public bool HasAdequateRefineryCount + { + get + { + // Require at least one refinery, unless we can't build it. + return AIUtils.CountBuildingByCommonName(Info.RefineryTypes, player) >= MinimumRefineryCount || + AIUtils.CountBuildingByCommonName(Info.PowerTypes, player) == 0 || + AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) == 0; + } + } + + int MinimumRefineryCount + { + get + { + // Unless we have no barracks (higher priority), require a 2nd refinery. + // TODO: Possibly unhardcode this, at least the targeted minimum of 2 (the fallback can probably stay at 1). + return AIUtils.CountBuildingByCommonName(Info.BarracksTypes, player) > 0 ? 2 : 1; + } + } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 0ce0524d10..c83150fdad 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -13,7 +13,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.AI; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -219,7 +218,7 @@ namespace OpenRA.Mods.Common.Traits } // Next is to build up a strong economy - if (!HasAdequateRefineryCount) + if (!baseBuilder.HasAdequateRefineryCount) { var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) @@ -410,26 +409,5 @@ namespace OpenRA.Mods.Common.Traits // Can't find a build location return null; } - - bool HasAdequateRefineryCount - { - get - { - // Require at least one refinery, unless we can't build it. - return AIUtils.CountBuildingByCommonName(baseBuilder.Info.RefineryTypes, player) >= MinimumRefineryCount || - AIUtils.CountBuildingByCommonName(baseBuilder.Info.PowerTypes, player) == 0 || - AIUtils.CountBuildingByCommonName(baseBuilder.Info.ConstructionYardTypes, player) == 0; - } - } - - int MinimumRefineryCount - { - get - { - // Unless we have no barracks (higher priority), require a 2nd refinery. - // TODO: Possibly unhardcode this, at least the targeted minimum of 2 (the fallback can probably stay at 1). - return AIUtils.CountBuildingByCommonName(baseBuilder.Info.BarracksTypes, player) > 0 ? 2 : 1; - } - } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/SupportPowerDecision.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/SupportPowerDecision.cs index 210b02feb5..4e8538d0ee 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/SupportPowerDecision.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/SupportPowerDecision.cs @@ -10,7 +10,6 @@ #endregion using System.Collections.Generic; -using OpenRA.Mods.Common.AI; using OpenRA.Primitives; using OpenRA.Traits; diff --git a/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs index 69863e9d2d..90e1c2d262 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs @@ -9,7 +9,6 @@ */ #endregion -using OpenRA.Mods.Common.AI; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits diff --git a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs index 34209f3aeb..a651c467e1 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs @@ -12,7 +12,6 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.AI; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits diff --git a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs index a90f7257bf..192c4b7491 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.AI; using OpenRA.Mods.Common.Pathfinder; using OpenRA.Traits; diff --git a/OpenRA.Mods.Common/Traits/BotModules/McvManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/McvManagerBotModule.cs new file mode 100644 index 0000000000..c035738730 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/McvManagerBotModule.cs @@ -0,0 +1,222 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Manages AI MCVs.")] + public class McvManagerBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that are considered MCVs (deploy into base builders).")] + public readonly HashSet McvTypes = new HashSet(); + + [Desc("Actor types that are considered construction yards (base builders).")] + public readonly HashSet ConstructionYardTypes = new HashSet(); + + [Desc("Actor types that are able to produce MCVs.")] + public readonly HashSet McvFactoryTypes = new HashSet(); + + [Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")] + public readonly int MinimumConstructionYardCount = 1; + + [Desc("Delay (in ticks) between looking for and giving out orders to new MCVs.")] + public readonly int ScanForNewMcvInterval = 20; + + [Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")] + public readonly int MinBaseRadius = 2; + + [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("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] + public readonly bool RestrictMCVDeploymentFallbackToBase = true; + + public override object Create(ActorInitializer init) { return new McvManagerBotModule(init.Self, this); } + } + + public class McvManagerBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated + { + public CPos GetRandomBaseCenter() + { + var randomConstructionYard = world.Actors.Where(a => a.Owner == player && + Info.ConstructionYardTypes.Contains(a.Info.Name)) + .RandomOrDefault(world.LocalRandom); + + return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; + } + + readonly World world; + readonly Player player; + + readonly Predicate unitCannotBeOrdered; + + IBotPositionsUpdated[] notifyPositionsUpdated; + IBotRequestUnitProduction[] requestUnitProduction; + + CPos initialBaseCenter; + int scanInterval; + int ticks; + + // MCVs that the bot already knows about. Any MCV not on this list needs to be given an order. + List activeMCVs = new List(); + + public McvManagerBotModule(Actor self, McvManagerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + unitCannotBeOrdered = a => a.Owner != player || a.IsDead || !a.IsInWorld; + } + + protected override void TraitEnabled(Actor self) + { + notifyPositionsUpdated = player.PlayerActor.TraitsImplementing().ToArray(); + requestUnitProduction = player.PlayerActor.TraitsImplementing().ToArray(); + + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval * 2); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + void IBotTick.BotTick(IBot bot) + { + ticks++; + + if (ticks == 1) + DeployMcvs(bot, false); + + if (--scanInterval <= 0) + { + scanInterval = Info.ScanForNewMcvInterval; + DeployMcvs(bot, true); + + // No construction yards - Build a new MCV + if (ShouldBuildMCV()) + { + var unitBuilder = requestUnitProduction.FirstOrDefault(Exts.IsTraitEnabled); + if (unitBuilder != null) + { + var mcvInfo = AIUtils.GetInfoByCommonName(Info.McvTypes, player); + unitBuilder.RequestUnitProduction(bot, mcvInfo.Name); + } + } + } + } + + bool ShouldBuildMCV() + { + // Only build MCV if we don't already have one in the field. + var allowedToBuildMCV = AIUtils.CountActorByCommonName(Info.McvTypes, player) == 0; + if (!allowedToBuildMCV) + return false; + + // Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it). + return AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) < Info.MinimumConstructionYardCount && + AIUtils.CountBuildingByCommonName(Info.McvFactoryTypes, player) > 0; + } + + void DeployMcvs(IBot bot, bool chooseLocation) + { + activeMCVs.RemoveAll(unitCannotBeOrdered); + + var newMCVs = world.ActorsHavingTrait() + .Where(a => a.Owner == player && + a.IsIdle && + Info.McvTypes.Contains(a.Info.Name) && + !activeMCVs.Contains(a)); + + foreach (var a in newMCVs) + activeMCVs.Add(a); + + foreach (var mcv in activeMCVs) + DeployMcv(bot, mcv, chooseLocation); + } + + // Find any MCV and deploy them at a sensible location. + void DeployMcv(IBot bot, Actor mcv, bool move) + { + if (move) + { + // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! + var restrictToBase = Info.RestrictMCVDeploymentFallbackToBase && AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) > 0; + + var transformsInfo = mcv.Info.TraitInfo(); + var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase); + if (desiredLocation == null) + return; + + bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true)); + } + + // If the MCV has to move first, we can't be sure it reaches the destination alive, so we only + // update base and defense center if the MCV is deployed immediately (i.e. at game start). + // TODO: This could be adressed via INotifyTransform. + foreach (var n in notifyPositionsUpdated) + { + n.UpdatedBaseCenter(mcv.Location); + n.UpdatedDefenseCenter(mcv.Location); + } + + bot.QueueOrder(new Order("DeployTransform", mcv, true)); + } + + CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant) + { + var actorInfo = world.Map.Rules.Actors[actorType]; + var bi = actorInfo.TraitInfoOrDefault(); + if (bi == null) + return null; + + // Find the buildable cell that is closest to pos and centered around center + Func findPos = (center, target, minRange, maxRange) => + { + var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); + + // Sort by distance to target if we have one + if (center != target) + cells = cells.OrderBy(c => (c - target).LengthSquared); + else + cells = cells.Shuffle(world.LocalRandom); + + foreach (var cell in cells) + { + if (!world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) + continue; + + if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(world, player, actorInfo, cell)) + continue; + + return cell; + } + + return null; + }; + + var baseCenter = GetRandomBaseCenter(); + + return findPos(baseCenter, baseCenter, Info.MinBaseRadius, + distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs new file mode 100644 index 0000000000..37a28aa3db --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs @@ -0,0 +1,355 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits.BotModules.Squads; +using OpenRA.Support; +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 = 20; + + [Desc("Delay (in ticks) between attempting rush attacks.")] + public readonly int RushInterval = 600; + + [Desc("Delay (in ticks) between updating squads.")] + public readonly int AttackForceInterval = 30; + + [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("Maximum range at which to scan for enemies when creating protection squads.")] + public readonly int MaximumDefenseRadius = 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; + + public override object Create(ActorInitializer init) { return new SquadManagerBotModule(init.Self, this); } + } + + public class SquadManagerBotModule : ConditionalTrait, IBotTick, IBotRespondToAttack, IBotPositionsUpdated + { + public CPos GetRandomBaseCenter() + { + var randomConstructionYard = World.Actors.Where(a => a.Owner == Player && + Info.ConstructionYardTypes.Contains(a.Info.Name)) + .RandomOrDefault(World.LocalRandom); + + return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; + } + + public readonly World World; + public readonly Player Player; + + readonly Func isEnemyUnit; + readonly Predicate unitCannotBeOrdered; + + public List Squads = new List(); + + IBotPositionsUpdated[] notifyPositionsUpdated; + IBotNotifyIdleBaseUnits[] notifyIdleBaseUnits; + + CPos initialBaseCenter; + List unitsHangingAroundTheBase = new List(); + + // Units that the bot already knows about. Any unit not on this list needs to be given a role. + List activeUnits = new List(); + + int rushTicks; + int assignRolesTicks; + int attackForceTicks; + int minAttackForceDelayTicks; + + public SquadManagerBotModule(Actor self, SquadManagerBotModuleInfo info) + : base(info) + { + World = self.World; + Player = self.Owner; + + isEnemyUnit = unit => + Player.Stances[unit.Owner] == Stance.Enemy + && !unit.Info.HasTraitInfo() + && unit.Info.HasTraitInfo(); + + unitCannotBeOrdered = a => a.Owner != Player || a.IsDead || !a.IsInWorld; + } + + protected override void TraitEnabled(Actor self) + { + notifyPositionsUpdated = Player.PlayerActor.TraitsImplementing().ToArray(); + notifyIdleBaseUnits = Player.PlayerActor.TraitsImplementing().ToArray(); + + // 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 IBotTick.BotTick(IBot bot) + { + AssignRolesToIdleUnits(bot); + } + + internal Actor FindClosestEnemy(WPos pos) + { + return World.Actors.Where(isEnemyUnit).ClosestTo(pos); + } + + internal Actor FindClosestEnemy(WPos pos, WDist radius) + { + return World.FindActorsInCircle(pos, radius).Where(isEnemyUnit).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) + { + unitsHangingAroundTheBase.Add(a); + + 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); + } + + 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) + if (!a.Info.HasTraitInfo()) + 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() && !unit.Info.HasTraitInfo()).ToList(); + + if (!allEnemyBaseBuilder.Any() || ownUnits.Count < Info.SquadSize) + return; + + foreach (var b in allEnemyBaseBuilder) + { + var enemies = World.FindActorsInCircle(b.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) + .Where(unit => Player.Stances[unit.Owner] == Stance.Enemy && unit.Info.HasTraitInfo()).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 (e.Attacker == null) + return; + + if (e.Attacker.Disposed) + return; + + if (e.Attacker.Owner.Stances[self.Owner] != Stance.Enemy) + return; + + if (!e.Attacker.Info.HasTraitInfo()) + 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); + } + } + } +} diff --git a/OpenRA.Mods.Common/AI/AttackOrFleeFuzzy.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/AttackOrFleeFuzzy.cs similarity index 99% rename from OpenRA.Mods.Common/AI/AttackOrFleeFuzzy.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/AttackOrFleeFuzzy.cs index f16791842f..0ecf669229 100644 --- a/OpenRA.Mods.Common/AI/AttackOrFleeFuzzy.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/AttackOrFleeFuzzy.cs @@ -13,11 +13,10 @@ using System; using System.Collections.Generic; using System.Linq; using AI.Fuzzy.Library; -using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Warheads; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { sealed class AttackOrFleeFuzzy { diff --git a/OpenRA.Mods.Common/AI/Squad.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs similarity index 82% rename from OpenRA.Mods.Common/AI/Squad.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs index 506afeb907..773e38fa59 100644 --- a/OpenRA.Mods.Common/AI/Squad.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs @@ -11,11 +11,10 @@ using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.Traits; using OpenRA.Support; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { public enum SquadType { Assault, Air, Rush, Protection, Naval } @@ -24,20 +23,22 @@ namespace OpenRA.Mods.Common.AI public List Units = new List(); public SquadType Type; + internal IBot Bot; internal World World; - internal HackyAI Bot; + internal SquadManagerBotModule SquadManager; internal MersenneTwister Random; internal Target Target; internal StateMachine FuzzyStateMachine; - public Squad(HackyAI bot, SquadType type) : this(bot, type, null) { } + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) : this(bot, squadManager, type, null) { } - public Squad(HackyAI bot, SquadType type, Actor target) + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, Actor target) { Bot = bot; - World = bot.World; - Random = bot.Random; + SquadManager = squadManager; + World = bot.Player.PlayerActor.World; + Random = World.LocalRandom; Type = type; Target = Target.FromActor(target); FuzzyStateMachine = new StateMachine(); diff --git a/OpenRA.Mods.Common/AI/StateMachine.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs similarity index 95% rename from OpenRA.Mods.Common/AI/StateMachine.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs index 9a3d34d486..144dc241aa 100644 --- a/OpenRA.Mods.Common/AI/StateMachine.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs @@ -9,7 +9,7 @@ */ #endregion -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { class StateMachine { diff --git a/OpenRA.Mods.Common/AI/States/AirStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs similarity index 96% rename from OpenRA.Mods.Common/AI/States/AirStates.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs index d836644198..eee435bb0a 100644 --- a/OpenRA.Mods.Common/AI/States/AirStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs @@ -12,11 +12,10 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class AirStateBase : StateBase { @@ -64,7 +63,7 @@ namespace OpenRA.Mods.Common.AI protected static CPos? FindSafePlace(Squad owner, out Actor detectedEnemyTarget, bool needTarget) { var map = owner.World.Map; - var dangerRadius = owner.Bot.Info.DangerScanRadius; + var dangerRadius = owner.SquadManager.Info.DangerScanRadius; detectedEnemyTarget = null; var x = (map.MapSize.X % dangerRadius) == 0 ? map.MapSize.X : map.MapSize.X + dangerRadius; var y = (map.MapSize.Y % dangerRadius) == 0 ? map.MapSize.Y : map.MapSize.Y + dangerRadius; @@ -96,7 +95,7 @@ namespace OpenRA.Mods.Common.AI protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedEnemyTarget) { detectedEnemyTarget = null; - var dangerRadius = owner.Bot.Info.DangerScanRadius; + var dangerRadius = owner.SquadManager.Info.DangerScanRadius; var unitsAroundPos = owner.World.FindActorsInCircle(loc, WDist.FromCells(dangerRadius)) .Where(unit => owner.Bot.Player.Stances[unit.Owner] == Stance.Enemy).ToList(); @@ -200,7 +199,7 @@ namespace OpenRA.Mods.Common.AI if (!owner.IsTargetValid) { var a = owner.Units.Random(owner.Random); - var closestEnemy = owner.Bot.FindClosestEnemy(a.CenterPosition); + var closestEnemy = owner.SquadManager.FindClosestEnemy(a.CenterPosition); if (closestEnemy != null) owner.TargetActor = closestEnemy; else diff --git a/OpenRA.Mods.Common/AI/States/GroundStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs similarity index 94% rename from OpenRA.Mods.Common/AI/States/GroundStates.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs index 04100ec3cf..150781b0db 100644 --- a/OpenRA.Mods.Common/AI/States/GroundStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs @@ -12,7 +12,7 @@ using System.Linq; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class GroundStateBase : StateBase { @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.AI protected Actor FindClosestEnemy(Squad owner) { - return owner.Bot.FindClosestEnemy(owner.Units.First().CenterPosition); + return owner.SquadManager.FindClosestEnemy(owner.Units.First().CenterPosition); } } @@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.AI owner.TargetActor = closestEnemy; } - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.Bot.Info.IdleScanRadius)) + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) .Where(unit => owner.Bot.Player.Stances[unit.Owner] == Stance.Enemy).ToList(); if (enemyUnits.Count == 0) @@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.AI } else { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Bot.Info.AttackScanRadius)) + var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) .Where(a => !a.IsDead && leader.Owner.Stances[a.Owner] == Stance.Enemy && !a.GetEnabledTargetTypes().IsEmpty); var target = enemies.ClosestTo(leader.CenterPosition); if (target != null) diff --git a/OpenRA.Mods.Common/AI/States/NavyStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs similarity index 93% rename from OpenRA.Mods.Common/AI/States/NavyStates.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs index 9caec881c0..462638e368 100644 --- a/OpenRA.Mods.Common/AI/States/NavyStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs @@ -10,10 +10,9 @@ #endregion using System.Linq; -using OpenRA.Mods.Common.Traits; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class NavyStateBase : StateBase { @@ -33,7 +32,7 @@ namespace OpenRA.Mods.Common.AI var locomotorInfo = first.Info.TraitInfo().LocomotorInfo; var navalProductions = owner.World.ActorsHavingTrait().Where(a - => owner.Bot.Info.BuildingCommonNames.NavalProduction.Contains(a.Info.Name) + => owner.SquadManager.Info.NavalProductionTypes.Contains(a.Info.Name) && domainIndex.IsPassable(first.Location, a.Location, locomotorInfo) && a.AppearsHostileTo(first)); @@ -45,11 +44,11 @@ namespace OpenRA.Mods.Common.AI // If the naval production is within MaxBaseRadius, it implies that // this squad is close to enemy territory and they should expect a naval combat; // closest enemy makes more sense in that case. - if ((nearest.Location - first.Location).LengthSquared > owner.Bot.Info.MaxBaseRadius * owner.Bot.Info.MaxBaseRadius) + if ((nearest.Location - first.Location).LengthSquared > owner.SquadManager.Info.MaxBaseRadius * owner.SquadManager.Info.MaxBaseRadius) return nearest; } - return owner.Bot.FindClosestEnemy(first.CenterPosition); + return owner.SquadManager.FindClosestEnemy(first.CenterPosition); } } @@ -71,7 +70,7 @@ namespace OpenRA.Mods.Common.AI owner.TargetActor = closestEnemy; } - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.Bot.Info.IdleScanRadius)) + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) .Where(unit => owner.Bot.Player.Stances[unit.Owner] == Stance.Enemy).ToList(); if (enemyUnits.Count == 0) @@ -130,7 +129,7 @@ namespace OpenRA.Mods.Common.AI } else { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Bot.Info.AttackScanRadius)) + var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) .Where(a => !a.IsDead && leader.Owner.Stances[a.Owner] == Stance.Enemy && !a.GetEnabledTargetTypes().IsEmpty); var target = enemies.ClosestTo(leader.CenterPosition); if (target != null) diff --git a/OpenRA.Mods.Common/AI/States/ProtectionStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs similarity index 90% rename from OpenRA.Mods.Common/AI/States/ProtectionStates.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs index 6bdfe7a2cd..d5650120ae 100644 --- a/OpenRA.Mods.Common/AI/States/ProtectionStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs @@ -11,7 +11,7 @@ using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { class UnitsForProtectionIdleState : GroundStateBase, IState { @@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.AI if (!owner.IsTargetValid) { - owner.TargetActor = owner.Bot.FindClosestEnemy(owner.CenterPosition, WDist.FromCells(owner.Bot.Info.ProtectionScanRadius)); + owner.TargetActor = owner.SquadManager.FindClosestEnemy(owner.CenterPosition, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); if (owner.TargetActor == null) { diff --git a/OpenRA.Mods.Common/AI/States/StateBase.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs similarity index 93% rename from OpenRA.Mods.Common/AI/States/StateBase.cs rename to OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs index 662fcc6896..41c50eb241 100644 --- a/OpenRA.Mods.Common/AI/States/StateBase.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs @@ -13,10 +13,9 @@ using System; using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.Traits; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class StateBase { @@ -29,7 +28,7 @@ namespace OpenRA.Mods.Common.AI protected static CPos RandomBuildingLocation(Squad squad) { - var location = squad.Bot.GetRandomBaseCenter(); + var location = squad.SquadManager.GetRandomBaseCenter(); var buildings = squad.World.ActorsHavingTrait() .Where(a => a.Owner == squad.Bot.Player).ToList(); if (buildings.Count > 0) @@ -87,7 +86,7 @@ namespace OpenRA.Mods.Common.AI return false; var randomSquadUnit = squad.Units.Random(squad.Random); - var dangerRadius = squad.Bot.Info.DangerScanRadius; + var dangerRadius = squad.SquadManager.Info.DangerScanRadius; var units = squad.World.FindActorsInCircle(randomSquadUnit.CenterPosition, WDist.FromCells(dangerRadius)).ToList(); // If there are any own buildings within the DangerRadius, don't flee diff --git a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs index 626bff9980..fbcf250fc7 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.AI; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits diff --git a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs new file mode 100644 index 0000000000..ca9bf31fdf --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs @@ -0,0 +1,209 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Controls AI unit production.")] + public class UnitBuilderBotModuleInfo : ConditionalTraitInfo + { + // TODO: Investigate whether this might the (or at least one) reason why bots occasionally get into a state of doing nothing. + // Reason: If this is less than SquadSize, the bot might get stuck between not producing more units due to this, + // but also not creating squads since there aren't enough idle units. + [Desc("Only produce units as long as there are less than this amount of units idling inside the base.")] + public readonly int IdleBaseUnitsMaximum = 12; + + [Desc("Production queues AI uses for producing units.")] + public readonly HashSet UnitQueues = new HashSet { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; + + [Desc("What units to the AI should build.", "What relative share of the total army must be this type of unit.")] + public readonly Dictionary UnitsToBuild = null; + + [Desc("What units should the AI have a maximum limit to train.")] + public readonly Dictionary UnitLimits = null; + + public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); } + } + + public class UnitBuilderBotModule : ConditionalTrait, IBotTick, IBotNotifyIdleBaseUnits, IBotRequestUnitProduction + { + public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. + + readonly World world; + readonly Player player; + + readonly List queuedBuildRequests = new List(); + + IBotRequestPauseUnitProduction[] requestPause; + + List idleUnits = new List(); + + int ticks; + + public UnitBuilderBotModule(Actor self, UnitBuilderBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + } + + protected override void TraitEnabled(Actor self) + { + requestPause = player.PlayerActor.TraitsImplementing().ToArray(); + } + + void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) + { + this.idleUnits = idleUnits; + } + + void IBotTick.BotTick(IBot bot) + { + if (requestPause.Any(rp => rp.PauseUnitProduction)) + return; + + ticks++; + + if (ticks % FeedbackTime == 0) + { + var buildRequest = queuedBuildRequests.FirstOrDefault(); + if (buildRequest != null) + { + BuildUnit(bot, buildRequest); + queuedBuildRequests.Remove(buildRequest); + } + + foreach (var q in Info.UnitQueues) + BuildUnit(bot, q, idleUnits.Count < Info.IdleBaseUnitsMaximum); + } + } + + void IBotRequestUnitProduction.RequestUnitProduction(IBot bot, string requestedActor) + { + queuedBuildRequests.Add(requestedActor); + } + + void BuildUnit(IBot bot, string category, bool buildRandom) + { + // Pick a free queue + var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any()); + if (queue == null) + return; + + var unit = buildRandom ? + ChooseRandomUnitToBuild(queue) : + ChooseUnitToBuild(queue); + + if (unit == null) + return; + + var name = unit.Name; + + if (Info.UnitsToBuild != null && !Info.UnitsToBuild.ContainsKey(name)) + return; + + if (Info.UnitLimits != null && + Info.UnitLimits.ContainsKey(name) && + world.Actors.Count(a => a.Owner == player && a.Info.Name == name) >= Info.UnitLimits[name]) + return; + + bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1)); + } + + void BuildUnit(IBot bot, string category, string name) + { + var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any()); + if (queue == null) + return; + + if (world.Map.Rules.Actors[name] != null) + bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1)); + } + + // In cases where we want to build a specific unit but don't know the queue name (because there's more than one possibility) + void BuildUnit(IBot bot, string name) + { + var actorInfo = world.Map.Rules.Actors[name]; + if (actorInfo == null) + return; + + var buildableInfo = actorInfo.TraitInfoOrDefault(); + if (buildableInfo == null) + return; + + ProductionQueue queue = null; + foreach (var pq in buildableInfo.Queue) + { + queue = AIUtils.FindQueues(player, pq).FirstOrDefault(q => !q.AllQueued().Any()); + if (queue != null) + break; + } + + if (queue != null) + bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1)); + } + + ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) + { + var buildableThings = queue.BuildableItems(); + if (!buildableThings.Any()) + return null; + + var unit = buildableThings.Random(world.LocalRandom); + return HasAdequateAirUnitReloadBuildings(unit) ? unit : null; + } + + ActorInfo ChooseUnitToBuild(ProductionQueue queue) + { + var buildableThings = queue.BuildableItems(); + if (!buildableThings.Any()) + return null; + + var myUnits = player.World + .ActorsHavingTrait() + .Where(a => a.Owner == player) + .Select(a => a.Info.Name).ToList(); + + foreach (var unit in Info.UnitsToBuild.Shuffle(world.LocalRandom)) + if (buildableThings.Any(b => b.Name == unit.Key)) + if (myUnits.Count(a => a == unit.Key) * 100 < unit.Value * myUnits.Count) + if (HasAdequateAirUnitReloadBuildings(world.Map.Rules.Actors[unit.Key])) + return world.Map.Rules.Actors[unit.Key]; + + return null; + } + + // For mods like RA (number of RearmActors must match the number of aircraft) + bool HasAdequateAirUnitReloadBuildings(ActorInfo actorInfo) + { + var aircraftInfo = actorInfo.TraitInfoOrDefault(); + if (aircraftInfo == null) + return true; + + // If actor isn't Rearmable, it doesn't need a RearmActor to reload + var rearmableInfo = actorInfo.TraitInfoOrDefault(); + if (rearmableInfo == null) + return true; + + var countOwnAir = AIUtils.CountActorsWithTrait(actorInfo.Name, player); + var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait(b, player)); + if (countOwnAir >= countBuildings) + return false; + + return true; + } + } +} diff --git a/OpenRA.Mods.Common/AI/DummyAI.cs b/OpenRA.Mods.Common/Traits/Player/DummyBot.cs similarity index 70% rename from OpenRA.Mods.Common/AI/DummyAI.cs rename to OpenRA.Mods.Common/Traits/Player/DummyBot.cs index 4ae4329576..4e1dfab468 100644 --- a/OpenRA.Mods.Common/AI/DummyAI.cs +++ b/OpenRA.Mods.Common/Traits/Player/DummyBot.cs @@ -11,9 +11,10 @@ using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits { - public sealed class DummyAIInfo : ITraitInfo, IBotInfo + [Desc("A placeholder bot that doesn't do anything.")] + public sealed class DummyBotInfo : ITraitInfo, IBotInfo { [Desc("Human-readable name this bot uses.")] public readonly string Name = "Unnamed Bot"; @@ -26,26 +27,29 @@ namespace OpenRA.Mods.Common.AI string IBotInfo.Name { get { return Name; } } - public object Create(ActorInitializer init) { return new DummyAI(this); } + public object Create(ActorInitializer init) { return new DummyBot(this); } } - public sealed class DummyAI : IBot + public sealed class DummyBot : IBot { - readonly DummyAIInfo info; - public bool Enabled { get; private set; } + readonly DummyBotInfo info; + public bool IsEnabled { get; private set; } + Player player; - public DummyAI(DummyAIInfo info) + public DummyBot(DummyBotInfo info) { this.info = info; } void IBot.Activate(Player p) { - Enabled = true; + IsEnabled = true; + player = p; } void IBot.QueueOrder(Order order) { } IBotInfo IBot.Info { get { return info; } } + Player IBot.Player { get { return player; } } } } diff --git a/OpenRA.Mods.Common/Traits/Player/ModularBot.cs b/OpenRA.Mods.Common/Traits/Player/ModularBot.cs new file mode 100644 index 0000000000..0ab6fe8f45 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Player/ModularBot.cs @@ -0,0 +1,113 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Bot that uses BotModules.")] + public sealed class ModularBotInfo : IBotInfo, ITraitInfo + { + [FieldLoader.Require] + [Desc("Internal id for this bot.")] + public readonly string Type = null; + + [Desc("Human-readable name this bot uses.")] + public readonly string Name = "Unnamed Bot"; + + [Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). Excess orders remain queued for subsequent ticks.")] + public readonly int MinOrderQuotientPerTick = 5; + + string IBotInfo.Type { get { return Type; } } + + string IBotInfo.Name { get { return Name; } } + + public object Create(ActorInitializer init) { return new ModularBot(this, init); } + } + + public sealed class ModularBot : ITick, IBot, INotifyDamage + { + public bool IsEnabled; + + readonly ModularBotInfo info; + readonly World world; + readonly Queue orders = new Queue(); + + Player player; + + IBotTick[] tickModules; + IBotRespondToAttack[] attackResponseModules; + + IBotInfo IBot.Info { get { return info; } } + Player IBot.Player { get { return player; } } + + public ModularBot(ModularBotInfo info, ActorInitializer init) + { + this.info = info; + this.world = init.World; + } + + // Called by the host's player creation code + public void Activate(Player p) + { + IsEnabled = true; + player = p; + tickModules = p.PlayerActor.TraitsImplementing().ToArray(); + attackResponseModules = p.PlayerActor.TraitsImplementing().ToArray(); + } + + void IBot.QueueOrder(Order order) + { + orders.Enqueue(order); + } + + void ITick.Tick(Actor self) + { + if (!IsEnabled) + return; + + using (new PerfSample("bot_tick")) + { + Sync.RunUnsynced(Game.Settings.Debug.SyncCheckBotModuleCode, world, () => + { + foreach (var t in tickModules) + if (t.IsTraitEnabled()) + t.BotTick(this); + }); + } + + var ordersToIssueThisTick = Math.Min((orders.Count + info.MinOrderQuotientPerTick - 1) / info.MinOrderQuotientPerTick, orders.Count); + for (var i = 0; i < ordersToIssueThisTick; i++) + world.IssueOrder(orders.Dequeue()); + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (!IsEnabled) + return; + + using (new PerfSample("bot_attack_response")) + { + Sync.RunUnsynced(Game.Settings.Debug.SyncCheckBotModuleCode, world, () => + { + foreach (var t in attackResponseModules) + if (t.IsTraitEnabled()) + t.RespondToAttack(this, self, e); + }); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs b/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs index 1c45227b28..ab99e51a96 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs @@ -13,7 +13,6 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using OpenRA.Graphics; -using OpenRA.Mods.Common.AI; using OpenRA.Mods.Common.Graphics; using OpenRA.Traits; @@ -27,13 +26,13 @@ namespace OpenRA.Mods.Common.Traits.Render public object Create(ActorInitializer init) { return new RenderDebugState(init.Self, this); } } - class RenderDebugState : INotifyAddedToWorld, INotifyOwnerChanged, IRenderAboveShroudWhenSelected + class RenderDebugState : INotifyAddedToWorld, INotifyOwnerChanged, INotifyCreated, IRenderAboveShroudWhenSelected { readonly DebugVisualizations debugVis; readonly SpriteFont font; readonly Actor self; readonly WVec offset; - readonly HackyAI ai; + SquadManagerBotModule ai; Color color; string tagString; @@ -49,7 +48,11 @@ namespace OpenRA.Mods.Common.Traits.Render font = Game.Renderer.Fonts[info.Font]; debugVis = self.World.WorldActor.TraitOrDefault(); - ai = self.Owner.PlayerActor.TraitsImplementing().FirstOrDefault(x => x.IsEnabled); + } + + void INotifyCreated.Created(Actor self) + { + ai = self.Owner.PlayerActor.TraitsImplementing().FirstOrDefault(Exts.IsTraitEnabled); } void INotifyAddedToWorld.AddedToWorld(Actor self) diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index eb94e77c1d..6a4df6063f 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -15,7 +15,6 @@ using System.Drawing; using OpenRA.Activities; using OpenRA.Graphics; using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.AI; using OpenRA.Mods.Common.Graphics; using OpenRA.Primitives; using OpenRA.Traits; @@ -463,6 +462,24 @@ namespace OpenRA.Mods.Common.Traits void UpdatedDefenseCenter(CPos newLocation); } + [RequireExplicitImplementation] + public interface IBotNotifyIdleBaseUnits + { + void UpdatedIdleBaseUnits(List idleUnits); + } + + [RequireExplicitImplementation] + public interface IBotRequestUnitProduction + { + void RequestUnitProduction(IBot bot, string requestedActor); + } + + [RequireExplicitImplementation] + public interface IBotRequestPauseUnitProduction + { + bool PauseUnitProduction { get; } + } + [RequireExplicitImplementation] public interface IEditorActorOptions : ITraitInfoInterface {