1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections; 14 using System.Collections.Generic; 15 using System.Linq; 16 using OpenRA.Support; 17 using OpenRA.Traits; 18 19 namespace OpenRA.Mods.Common.Traits 20 { 21 [Desc("Manages AI MCVs.")] 22 public class McvManagerBotModuleInfo : ConditionalTraitInfo 23 { 24 [Desc("Actor types that are considered MCVs (deploy into base builders).")] 25 public readonly HashSet<string> McvTypes = new HashSet<string>(); 26 27 [Desc("Actor types that are considered construction yards (base builders).")] 28 public readonly HashSet<string> ConstructionYardTypes = new HashSet<string>(); 29 30 [Desc("Actor types that are able to produce MCVs.")] 31 public readonly HashSet<string> McvFactoryTypes = new HashSet<string>(); 32 33 [Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")] 34 public readonly int MinimumConstructionYardCount = 1; 35 36 [Desc("Delay (in ticks) between looking for and giving out orders to new MCVs.")] 37 public readonly int ScanForNewMcvInterval = 20; 38 39 [Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")] 40 public readonly int MinBaseRadius = 2; 41 42 [Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.", 43 "Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")] 44 public readonly int MaxBaseRadius = 20; 45 46 [Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] 47 public readonly bool RestrictMCVDeploymentFallbackToBase = true; 48 Create(ActorInitializer init)49 public override object Create(ActorInitializer init) { return new McvManagerBotModule(init.Self, this); } 50 } 51 52 public class McvManagerBotModule : ConditionalTrait<McvManagerBotModuleInfo>, IBotTick, IBotPositionsUpdated, IGameSaveTraitData 53 { GetRandomBaseCenter()54 public CPos GetRandomBaseCenter() 55 { 56 var randomConstructionYard = world.Actors.Where(a => a.Owner == player && 57 Info.ConstructionYardTypes.Contains(a.Info.Name)) 58 .RandomOrDefault(world.LocalRandom); 59 60 return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; 61 } 62 63 readonly World world; 64 readonly Player player; 65 66 readonly Predicate<Actor> unitCannotBeOrdered; 67 68 IBotPositionsUpdated[] notifyPositionsUpdated; 69 IBotRequestUnitProduction[] requestUnitProduction; 70 71 CPos initialBaseCenter; 72 int scanInterval; 73 bool firstTick = true; 74 McvManagerBotModule(Actor self, McvManagerBotModuleInfo info)75 public McvManagerBotModule(Actor self, McvManagerBotModuleInfo info) 76 : base(info) 77 { 78 world = self.World; 79 player = self.Owner; 80 unitCannotBeOrdered = a => a.Owner != player || a.IsDead || !a.IsInWorld; 81 } 82 Created(Actor self)83 protected override void Created(Actor self) 84 { 85 // Special case handling is required for the Player actor. 86 // Created is called before Player.PlayerActor is assigned, 87 // so we must query player traits from self, which refers 88 // for bot modules always to the Player actor. 89 notifyPositionsUpdated = self.TraitsImplementing<IBotPositionsUpdated>().ToArray(); 90 requestUnitProduction = self.TraitsImplementing<IBotRequestUnitProduction>().ToArray(); 91 } 92 TraitEnabled(Actor self)93 protected override void TraitEnabled(Actor self) 94 { 95 // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. 96 scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval * 2); 97 } 98 IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)99 void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) 100 { 101 initialBaseCenter = newLocation; 102 } 103 IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation)104 void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } 105 IBotTick.BotTick(IBot bot)106 void IBotTick.BotTick(IBot bot) 107 { 108 if (firstTick) 109 { 110 DeployMcvs(bot, false); 111 firstTick = false; 112 } 113 114 if (--scanInterval <= 0) 115 { 116 scanInterval = Info.ScanForNewMcvInterval; 117 DeployMcvs(bot, true); 118 119 // No construction yards - Build a new MCV 120 if (ShouldBuildMCV()) 121 { 122 var unitBuilder = requestUnitProduction.FirstOrDefault(Exts.IsTraitEnabled); 123 if (unitBuilder != null) 124 { 125 var mcvInfo = AIUtils.GetInfoByCommonName(Info.McvTypes, player); 126 if (unitBuilder.RequestedProductionCount(bot, mcvInfo.Name) == 0) 127 unitBuilder.RequestUnitProduction(bot, mcvInfo.Name); 128 } 129 } 130 } 131 } 132 ShouldBuildMCV()133 bool ShouldBuildMCV() 134 { 135 // Only build MCV if we don't already have one in the field. 136 var allowedToBuildMCV = AIUtils.CountActorByCommonName(Info.McvTypes, player) == 0; 137 if (!allowedToBuildMCV) 138 return false; 139 140 // Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it). 141 return AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) < Info.MinimumConstructionYardCount && 142 AIUtils.CountBuildingByCommonName(Info.McvFactoryTypes, player) > 0; 143 } 144 DeployMcvs(IBot bot, bool chooseLocation)145 void DeployMcvs(IBot bot, bool chooseLocation) 146 { 147 var newMCVs = world.ActorsHavingTrait<Transforms>() 148 .Where(a => a.Owner == player && a.IsIdle && Info.McvTypes.Contains(a.Info.Name)); 149 150 foreach (var mcv in newMCVs) 151 DeployMcv(bot, mcv, chooseLocation); 152 } 153 154 // Find any MCV and deploy them at a sensible location. DeployMcv(IBot bot, Actor mcv, bool move)155 void DeployMcv(IBot bot, Actor mcv, bool move) 156 { 157 if (move) 158 { 159 // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! 160 var restrictToBase = Info.RestrictMCVDeploymentFallbackToBase && AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) > 0; 161 162 var transformsInfo = mcv.Info.TraitInfo<TransformsInfo>(); 163 var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase); 164 if (desiredLocation == null) 165 return; 166 167 bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true)); 168 } 169 170 // If the MCV has to move first, we can't be sure it reaches the destination alive, so we only 171 // update base and defense center if the MCV is deployed immediately (i.e. at game start). 172 // TODO: This could be adressed via INotifyTransform. 173 foreach (var n in notifyPositionsUpdated) 174 { 175 n.UpdatedBaseCenter(mcv.Location); 176 n.UpdatedDefenseCenter(mcv.Location); 177 } 178 179 bot.QueueOrder(new Order("DeployTransform", mcv, true)); 180 } 181 ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant)182 CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant) 183 { 184 var actorInfo = world.Map.Rules.Actors[actorType]; 185 var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>(); 186 if (bi == null) 187 return null; 188 189 // Find the buildable cell that is closest to pos and centered around center 190 Func<CPos, CPos, int, int, CPos?> findPos = (center, target, minRange, maxRange) => 191 { 192 var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); 193 194 // Sort by distance to target if we have one 195 if (center != target) 196 cells = cells.OrderBy(c => (c - target).LengthSquared); 197 else 198 cells = cells.Shuffle(world.LocalRandom); 199 200 foreach (var cell in cells) 201 if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) 202 return cell; 203 204 return null; 205 }; 206 207 var baseCenter = GetRandomBaseCenter(); 208 209 return findPos(baseCenter, baseCenter, Info.MinBaseRadius, 210 distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); 211 } 212 IGameSaveTraitData.IssueTraitData(Actor self)213 List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self) 214 { 215 if (IsTraitDisabled) 216 return null; 217 218 return new List<MiniYamlNode>() 219 { 220 new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) 221 }; 222 } 223 IGameSaveTraitData.ResolveTraitData(Actor self, List<MiniYamlNode> data)224 void IGameSaveTraitData.ResolveTraitData(Actor self, List<MiniYamlNode> data) 225 { 226 if (self.World.IsReplay) 227 return; 228 229 var initialBaseCenterNode = data.FirstOrDefault(n => n.Key == "InitialBaseCenter"); 230 if (initialBaseCenterNode != null) 231 initialBaseCenter = FieldLoader.GetValue<CPos>("InitialBaseCenter", initialBaseCenterNode.Value.Value); 232 } 233 } 234 } 235