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