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