diff --git a/OpenRA.Mods.Common/AI/AIUtils.cs b/OpenRA.Mods.Common/AI/AIUtils.cs index f48c287160..2db7a533e1 100644 --- a/OpenRA.Mods.Common/AI/AIUtils.cs +++ b/OpenRA.Mods.Common/AI/AIUtils.cs @@ -11,12 +11,15 @@ using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Common.AI { public enum BuildingType { Building, Defense, Refinery } + public enum WaterCheck { NotChecked, EnoughWater, NotEnoughWater } + public class CaptureTarget where TInfoType : class, ITraitInfoInterface { internal readonly Actor Actor; @@ -48,6 +51,40 @@ namespace OpenRA.Mods.Common.AI .Any(availableCells => availableCells > 0); } + public static IEnumerable FindQueues(Player player, string category) + { + return player.World.ActorsWithTrait() + .Where(a => a.Actor.Owner == player && a.Trait.Info.Type == category && a.Trait.Enabled) + .Select(a => a.Trait); + } + + public static IEnumerable GetActorsWithTrait(World world) + { + return world.ActorsHavingTrait(); + } + + public static int CountActorsWithTrait(string actorName, Player owner) + { + return GetActorsWithTrait(owner.World).Count(a => a.Owner == owner && a.Info.Name == actorName); + } + + public static int CountBuildingByCommonName(HashSet buildings, Player owner) + { + return GetActorsWithTrait(owner.World) + .Count(a => a.Owner == owner && buildings.Contains(a.Info.Name)); + } + + public static List FindEnemiesByCommonName(HashSet commonNames, Player player) + { + return player.World.Actors.Where(a => !a.IsDead && player.Stances[a.Owner] == Stance.Enemy && + commonNames.Contains(a.Info.Name)).ToList(); + } + + public static ActorInfo GetInfoByCommonName(HashSet names, Player owner) + { + return owner.World.Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(owner.World.LocalRandom).Value; + } + public static void BotDebug(string s, params object[] args) { if (Game.Settings.Debug.BotDebug) diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index 9f8044d930..aec770f691 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -28,6 +28,7 @@ namespace OpenRA.Mods.Common.AI public readonly HashSet ExcludeFromSquads = new HashSet(); } + // TODO: Move this to SquadManagerBotModule later public class BuildingCategories { public readonly HashSet ConstructionYard = new HashSet(); @@ -53,12 +54,6 @@ namespace OpenRA.Mods.Common.AI [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("Production queues AI uses for buildings.")] - public readonly HashSet BuildingQueues = new HashSet { "Building" }; - - [Desc("Production queues AI uses for defenses.")] - public readonly HashSet DefenseQueues = new HashSet { "Defense" }; - [Desc("Delay (in ticks) between giving out orders to units.")] public readonly int AssignRolesInterval = 20; @@ -74,51 +69,6 @@ namespace OpenRA.Mods.Common.AI [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("Minimum excess power the AI should try to maintain.")] - public readonly int MinimumExcessPower = 0; - - [Desc("Increase maintained excess power by this amount for every ExcessPowerIncreaseThreshold of base buildings.")] - public readonly int ExcessPowerIncrement = 0; - - [Desc("Increase maintained excess power by ExcessPowerIncrement for every N base buildings.")] - public readonly int ExcessPowerIncreaseThreshold = 1; - - [Desc("The targeted excess power the AI tries to maintain cannot rise above this.")] - public readonly int MaximumExcessPower = 0; - - [Desc("Additional delay (in ticks) between structure production checks when there is no active production.", - "StructureProductionRandomBonusDelay is added to this.")] - public readonly int StructureProductionInactiveDelay = 125; - - [Desc("Additional delay (in ticks) added between structure production checks when actively building things.", - "Note: The total delay is gamespeed OrderLatency x 4 + this + StructureProductionRandomBonusDelay.")] - public readonly int StructureProductionActiveDelay = 0; - - [Desc("A random delay (in ticks) of up to this is added to active/inactive production delays.")] - public readonly int StructureProductionRandomBonusDelay = 10; - - [Desc("Delay (in ticks) until retrying to build structure after the last 3 consecutive attempts failed.")] - public readonly int StructureProductionResumeDelay = 1500; - - [Desc("After how many failed attempts to place a structure should AI give up and wait", - "for StructureProductionResumeDelay before retrying.")] - public readonly int MaximumFailedPlacementAttempts = 3; - - [Desc("How many randomly chosen cells with resources to check when deciding refinery placement.")] - public readonly int MaxResourceCellsToCheck = 3; - - [Desc("Delay (in ticks) until rechecking for new BaseProviders.")] - public readonly int CheckForNewBasesDelay = 1500; - - [Desc("Minimum range at which to build defensive structures near a combat hotspot.")] - public readonly int MinimumDefenseRadius = 5; - - [Desc("Maximum range at which to build defensive structures near a combat hotspot.")] - public readonly int MaximumDefenseRadius = 20; - - [Desc("Try to build another production building if there is too much cash.")] - public readonly int NewProductionCashThreshold = 5000; - [Desc("Only produce units as long as there are less than this amount of units idling inside the base.")] public readonly int IdleBaseUnitsMaximum = 12; @@ -128,13 +78,11 @@ namespace OpenRA.Mods.Common.AI [Desc("Radius in cells around the base that should be scanned for units to be protected.")] public readonly int ProtectUnitScanRadius = 15; - [Desc("Radius in cells around a factory scanned for rally points by the AI.")] - public readonly int RallyPointScanRadius = 8; - - [Desc("Minimum distance in cells from center of the base when checking for building placement.")] + [Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")] public readonly int MinBaseRadius = 2; - [Desc("Radius in cells around the center of the base to expand.")] + [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.")] @@ -152,41 +100,25 @@ namespace OpenRA.Mods.Common.AI [Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] public readonly bool RestrictMCVDeploymentFallbackToBase = true; - [Desc("Radius in cells around each building with ProvideBuildableArea", - "to check for a 3x3 area of water where naval structures can be built.", - "Should match maximum adjacency of naval structures.")] - public readonly int CheckForWaterRadius = 8; - - [Desc("Terrain types which are considered water for base building purposes.")] - public readonly HashSet WaterTerrainTypes = new HashSet { "Water" }; - [Desc("Production queues AI uses for producing units.")] public readonly HashSet UnitQueues = new HashSet { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; - [Desc("Should the AI repair its buildings if damaged?")] - public readonly bool ShouldRepairBuildings = true; - [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("What buildings to the AI should build.", "What % of the total base must be this type of building.")] - public readonly Dictionary BuildingFractions = 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; - [Desc("What buildings should the AI have a maximum limit to build.")] - public readonly Dictionary BuildingLimits = null; - [Desc("Actor types that can capture other actors (via `Captures` or `ExternalCaptures`).", "Leave this empty to disable capturing.")] public HashSet CapturingActorTypes = new HashSet(); @@ -214,6 +146,7 @@ namespace OpenRA.Mods.Common.AI 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"); @@ -227,7 +160,7 @@ namespace OpenRA.Mods.Common.AI public object Create(ActorInitializer init) { return new HackyAI(this, init); } } - public sealed class HackyAI : ITick, IBot, INotifyDamage + public sealed class HackyAI : ITick, IBot, INotifyDamage, IBotPositionsUpdated { // DEPRECATED: Modules should use World.LocalRandom. public MersenneTwister Random { get; private set; } @@ -252,16 +185,12 @@ namespace OpenRA.Mods.Common.AI readonly Predicate unitCannotBeOrdered; IBotTick[] tickModules; + IBotRespondToAttack[] attackResponseModules; + IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; - PowerManager playerPower; - PlayerResources playerResource; int ticks; - BitArray resourceTypeIndices; - - List builders = new List(); - List unitsHangingAroundTheBase = new List(); // Units that the ai already knows about. Any unit not on this list needs to be given a role. @@ -303,14 +232,9 @@ namespace OpenRA.Mods.Common.AI { Player = p; IsEnabled = true; - playerPower = p.PlayerActor.TraitOrDefault(); - playerResource = p.PlayerActor.Trait(); tickModules = p.PlayerActor.TraitsImplementing().ToArray(); - - foreach (var building in Info.BuildingQueues) - builders.Add(new BaseBuilder(this, building, p, playerPower, playerResource)); - foreach (var defense in Info.DefenseQueues) - builders.Add(new BaseBuilder(this, defense, p, playerPower, playerResource)); + attackResponseModules = p.PlayerActor.TraitsImplementing().ToArray(); + positionsUpdatedModules = p.PlayerActor.TraitsImplementing().ToArray(); Random = new MersenneTwister(Game.CosmeticRandom.Next()); @@ -323,11 +247,6 @@ namespace OpenRA.Mods.Common.AI attackForceTicks = Random.Next(0, Info.AttackForceInterval); minAttackForceDelayTicks = Random.Next(0, Info.MinimumAttackForceDelay); minCaptureDelayTicks = Random.Next(0, Info.MinimumCaptureDelay); - - var tileset = World.Map.Rules.TileSet; - resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough - foreach (var t in Map.Rules.Actors["world"].TraitInfos()) - resourceTypeIndices.Set(tileset.GetTerrainIndex(t.TerrainType), true); } void IBot.QueueOrder(Order order) @@ -371,49 +290,25 @@ namespace OpenRA.Mods.Common.AI return null; } - IEnumerable GetActorsWithTrait() + bool HasAdequateConstructionYardCount { - return World.ActorsHavingTrait(); + 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; + } } - int CountActorsWithTrait(string actorName, Player owner) + bool HasAdequateRefineryCount { - return GetActorsWithTrait().Count(a => a.Owner == owner && a.Info.Name == actorName); - } - - int CountBuildingByCommonName(HashSet buildings, Player owner) - { - return GetActorsWithTrait() - .Count(a => a.Owner == owner && buildings.Contains(a.Info.Name)); - } - - public ActorInfo GetInfoByCommonName(HashSet names, Player owner) - { - return Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(Random).Value; - } - - public bool HasAdequateFact() - { - // Require at least one construction yard, unless we have no vehicles factory (can't build it). - return CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 || - CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0; - } - - public bool HasAdequateProc() - { - // Require at least one refinery, unless we can't build it. - return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 || - CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 || - CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) == 0; - } - - public bool HasMinimumProc() - { - // Require at least two refineries, unless we have no power (can't build it) - // or barracks (higher priority?) - return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) >= 2 || - CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 || - CountBuildingByCommonName(Info.BuildingCommonNames.Barracks, Player) == 0; + 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) @@ -428,39 +323,38 @@ namespace OpenRA.Mods.Common.AI if (rearmableInfo == null) return true; - var countOwnAir = CountActorsWithTrait(actorInfo.Name, Player); - var countBuildings = rearmableInfo.RearmActors.Sum(b => CountActorsWithTrait(b, Player)); + 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 defenseCenter; - public CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) + CPos? ChooseMcvDeployLocation(string actorType, bool distanceToBaseIsImportant) { - var ai = Map.Rules.Actors[actorType]; - var bi = ai.TraitInfoOrDefault(); + 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 = Map.FindTilesInAnnulus(center, 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(Random); + cells = cells.Shuffle(World.LocalRandom); foreach (var cell in cells) { - if (!World.CanPlaceBuilding(cell, ai, bi, null)) + if (!World.CanPlaceBuilding(cell, actorInfo, bi, null)) continue; - if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(World, Player, ai, cell)) + if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(World, Player, actorInfo, cell)) continue; return cell; @@ -471,40 +365,8 @@ namespace OpenRA.Mods.Common.AI var baseCenter = GetRandomBaseCenter(); - switch (type) - { - case BuildingType.Defense: - - // Build near the closest enemy structure - var closestEnemy = World.ActorsHavingTrait().Where(a => !a.Disposed && Player.Stances[a.Owner] == Stance.Enemy) - .ClosestTo(World.Map.CenterOfCell(defenseCenter)); - - var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter; - return findPos(defenseCenter, targetCell, Info.MinimumDefenseRadius, Info.MaximumDefenseRadius); - - case BuildingType.Refinery: - - // Try and place the refinery near a resource field - var nearbyResources = Map.FindTilesInAnnulus(baseCenter, Info.MinBaseRadius, Info.MaxBaseRadius) - .Where(a => resourceTypeIndices.Get(Map.GetTerrainIndex(a))) - .Shuffle(Random).Take(Info.MaxResourceCellsToCheck); - - foreach (var r in nearbyResources) - { - var found = findPos(baseCenter, r, Info.MinBaseRadius, Info.MaxBaseRadius); - if (found != null) - return found; - } - - // Try and find a free spot somewhere else in the base - return findPos(baseCenter, baseCenter, Info.MinBaseRadius, Info.MaxBaseRadius); - - case BuildingType.Building: - return findPos(baseCenter, baseCenter, Info.MinBaseRadius, distanceToBaseIsImportant ? Info.MaxBaseRadius : Map.Grid.MaximumTileSearchRange); - } - - // Can't find a build location - return null; + return findPos(baseCenter, baseCenter, Info.MinBaseRadius, + distanceToBaseIsImportant ? Info.MaxBaseRadius : World.Map.Grid.MaximumTileSearchRange); } void ITick.Tick(Actor self) @@ -521,15 +383,11 @@ namespace OpenRA.Mods.Common.AI ProductionUnits(self); AssignRolesToIdleUnits(self); - SetRallyPointsForNewProductionBuildings(self); - foreach (var b in builders) - b.Tick(); - - // TODO: Add an option to include this in CheckSyncAroundUnsyncedCode. + // 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("tick_bots")) + using (new PerfSample("bot_tick")) foreach (var t in tickModules) if (t.IsTraitEnabled()) t.BotTick(this); @@ -549,12 +407,6 @@ namespace OpenRA.Mods.Common.AI return World.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestTo(pos); } - List FindEnemyConstructionYards() - { - return World.Actors.Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead && - Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)).ToList(); - } - void CleanSquads() { Squads.RemoveAll(s => !s.IsValid); @@ -752,7 +604,7 @@ namespace OpenRA.Mods.Common.AI void TryToRushAttack() { - var allEnemyBaseBuilder = FindEnemyConstructionYards(); + 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(); @@ -800,41 +652,6 @@ namespace OpenRA.Mods.Common.AI } } - bool IsRallyPointValid(CPos x, BuildingInfo info) - { - return info != null && World.IsCellBuildable(x, null, info); - } - - void SetRallyPointsForNewProductionBuildings(Actor self) - { - foreach (var rp in self.World.ActorsWithTrait()) - { - if (rp.Actor.Owner == Player && - !IsRallyPointValid(rp.Trait.Location, rp.Actor.Info.TraitInfoOrDefault())) - { - QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(World, ChooseRallyLocationNear(rp.Actor)), false) - { - SuppressVisualFeedback = true - }); - } - } - } - - // Won't work for shipyards... - CPos ChooseRallyLocationNear(Actor producer) - { - var possibleRallyPoints = Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius) - .Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault())); - - if (!possibleRallyPoints.Any()) - { - AIUtils.BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location); - return producer.Location; - } - - return possibleRallyPoints.Random(Random); - } - void InitializeBase(Actor self, bool chooseLocation) { var mcv = FindAndDeployMcv(self, chooseLocation); @@ -842,10 +659,20 @@ namespace OpenRA.Mods.Common.AI if (mcv == null) return; - initialBaseCenter = mcv.Location; - defenseCenter = mcv.Location; + 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) { @@ -862,11 +689,11 @@ namespace OpenRA.Mods.Common.AI // 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 && - CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0; + AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0; if (move) { - var desiredLocation = ChooseBuildLocation(transformsInfo.IntoActor, restrictToBase, BuildingType.Building); + var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, restrictToBase); if (desiredLocation == null) return null; @@ -878,23 +705,16 @@ namespace OpenRA.Mods.Common.AI return mcv; } - internal IEnumerable FindQueues(string category) - { - return World.ActorsWithTrait() - .Where(a => a.Actor.Owner == Player && a.Trait.Info.Type == category && a.Trait.Enabled) - .Select(a => a.Trait); - } - void ProductionUnits(Actor self) { // Stop building until economy is restored - if (!HasAdequateProc()) + if (!HasAdequateRefineryCount) return; // No construction yards - Build a new MCV - if (Info.UnitsCommonNames.Mcv.Any() && !HasAdequateFact() && !self.World.Actors.Any(a => a.Owner == Player && - Info.UnitsCommonNames.Mcv.Contains(a.Info.Name))) - BuildUnit("Vehicle", GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name); + 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); @@ -903,7 +723,7 @@ namespace OpenRA.Mods.Common.AI void BuildUnit(string category, bool buildRandom) { // Pick a free queue - var queue = FindQueues(category).FirstOrDefault(q => !q.AllQueued().Any()); + var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any()); if (queue == null) return; @@ -929,7 +749,7 @@ namespace OpenRA.Mods.Common.AI void BuildUnit(string category, string name) { - var queue = FindQueues(category).FirstOrDefault(q => !q.AllQueued().Any()); + var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any()); if (queue == null) return; @@ -939,37 +759,30 @@ namespace OpenRA.Mods.Common.AI void INotifyDamage.Damaged(Actor self, AttackInfo e) { - if (!IsEnabled || e.Attacker == null) + if (!IsEnabled) return; - if (e.Attacker.Owner.Stances[self.Owner] == Stance.Neutral) + // 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")) + foreach (var t in attackResponseModules) + if (t.IsTraitEnabled()) + t.RespondToAttack(this, self, e); + + if (e.Attacker == null || e.Attacker.Disposed) return; - var rb = self.TraitOrDefault(); - - if (Info.ShouldRepairBuildings && rb != null) - { - if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light && !rb.RepairActive) - { - AIUtils.BotDebug("Bot noticed damage {0} {1}->{2}, repairing.", - self, e.PreviousDamageState, e.DamageState); - QueueOrder(new Order("RepairBuilding", self.Owner.PlayerActor, Target.FromActor(self), false)); - } - } - - if (e.Attacker.Disposed) + if (e.Attacker.Owner.Stances[self.Owner] != Stance.Enemy) return; if (!e.Attacker.Info.HasTraitInfo()) return; // Protected priority assets, MCVs, harvesters and buildings - if ((self.Info.HasTraitInfo() || self.Info.HasTraitInfo() || self.Info.HasTraitInfo()) && - Player.Stances[e.Attacker.Owner] == Stance.Enemy) - { - defenseCenter = e.Attacker.Location; + // 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/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 83247023a3..9c6689b195 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -122,10 +122,12 @@ - + + + diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs new file mode 100644 index 0000000000..b87f0f7719 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -0,0 +1,244 @@ +#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.AI; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Manages AI base construction.")] + public class BaseBuilderBotModuleInfo : ConditionalTraitInfo + { + [Desc("Tells the AI what building types are considered construction yards.")] + public readonly HashSet ConstructionYardTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered vehicle production facilities.")] + public readonly HashSet VehiclesFactoryTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered refineries.")] + public readonly HashSet RefineryTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered power plants.")] + public readonly HashSet PowerTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered infantry production facilities.")] + public readonly HashSet BarracksTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered production facilities.")] + public readonly HashSet ProductionTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered naval production facilities.")] + public readonly HashSet NavalProductionTypes = new HashSet(); + + [Desc("Tells the AI what building types are considered silos (resource storage).")] + public readonly HashSet SiloTypes = new HashSet(); + + [Desc("Production queues AI uses for buildings.")] + public readonly HashSet BuildingQueues = new HashSet { "Building" }; + + [Desc("Production queues AI uses for defenses.")] + public readonly HashSet DefenseQueues = new HashSet { "Defense" }; + + [Desc("Minimum distance in cells from center of the base when checking for building placement.")] + public readonly int MinBaseRadius = 2; + + [Desc("Radius in cells around the center of the base to expand.")] + public readonly int MaxBaseRadius = 20; + + [Desc("Minimum excess power the AI should try to maintain.")] + public readonly int MinimumExcessPower = 0; + + [Desc("The targeted excess power the AI tries to maintain cannot rise above this.")] + public readonly int MaximumExcessPower = 0; + + [Desc("Increase maintained excess power by this amount for every ExcessPowerIncreaseThreshold of base buildings.")] + public readonly int ExcessPowerIncrement = 0; + + [Desc("Increase maintained excess power by ExcessPowerIncrement for every N base buildings.")] + public readonly int ExcessPowerIncreaseThreshold = 1; + + [Desc("Additional delay (in ticks) between structure production checks when there is no active production.", + "StructureProductionRandomBonusDelay is added to this.")] + public readonly int StructureProductionInactiveDelay = 125; + + [Desc("Additional delay (in ticks) added between structure production checks when actively building things.", + "Note: The total delay is gamespeed OrderLatency x 4 + this + StructureProductionRandomBonusDelay.")] + public readonly int StructureProductionActiveDelay = 0; + + [Desc("A random delay (in ticks) of up to this is added to active/inactive production delays.")] + public readonly int StructureProductionRandomBonusDelay = 10; + + [Desc("Delay (in ticks) until retrying to build structure after the last 3 consecutive attempts failed.")] + public readonly int StructureProductionResumeDelay = 1500; + + [Desc("After how many failed attempts to place a structure should AI give up and wait", + "for StructureProductionResumeDelay before retrying.")] + public readonly int MaximumFailedPlacementAttempts = 3; + + [Desc("How many randomly chosen cells with resources to check when deciding refinery placement.")] + public readonly int MaxResourceCellsToCheck = 3; + + [Desc("Delay (in ticks) until rechecking for new BaseProviders.")] + public readonly int CheckForNewBasesDelay = 1500; + + [Desc("Minimum range at which to build defensive structures near a combat hotspot.")] + public readonly int MinimumDefenseRadius = 5; + + [Desc("Maximum range at which to build defensive structures near a combat hotspot.")] + public readonly int MaximumDefenseRadius = 20; + + [Desc("Try to build another production building if there is too much cash.")] + public readonly int NewProductionCashThreshold = 5000; + + [Desc("Radius in cells around a factory scanned for rally points by the AI.")] + public readonly int RallyPointScanRadius = 8; + + [Desc("Radius in cells around each building with ProvideBuildableArea", + "to check for a 3x3 area of water where naval structures can be built.", + "Should match maximum adjacency of naval structures.")] + public readonly int CheckForWaterRadius = 8; + + [Desc("Terrain types which are considered water for base building purposes.")] + public readonly HashSet WaterTerrainTypes = new HashSet { "Water" }; + + [Desc("What buildings to the AI should build.", "What integer percentage of the total base must be this type of building.")] + public readonly Dictionary BuildingFractions = null; + + [Desc("What buildings should the AI have a maximum limit to build.")] + public readonly Dictionary BuildingLimits = null; + + public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); } + } + + public class BaseBuilderBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IBotRespondToAttack + { + 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 CPos DefenseCenter { get { return defenseCenter; } } + + readonly World world; + readonly Player player; + PowerManager playerPower; + PlayerResources playerResources; + IBotPositionsUpdated[] positionsUpdatedModules; + BitArray resourceTypeIndices; + CPos initialBaseCenter; + CPos defenseCenter; + + List builders = new List(); + + public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + } + + protected override void TraitEnabled(Actor self) + { + playerPower = player.PlayerActor.TraitOrDefault(); + playerResources = player.PlayerActor.Trait(); + positionsUpdatedModules = player.PlayerActor.TraitsImplementing().ToArray(); + + var tileset = world.Map.Rules.TileSet; + resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough + foreach (var t in world.Map.Rules.Actors["world"].TraitInfos()) + resourceTypeIndices.Set(tileset.GetTerrainIndex(t.TerrainType), true); + + foreach (var building in Info.BuildingQueues) + builders.Add(new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceTypeIndices)); + foreach (var defense in Info.DefenseQueues) + builders.Add(new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceTypeIndices)); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) + { + defenseCenter = newLocation; + } + + void IBotTick.BotTick(IBot bot) + { + SetRallyPointsForNewProductionBuildings(bot); + + foreach (var b in builders) + b.Tick(bot); + } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo 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 + if (self.Info.HasTraitInfo() || self.Info.HasTraitInfo()) + foreach (var n in positionsUpdatedModules) + n.UpdatedDefenseCenter(e.Attacker.Location); + } + + void SetRallyPointsForNewProductionBuildings(IBot bot) + { + foreach (var rp in world.ActorsWithTrait()) + { + if (rp.Actor.Owner == player && + !IsRallyPointValid(rp.Trait.Location, rp.Actor.Info.TraitInfoOrDefault())) + { + bot.QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(world, ChooseRallyLocationNear(rp.Actor)), false) + { + SuppressVisualFeedback = true + }); + } + } + } + + // Won't work for shipyards... + CPos ChooseRallyLocationNear(Actor producer) + { + var possibleRallyPoints = world.Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius) + .Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault())); + + if (!possibleRallyPoints.Any()) + { + AIUtils.BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location); + return producer.Location; + } + + return possibleRallyPoints.Random(world.LocalRandom); + } + + bool IsRallyPointValid(CPos x, BuildingInfo info) + { + return info != null && world.IsCellBuildable(x, null, info); + } + } +} diff --git a/OpenRA.Mods.Common/AI/BaseBuilder.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs similarity index 54% rename from OpenRA.Mods.Common/AI/BaseBuilder.cs rename to OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 96a009849c..0ce0524d10 100644 --- a/OpenRA.Mods.Common/AI/BaseBuilder.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -10,18 +10,19 @@ #endregion using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.AI; using OpenRA.Traits; -namespace OpenRA.Mods.Common.AI +namespace OpenRA.Mods.Common.Traits { - class BaseBuilder + class BaseBuilderQueueManager { readonly string category; - readonly HackyAI ai; + readonly BaseBuilderBotModule baseBuilder; readonly World world; readonly Player player; readonly PowerManager playerPower; @@ -35,32 +36,28 @@ namespace OpenRA.Mods.Common.AI int cachedBases; int cachedBuildings; int minimumExcessPower; + BitArray resourceTypeIndices; - enum Water + WaterCheck waterState = WaterCheck.NotChecked; + + public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, + PlayerResources pr, BitArray resourceTypeIndices) { - NotChecked, - EnoughWater, - NotEnoughWater - } - - Water waterState = Water.NotChecked; - - public BaseBuilder(HackyAI ai, string category, Player p, PowerManager pm, PlayerResources pr) - { - this.ai = ai; + this.baseBuilder = baseBuilder; world = p.World; player = p; playerPower = pm; playerResources = pr; this.category = category; - failRetryTicks = ai.Info.StructureProductionResumeDelay; - minimumExcessPower = ai.Info.MinimumExcessPower; + failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; + minimumExcessPower = baseBuilder.Info.MinimumExcessPower; + this.resourceTypeIndices = resourceTypeIndices; } - public void Tick() + public void Tick(IBot bot) { // If failed to place something N consecutive times, wait M ticks until resuming building production - if (failCount >= ai.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0) + if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0) { var currentBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); var baseProviders = world.ActorsHavingTrait().Count(a => a.Owner == player); @@ -71,28 +68,28 @@ namespace OpenRA.Mods.Common.AI if (currentBuildings < cachedBuildings || baseProviders > cachedBases) failCount = 0; else - failRetryTicks = ai.Info.StructureProductionResumeDelay; + failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; } - if (waterState == Water.NotChecked) + if (waterState == WaterCheck.NotChecked) { - if (AIUtils.IsAreaAvailable(ai.World, ai.Player, ai.Map, ai.Info.MaxBaseRadius, ai.Info.WaterTerrainTypes)) - waterState = Water.EnoughWater; + if (AIUtils.IsAreaAvailable(world, player, world.Map, baseBuilder.Info.MaxBaseRadius, baseBuilder.Info.WaterTerrainTypes)) + waterState = WaterCheck.EnoughWater; else { - waterState = Water.NotEnoughWater; - checkForBasesTicks = ai.Info.CheckForNewBasesDelay; + waterState = WaterCheck.NotEnoughWater; + checkForBasesTicks = baseBuilder.Info.CheckForNewBasesDelay; } } - if (waterState == Water.NotEnoughWater && --checkForBasesTicks <= 0) + if (waterState == WaterCheck.NotEnoughWater && --checkForBasesTicks <= 0) { var currentBases = world.ActorsHavingTrait().Count(a => a.Owner == player); if (currentBases > cachedBases) { cachedBases = currentBases; - waterState = Water.NotChecked; + waterState = WaterCheck.NotChecked; } } @@ -101,35 +98,35 @@ namespace OpenRA.Mods.Common.AI return; playerBuildings = world.ActorsHavingTrait().Where(a => a.Owner == player).ToArray(); - var excessPowerBonus = ai.Info.ExcessPowerIncrement * (playerBuildings.Count() / ai.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); - minimumExcessPower = (ai.Info.MinimumExcessPower + excessPowerBonus).Clamp(ai.Info.MinimumExcessPower, ai.Info.MaximumExcessPower); + var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Count() / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); + minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower); var active = false; - foreach (var queue in ai.FindQueues(category)) - if (TickQueue(queue)) + foreach (var queue in AIUtils.FindQueues(player, category)) + if (TickQueue(bot, queue)) active = true; // Add a random factor so not every AI produces at the same tick early in the game. // Minimum should not be negative as delays in HackyAI could be zero. - var randomFactor = ai.Random.Next(0, ai.Info.StructureProductionRandomBonusDelay); + var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay); // Needs to be at least 4 * OrderLatency because otherwise the AI frequently duplicates build orders (i.e. makes the same build decision twice) - waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + ai.Info.StructureProductionActiveDelay + randomFactor - : ai.Info.StructureProductionInactiveDelay + randomFactor; + waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + baseBuilder.Info.StructureProductionActiveDelay + randomFactor + : baseBuilder.Info.StructureProductionInactiveDelay + randomFactor; } - bool TickQueue(ProductionQueue queue) + bool TickQueue(IBot bot, ProductionQueue queue) { var currentBuilding = queue.AllQueued().FirstOrDefault(); // Waiting to build something - if (currentBuilding == null && failCount < ai.Info.MaximumFailedPlacementAttempts) + if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts) { var item = ChooseBuildingToBuild(queue); if (item == null) return false; - ai.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1)); + bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1)); } else if (currentBuilding != null && currentBuilding.Done) { @@ -140,18 +137,18 @@ namespace OpenRA.Mods.Common.AI var type = BuildingType.Building; if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo()) type = BuildingType.Defense; - else if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo()) + else if (baseBuilder.Info.RefineryTypes.Contains(world.Map.Rules.Actors[currentBuilding.Item].Name)) type = BuildingType.Refinery; - var location = ai.ChooseBuildLocation(currentBuilding.Item, true, type); + var location = ChooseBuildLocation(currentBuilding.Item, true, type); if (location == null) { AIUtils.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item)); - ai.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1)); + bot.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1)); failCount += failCount; // If we just reached the maximum fail count, cache the number of current structures - if (failCount == ai.Info.MaximumFailedPlacementAttempts) + if (failCount == baseBuilder.Info.MaximumFailedPlacementAttempts) { cachedBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); cachedBases = world.ActorsHavingTrait().Count(a => a.Owner == player); @@ -160,7 +157,7 @@ namespace OpenRA.Mods.Common.AI else { failCount = 0; - ai.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false) + bot.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false) { // Building to place TargetString = currentBuilding.Item, @@ -185,22 +182,22 @@ namespace OpenRA.Mods.Common.AI if (!actors.Contains(actor.Name)) return false; - if (!ai.Info.BuildingLimits.ContainsKey(actor.Name)) + if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name)) return true; - return playerBuildings.Count(a => a.Info.Name == actor.Name) < ai.Info.BuildingLimits[actor.Name]; + return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name]; }); if (orderBy != null) return available.MaxByOrDefault(orderBy); - return available.RandomOrDefault(ai.Random); + return available.RandomOrDefault(world.LocalRandom); } bool HasSufficientPowerForActor(ActorInfo actorInfo) { return playerPower == null || (actorInfo.TraitInfos().Where(i => i.EnabledByDefault) - .Sum(p => p.Amount) + playerPower.ExcessPower) >= minimumExcessPower; + .Sum(p => p.Amount) + playerPower.ExcessPower) >= baseBuilder.Info.MinimumExcessPower; } ActorInfo ChooseBuildingToBuild(ProductionQueue queue) @@ -208,7 +205,7 @@ namespace OpenRA.Mods.Common.AI var buildableThings = queue.BuildableItems(); // This gets used quite a bit, so let's cache it here - var power = GetProducibleBuilding(ai.Info.BuildingCommonNames.Power, buildableThings, + var power = GetProducibleBuilding(baseBuilder.Info.PowerTypes, buildableThings, a => a.TraitInfos().Where(i => i.EnabledByDefault).Sum(p => p.Amount)); // First priority is to get out of a low power situation @@ -222,9 +219,9 @@ namespace OpenRA.Mods.Common.AI } // Next is to build up a strong economy - if (!ai.HasAdequateProc() || !ai.HasMinimumProc()) + if (!HasAdequateRefineryCount) { - var refinery = GetProducibleBuilding(ai.Info.BuildingCommonNames.Refinery, buildableThings); + var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); @@ -239,9 +236,9 @@ namespace OpenRA.Mods.Common.AI } // Make sure that we can spend as fast as we are earning - if (ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold) + if (baseBuilder.Info.NewProductionCashThreshold > 0 && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold) { - var production = GetProducibleBuilding(ai.Info.BuildingCommonNames.Production, buildableThings); + var production = GetProducibleBuilding(baseBuilder.Info.ProductionTypes, buildableThings); if (production != null && HasSufficientPowerForActor(production)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); @@ -256,11 +253,11 @@ namespace OpenRA.Mods.Common.AI } // Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings - if (waterState == Water.EnoughWater && ai.Info.NewProductionCashThreshold > 0 - && playerResources.Resources > ai.Info.NewProductionCashThreshold - && AIUtils.IsAreaAvailable(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes)) + if (waterState == WaterCheck.EnoughWater && baseBuilder.Info.NewProductionCashThreshold > 0 + && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold + && AIUtils.IsAreaAvailable(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes)) { - var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings); + var navalproduction = GetProducibleBuilding(baseBuilder.Info.NavalProductionTypes, buildableThings); if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); @@ -277,7 +274,7 @@ namespace OpenRA.Mods.Common.AI // Create some head room for resource storage if we really need it if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity) { - var silo = GetProducibleBuilding(ai.Info.BuildingCommonNames.Silo, buildableThings); + var silo = GetProducibleBuilding(baseBuilder.Info.SiloTypes, buildableThings); if (silo != null && HasSufficientPowerForActor(silo)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); @@ -292,7 +289,7 @@ namespace OpenRA.Mods.Common.AI } // Build everything else - foreach (var frac in ai.Info.BuildingFractions.Shuffle(ai.Random)) + foreach (var frac in baseBuilder.Info.BuildingFractions.Shuffle(world.LocalRandom)) { var name = frac.Key; @@ -302,18 +299,18 @@ namespace OpenRA.Mods.Common.AI // Do we want to build this structure? var count = playerBuildings.Count(a => a.Info.Name == name); - if (count > frac.Value * playerBuildings.Length) + if (count * 100 > frac.Value * playerBuildings.Length) continue; - if (ai.Info.BuildingLimits.ContainsKey(name) && ai.Info.BuildingLimits[name] <= count) + if (baseBuilder.Info.BuildingLimits.ContainsKey(name) && baseBuilder.Info.BuildingLimits[name] <= count) continue; // If we're considering to build a naval structure, check whether there is enough water inside the base perimeter // and any structure providing buildable area close enough to that water. // TODO: Extend this check to cover any naval structure, not just production. - if (ai.Info.BuildingCommonNames.NavalProduction.Contains(name) - && (waterState == Water.NotEnoughWater - || !AIUtils.IsAreaAvailable(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes))) + if (baseBuilder.Info.NavalProductionTypes.Contains(name) + && (waterState == WaterCheck.NotEnoughWater + || !AIUtils.IsAreaAvailable(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes))) continue; // Will this put us into low power? @@ -342,5 +339,97 @@ namespace OpenRA.Mods.Common.AI // AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); return null; } + + CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) + { + 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 = baseBuilder.GetRandomBaseCenter(); + + switch (type) + { + case BuildingType.Defense: + + // Build near the closest enemy structure + var closestEnemy = world.ActorsHavingTrait().Where(a => !a.Disposed && player.Stances[a.Owner] == Stance.Enemy) + .ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); + + var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter; + return findPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius); + + case BuildingType.Refinery: + + // Try and place the refinery near a resource field + var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) + .Where(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a))) + .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); + + foreach (var r in nearbyResources) + { + var found = findPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + if (found != null) + return found; + } + + // Try and find a free spot somewhere else in the base + return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + + case BuildingType.Building: + return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, + distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); + } + + // 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/BuildingRepairBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs new file mode 100644 index 0000000000..69863e9d2d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/BuildingRepairBotModule.cs @@ -0,0 +1,42 @@ +#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 OpenRA.Mods.Common.AI; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Manages AI repairing base buildings.")] + public class BuildingRepairBotModuleInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new BuildingRepairBotModule(init.Self, this); } + } + + public class BuildingRepairBotModule : ConditionalTrait, IBotRespondToAttack + { + public BuildingRepairBotModule(Actor self, BuildingRepairBotModuleInfo info) + : base(info) { } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) + { + var rb = self.TraitOrDefault(); + if (rb != null) + { + if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light && !rb.RepairActive) + { + AIUtils.BotDebug("Bot noticed damage {0} {1}->{2}, repairing.", + self, e.PreviousDamageState, e.DamageState); + bot.QueueOrder(new Order("RepairBuilding", self.Owner.PlayerActor, Target.FromActor(self), false)); + } + } + } + } +} diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 4e31c2b234..1b6cb57de1 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -452,6 +452,15 @@ namespace OpenRA.Mods.Common.Traits [RequireExplicitImplementation] public interface IBotTick { void BotTick(IBot bot); } + public interface IBotRespondToAttack { void RespondToAttack(IBot bot, Actor self, AttackInfo e); } + + [RequireExplicitImplementation] + public interface IBotPositionsUpdated + { + void UpdatedBaseCenter(CPos newLocation); + void UpdatedDefenseCenter(CPos newLocation); + } + [RequireExplicitImplementation] public interface IEditorActorOptions : ITraitInfoInterface {