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.Traits; 17 18 namespace OpenRA.Mods.Common.Traits 19 { 20 class BaseBuilderQueueManager 21 { 22 readonly string category; 23 24 readonly BaseBuilderBotModule baseBuilder; 25 readonly World world; 26 readonly Player player; 27 readonly PowerManager playerPower; 28 readonly PlayerResources playerResources; 29 30 int waitTicks; 31 Actor[] playerBuildings; 32 int failCount; 33 int failRetryTicks; 34 int checkForBasesTicks; 35 int cachedBases; 36 int cachedBuildings; 37 int minimumExcessPower; 38 BitArray resourceTypeIndices; 39 40 WaterCheck waterState = WaterCheck.NotChecked; 41 BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, PlayerResources pr, BitArray resourceTypeIndices)42 public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, 43 PlayerResources pr, BitArray resourceTypeIndices) 44 { 45 this.baseBuilder = baseBuilder; 46 world = p.World; 47 player = p; 48 playerPower = pm; 49 playerResources = pr; 50 this.category = category; 51 failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; 52 minimumExcessPower = baseBuilder.Info.MinimumExcessPower; 53 this.resourceTypeIndices = resourceTypeIndices; 54 } 55 Tick(IBot bot)56 public void Tick(IBot bot) 57 { 58 // If failed to place something N consecutive times, wait M ticks until resuming building production 59 if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0) 60 { 61 var currentBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player); 62 var baseProviders = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player); 63 64 // Only bother resetting failCount if either a) the number of buildings has decreased since last failure M ticks ago, 65 // or b) number of BaseProviders (construction yard or similar) has increased since then. 66 // Otherwise reset failRetryTicks instead to wait again. 67 if (currentBuildings < cachedBuildings || baseProviders > cachedBases) 68 failCount = 0; 69 else 70 failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; 71 } 72 73 if (waterState == WaterCheck.NotChecked) 74 { 75 if (AIUtils.IsAreaAvailable<BaseProvider>(world, player, world.Map, baseBuilder.Info.MaxBaseRadius, baseBuilder.Info.WaterTerrainTypes)) 76 waterState = WaterCheck.EnoughWater; 77 else 78 { 79 waterState = WaterCheck.NotEnoughWater; 80 checkForBasesTicks = baseBuilder.Info.CheckForNewBasesDelay; 81 } 82 } 83 84 if (waterState == WaterCheck.NotEnoughWater && --checkForBasesTicks <= 0) 85 { 86 var currentBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player); 87 88 if (currentBases > cachedBases) 89 { 90 cachedBases = currentBases; 91 waterState = WaterCheck.NotChecked; 92 } 93 } 94 95 // Only update once per second or so 96 if (--waitTicks > 0) 97 return; 98 99 playerBuildings = world.ActorsHavingTrait<Building>().Where(a => a.Owner == player).ToArray(); 100 var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Count() / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); 101 minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower); 102 103 var active = false; 104 foreach (var queue in AIUtils.FindQueues(player, category)) 105 if (TickQueue(bot, queue)) 106 active = true; 107 108 // Add a random factor so not every AI produces at the same tick early in the game. 109 // Minimum should not be negative as delays in HackyAI could be zero. 110 var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay); 111 112 // Needs to be at least 4 * OrderLatency because otherwise the AI frequently duplicates build orders (i.e. makes the same build decision twice) 113 waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + baseBuilder.Info.StructureProductionActiveDelay + randomFactor 114 : baseBuilder.Info.StructureProductionInactiveDelay + randomFactor; 115 } 116 TickQueue(IBot bot, ProductionQueue queue)117 bool TickQueue(IBot bot, ProductionQueue queue) 118 { 119 var currentBuilding = queue.AllQueued().FirstOrDefault(); 120 121 // Waiting to build something 122 if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts) 123 { 124 var item = ChooseBuildingToBuild(queue); 125 if (item == null) 126 return false; 127 128 bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1)); 129 } 130 else if (currentBuilding != null && currentBuilding.Done) 131 { 132 // Production is complete 133 // Choose the placement logic 134 // HACK: HACK HACK HACK 135 // TODO: Derive this from BuildingCommonNames instead 136 var type = BuildingType.Building; 137 138 // Check if Building is a defense and if we should place it towards the enemy or not. 139 if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo<AttackBaseInfo>() && world.LocalRandom.Next(100) < baseBuilder.Info.PlaceDefenseTowardsEnemyChance) 140 type = BuildingType.Defense; 141 else if (baseBuilder.Info.RefineryTypes.Contains(world.Map.Rules.Actors[currentBuilding.Item].Name)) 142 type = BuildingType.Refinery; 143 144 var location = ChooseBuildLocation(currentBuilding.Item, true, type); 145 if (location == null) 146 { 147 AIUtils.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item)); 148 bot.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1)); 149 failCount += failCount; 150 151 // If we just reached the maximum fail count, cache the number of current structures 152 if (failCount == baseBuilder.Info.MaximumFailedPlacementAttempts) 153 { 154 cachedBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player); 155 cachedBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player); 156 } 157 } 158 else 159 { 160 failCount = 0; 161 bot.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false) 162 { 163 // Building to place 164 TargetString = currentBuilding.Item, 165 166 // Actor ID to associate the placement with 167 ExtraData = queue.Actor.ActorID, 168 SuppressVisualFeedback = true 169 }); 170 171 return true; 172 } 173 } 174 175 return true; 176 } 177 GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null)178 ActorInfo GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null) 179 { 180 var available = buildables.Where(actor => 181 { 182 // Are we able to build this? 183 if (!actors.Contains(actor.Name)) 184 return false; 185 186 if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name)) 187 return true; 188 189 return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name]; 190 }); 191 192 if (orderBy != null) 193 return available.MaxByOrDefault(orderBy); 194 195 return available.RandomOrDefault(world.LocalRandom); 196 } 197 HasSufficientPowerForActor(ActorInfo actorInfo)198 bool HasSufficientPowerForActor(ActorInfo actorInfo) 199 { 200 return playerPower == null || (actorInfo.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault) 201 .Sum(p => p.Amount) + playerPower.ExcessPower) >= baseBuilder.Info.MinimumExcessPower; 202 } 203 ChooseBuildingToBuild(ProductionQueue queue)204 ActorInfo ChooseBuildingToBuild(ProductionQueue queue) 205 { 206 var buildableThings = queue.BuildableItems(); 207 208 // This gets used quite a bit, so let's cache it here 209 var power = GetProducibleBuilding(baseBuilder.Info.PowerTypes, buildableThings, 210 a => a.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount)); 211 212 // First priority is to get out of a low power situation 213 if (playerPower != null && playerPower.ExcessPower < minimumExcessPower) 214 { 215 if (power != null && power.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0) 216 { 217 AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name); 218 return power; 219 } 220 } 221 222 // Next is to build up a strong economy 223 if (!baseBuilder.HasAdequateRefineryCount) 224 { 225 var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings); 226 if (refinery != null && HasSufficientPowerForActor(refinery)) 227 { 228 AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); 229 return refinery; 230 } 231 232 if (power != null && refinery != null && !HasSufficientPowerForActor(refinery)) 233 { 234 AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); 235 return power; 236 } 237 } 238 239 // Make sure that we can spend as fast as we are earning 240 if (baseBuilder.Info.NewProductionCashThreshold > 0 && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold) 241 { 242 var production = GetProducibleBuilding(baseBuilder.Info.ProductionTypes, buildableThings); 243 if (production != null && HasSufficientPowerForActor(production)) 244 { 245 AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); 246 return production; 247 } 248 249 if (power != null && production != null && !HasSufficientPowerForActor(production)) 250 { 251 AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); 252 return power; 253 } 254 } 255 256 // Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings 257 if (waterState == WaterCheck.EnoughWater && baseBuilder.Info.NewProductionCashThreshold > 0 258 && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold 259 && AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes)) 260 { 261 var navalproduction = GetProducibleBuilding(baseBuilder.Info.NavalProductionTypes, buildableThings); 262 if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) 263 { 264 AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); 265 return navalproduction; 266 } 267 268 if (power != null && navalproduction != null && !HasSufficientPowerForActor(navalproduction)) 269 { 270 AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); 271 return power; 272 } 273 } 274 275 // Create some head room for resource storage if we really need it 276 if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity) 277 { 278 var silo = GetProducibleBuilding(baseBuilder.Info.SiloTypes, buildableThings); 279 if (silo != null && HasSufficientPowerForActor(silo)) 280 { 281 AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); 282 return silo; 283 } 284 285 if (power != null && silo != null && !HasSufficientPowerForActor(silo)) 286 { 287 AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); 288 return power; 289 } 290 } 291 292 // Build everything else 293 foreach (var frac in baseBuilder.Info.BuildingFractions.Shuffle(world.LocalRandom)) 294 { 295 var name = frac.Key; 296 297 // Does this building have initial delay, if so have we passed it? 298 if (baseBuilder.Info.BuildingDelays != null && 299 baseBuilder.Info.BuildingDelays.ContainsKey(name) && 300 baseBuilder.Info.BuildingDelays[name] > world.WorldTick) 301 continue; 302 303 // Can we build this structure? 304 if (!buildableThings.Any(b => b.Name == name)) 305 continue; 306 307 // Do we want to build this structure? 308 var count = playerBuildings.Count(a => a.Info.Name == name); 309 if (count * 100 > frac.Value * playerBuildings.Length) 310 continue; 311 312 if (baseBuilder.Info.BuildingLimits.ContainsKey(name) && baseBuilder.Info.BuildingLimits[name] <= count) 313 continue; 314 315 // If we're considering to build a naval structure, check whether there is enough water inside the base perimeter 316 // and any structure providing buildable area close enough to that water. 317 // TODO: Extend this check to cover any naval structure, not just production. 318 if (baseBuilder.Info.NavalProductionTypes.Contains(name) 319 && (waterState == WaterCheck.NotEnoughWater 320 || !AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes))) 321 continue; 322 323 // Will this put us into low power? 324 var actor = world.Map.Rules.Actors[name]; 325 if (playerPower != null && (playerPower.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor))) 326 { 327 // Try building a power plant instead 328 if (power != null && power.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0) 329 { 330 if (playerPower.PowerOutageRemainingTicks > 0) 331 AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); 332 else 333 AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); 334 335 return power; 336 } 337 } 338 339 // Lets build this 340 AIUtils.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}", 341 queue.Actor.Owner, name, frac.Value, frac.Value * playerBuildings.Length, playerBuildings.Length, count); 342 return actor; 343 } 344 345 // Too spammy to keep enabled all the time, but very useful when debugging specific issues. 346 // AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); 347 return null; 348 } 349 ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type)350 CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) 351 { 352 var actorInfo = world.Map.Rules.Actors[actorType]; 353 var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>(); 354 if (bi == null) 355 return null; 356 357 // Find the buildable cell that is closest to pos and centered around center 358 Func<CPos, CPos, int, int, CPos?> findPos = (center, target, minRange, maxRange) => 359 { 360 var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); 361 362 // Sort by distance to target if we have one 363 if (center != target) 364 cells = cells.OrderBy(c => (c - target).LengthSquared); 365 else 366 cells = cells.Shuffle(world.LocalRandom); 367 368 foreach (var cell in cells) 369 { 370 if (!world.CanPlaceBuilding(cell, actorInfo, bi, null)) 371 continue; 372 373 if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(world, player, actorInfo, cell)) 374 continue; 375 376 return cell; 377 } 378 379 return null; 380 }; 381 382 var baseCenter = baseBuilder.GetRandomBaseCenter(); 383 384 switch (type) 385 { 386 case BuildingType.Defense: 387 388 // Build near the closest enemy structure 389 var closestEnemy = world.ActorsHavingTrait<Building>().Where(a => !a.Disposed && player.Stances[a.Owner] == Stance.Enemy) 390 .ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); 391 392 var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter; 393 return findPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius); 394 395 case BuildingType.Refinery: 396 397 // Try and place the refinery near a resource field 398 var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) 399 .Where(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a))) 400 .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); 401 402 foreach (var r in nearbyResources) 403 { 404 var found = findPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); 405 if (found != null) 406 return found; 407 } 408 409 // Try and find a free spot somewhere else in the base 410 return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); 411 412 case BuildingType.Building: 413 return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, 414 distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); 415 } 416 417 // Can't find a build location 418 return null; 419 } 420 } 421 } 422