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.Generic;
14 using System.Linq;
15 using OpenRA.Activities;
16 using OpenRA.Mods.Common.Activities;
17 using OpenRA.Mods.Common.Orders;
18 using OpenRA.Mods.Common.Pathfinder;
19 using OpenRA.Primitives;
20 using OpenRA.Traits;
21 
22 namespace OpenRA.Mods.Common.Traits
23 {
24 	public class HarvesterInfo : ITraitInfo, Requires<MobileInfo>
25 	{
26 		public readonly HashSet<string> DeliveryBuildings = new HashSet<string>();
27 
28 		[Desc("How long (in ticks) to wait until (re-)checking for a nearby available DeliveryBuilding if not yet linked to one.")]
29 		public readonly int SearchForDeliveryBuildingDelay = 125;
30 
31 		[Desc("Cell to move to when automatically unblocking DeliveryBuilding.")]
32 		public readonly CVec UnblockCell = new CVec(0, 4);
33 
34 		[Desc("How much resources it can carry.")]
35 		public readonly int Capacity = 28;
36 
37 		public readonly int BaleLoadDelay = 4;
38 
39 		[Desc("How fast it can dump it's carryage.")]
40 		public readonly int BaleUnloadDelay = 4;
41 
42 		[Desc("How many bales can it dump at once.")]
43 		public readonly int BaleUnloadAmount = 1;
44 
45 		[Desc("How many squares to show the fill level.")]
46 		public readonly int PipCount = 7;
47 
48 		public readonly int HarvestFacings = 0;
49 
50 		[Desc("Which resources it can harvest.")]
51 		public readonly HashSet<string> Resources = new HashSet<string>();
52 
53 		[Desc("Percentage of maximum speed when fully loaded.")]
54 		public readonly int FullyLoadedSpeed = 85;
55 
56 		[Desc("Automatically scan for resources when created.")]
57 		public readonly bool SearchOnCreation = true;
58 
59 		[Desc("Initial search radius (in cells) from the refinery that created us.")]
60 		public readonly int SearchFromProcRadius = 24;
61 
62 		[Desc("Search radius (in cells) from the last harvest order location to find more resources.")]
63 		public readonly int SearchFromHarvesterRadius = 12;
64 
65 		[Desc("Interval to wait between searches when there are no resources nearby.")]
66 		public readonly int WaitDuration = 25;
67 
68 		[Desc("Find a new refinery to unload at if more than this many harvesters are already waiting.")]
69 		public readonly int MaxUnloadQueue = 3;
70 
71 		[Desc("The pathfinding cost penalty applied for each harvester waiting to unload at a refinery.")]
72 		public readonly int UnloadQueueCostModifier = 12;
73 
74 		[Desc("The pathfinding cost penalty applied for cells directly away from the refinery.")]
75 		public readonly int ResourceRefineryDirectionPenalty = 200;
76 
77 		[Desc("Does the unit queue harvesting runs instead of individual harvest actions?")]
78 		public readonly bool QueueFullLoad = false;
79 
80 		[GrantedConditionReference]
81 		[Desc("Condition to grant while empty.")]
82 		public readonly string EmptyCondition = null;
83 
84 		[VoiceReference]
85 		public readonly string HarvestVoice = "Action";
86 
87 		[VoiceReference]
88 		public readonly string DeliverVoice = "Action";
89 
Create(ActorInitializer init)90 		public object Create(ActorInitializer init) { return new Harvester(init.Self, this); }
91 	}
92 
93 	public class Harvester : IIssueOrder, IResolveOrder, IPips, IOrderVoice,
94 		ISpeedModifier, ISync, INotifyCreated
95 	{
96 		public readonly HarvesterInfo Info;
97 		readonly Mobile mobile;
98 		readonly ResourceLayer resLayer;
99 		readonly ResourceClaimLayer claimLayer;
100 		readonly Dictionary<ResourceTypeInfo, int> contents = new Dictionary<ResourceTypeInfo, int>();
101 		INotifyHarvesterAction[] notifyHarvesterAction;
102 		ConditionManager conditionManager;
103 		int conditionToken = ConditionManager.InvalidConditionToken;
104 		HarvesterResourceMultiplier[] resourceMultipliers;
105 
106 		[Sync]
107 		public Actor LastLinkedProc = null;
108 
109 		[Sync]
110 		public Actor LinkedProc = null;
111 
112 		[Sync]
113 		int currentUnloadTicks;
114 
115 		[Sync]
116 		public int ContentValue
117 		{
118 			get
119 			{
120 				var value = 0;
121 				foreach (var c in contents)
122 					value += c.Key.ValuePerUnit * c.Value;
123 				return value;
124 			}
125 		}
126 
Harvester(Actor self, HarvesterInfo info)127 		public Harvester(Actor self, HarvesterInfo info)
128 		{
129 			Info = info;
130 			mobile = self.Trait<Mobile>();
131 			resLayer = self.World.WorldActor.Trait<ResourceLayer>();
132 			claimLayer = self.World.WorldActor.Trait<ResourceClaimLayer>();
133 		}
134 
INotifyCreated.Created(Actor self)135 		void INotifyCreated.Created(Actor self)
136 		{
137 			notifyHarvesterAction = self.TraitsImplementing<INotifyHarvesterAction>().ToArray();
138 			resourceMultipliers = self.TraitsImplementing<HarvesterResourceMultiplier>().ToArray();
139 			conditionManager = self.TraitOrDefault<ConditionManager>();
140 			UpdateCondition(self);
141 
142 			self.QueueActivity(new CallFunc(() => ChooseNewProc(self, null)));
143 
144 			// Note: This is queued in a FrameEndTask because otherwise the activity is dropped/overridden while moving out of a factory.
145 			if (Info.SearchOnCreation)
146 				self.World.AddFrameEndTask(w => self.QueueActivity(new FindAndDeliverResources(self)));
147 		}
148 
LinkProc(Actor self, Actor proc)149 		public void LinkProc(Actor self, Actor proc)
150 		{
151 			LinkedProc = proc;
152 		}
153 
UnlinkProc(Actor self, Actor proc)154 		public void UnlinkProc(Actor self, Actor proc)
155 		{
156 			if (LinkedProc == proc)
157 				ChooseNewProc(self, proc);
158 		}
159 
ChooseNewProc(Actor self, Actor ignore)160 		public void ChooseNewProc(Actor self, Actor ignore)
161 		{
162 			LastLinkedProc = null;
163 			LinkProc(self, ClosestProc(self, ignore));
164 		}
165 
IsAcceptableProcType(Actor proc)166 		bool IsAcceptableProcType(Actor proc)
167 		{
168 			return Info.DeliveryBuildings.Count == 0 ||
169 				Info.DeliveryBuildings.Contains(proc.Info.Name);
170 		}
171 
ClosestProc(Actor self, Actor ignore)172 		public Actor ClosestProc(Actor self, Actor ignore)
173 		{
174 			// Find all refineries and their occupancy count:
175 			var refs = self.World.ActorsWithTrait<IAcceptResources>()
176 				.Where(r => r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor))
177 				.Select(r => new
178 				{
179 					Location = r.Actor.Location + r.Trait.DeliveryOffset,
180 					Actor = r.Actor,
181 					Occupancy = self.World.ActorsHavingTrait<Harvester>(h => h.LinkedProc == r.Actor).Count()
182 				}).ToDictionary(r => r.Location);
183 
184 			// Start a search from each refinery's delivery location:
185 			List<CPos> path;
186 
187 			using (var search = PathSearch.FromPoints(self.World, mobile.Locomotor, self, refs.Values.Select(r => r.Location), self.Location, BlockedByActor.None)
188 				.WithCustomCost(loc =>
189 				{
190 					if (!refs.ContainsKey(loc))
191 						return 0;
192 
193 					var occupancy = refs[loc].Occupancy;
194 
195 					// Too many harvesters clogs up the refinery's delivery location:
196 					if (occupancy >= Info.MaxUnloadQueue)
197 						return PathGraph.CostForInvalidCell;
198 
199 					// Prefer refineries with less occupancy (multiplier is to offset distance cost):
200 					return occupancy * Info.UnloadQueueCostModifier;
201 				}))
202 				path = self.World.WorldActor.Trait<IPathFinder>().FindPath(search);
203 
204 			if (path.Count != 0)
205 				return refs[path.Last()].Actor;
206 
207 			return null;
208 		}
209 
210 		public bool IsFull { get { return contents.Values.Sum() == Info.Capacity; } }
211 		public bool IsEmpty { get { return contents.Values.Sum() == 0; } }
212 		public int Fullness { get { return contents.Values.Sum() * 100 / Info.Capacity; } }
213 
UpdateCondition(Actor self)214 		void UpdateCondition(Actor self)
215 		{
216 			if (string.IsNullOrEmpty(Info.EmptyCondition) || conditionManager == null)
217 				return;
218 
219 			var enabled = IsEmpty;
220 
221 			if (enabled && conditionToken == ConditionManager.InvalidConditionToken)
222 				conditionToken = conditionManager.GrantCondition(self, Info.EmptyCondition);
223 			else if (!enabled && conditionToken != ConditionManager.InvalidConditionToken)
224 				conditionToken = conditionManager.RevokeCondition(self, conditionToken);
225 		}
226 
AcceptResource(Actor self, ResourceType type)227 		public void AcceptResource(Actor self, ResourceType type)
228 		{
229 			if (!contents.ContainsKey(type.Info))
230 				contents[type.Info] = 1;
231 			else
232 				contents[type.Info]++;
233 
234 			UpdateCondition(self);
235 		}
236 
237 		// Returns true when unloading is complete
TickUnload(Actor self, Actor proc)238 		public bool TickUnload(Actor self, Actor proc)
239 		{
240 			// Wait until the next bale is ready
241 			if (--currentUnloadTicks > 0)
242 				return false;
243 
244 			if (contents.Keys.Count > 0)
245 			{
246 				var type = contents.First().Key;
247 				var iao = proc.Trait<IAcceptResources>();
248 				var count = Math.Min(contents[type], Info.BaleUnloadAmount);
249 				var value = Util.ApplyPercentageModifiers(type.ValuePerUnit * count, resourceMultipliers.Select(m => m.GetModifier()));
250 
251 				if (!iao.CanGiveResource(value))
252 					return false;
253 
254 				iao.GiveResource(value);
255 				contents[type] -= count;
256 				if (contents[type] == 0)
257 					contents.Remove(type);
258 
259 				currentUnloadTicks = Info.BaleUnloadDelay;
260 				UpdateCondition(self);
261 			}
262 
263 			return contents.Count == 0;
264 		}
265 
CanHarvestCell(Actor self, CPos cell)266 		public bool CanHarvestCell(Actor self, CPos cell)
267 		{
268 			// Resources only exist in the ground layer
269 			if (cell.Layer != 0)
270 				return false;
271 
272 			var resType = resLayer.GetResourceType(cell);
273 			if (resType == null)
274 				return false;
275 
276 			// Can the harvester collect this kind of resource?
277 			return Info.Resources.Contains(resType.Info.Type);
278 		}
279 
280 		IEnumerable<IOrderTargeter> IIssueOrder.Orders
281 		{
282 			get
283 			{
284 				yield return new EnterAlliedActorTargeter<IAcceptResourcesInfo>("Deliver", 5,
285 					(proc, _) => IsAcceptableProcType(proc),
286 					proc => proc.Trait<IAcceptResources>().AllowDocking);
287 				yield return new HarvestOrderTargeter();
288 			}
289 		}
290 
IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)291 		Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
292 		{
293 			if (order.OrderID == "Deliver" || order.OrderID == "Harvest")
294 				return new Order(order.OrderID, self, target, queued);
295 
296 			return null;
297 		}
298 
IOrderVoice.VoicePhraseForOrder(Actor self, Order order)299 		string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
300 		{
301 			if (order.OrderString == "Harvest")
302 				return Info.HarvestVoice;
303 
304 			if (order.OrderString == "Deliver" && !IsEmpty)
305 				return Info.DeliverVoice;
306 
307 			return null;
308 		}
309 
IResolveOrder.ResolveOrder(Actor self, Order order)310 		void IResolveOrder.ResolveOrder(Actor self, Order order)
311 		{
312 			if (order.OrderString == "Harvest")
313 			{
314 				// NOTE: An explicit harvest order allows the harvester to decide which refinery to deliver to.
315 				LinkProc(self, null);
316 
317 				CPos loc;
318 				if (order.Target.Type != TargetType.Invalid)
319 				{
320 					// Find the nearest claimable cell to the order location (useful for group-select harvest):
321 					var cell = self.World.Map.CellContaining(order.Target.CenterPosition);
322 					loc = mobile.NearestCell(cell, p => mobile.CanEnterCell(p) && claimLayer.TryClaimCell(self, p), 1, 6);
323 				}
324 				else
325 				{
326 					// A bot order gives us a CPos.Zero TargetLocation.
327 					loc = self.Location;
328 				}
329 
330 				// FindResources takes care of calling INotifyHarvesterAction
331 				self.QueueActivity(order.Queued, new FindAndDeliverResources(self, loc));
332 				self.ShowTargetLines();
333 			}
334 			else if (order.OrderString == "Deliver")
335 			{
336 				// Deliver orders are only valid for own/allied actors,
337 				// which are guaranteed to never be frozen.
338 				if (order.Target.Type != TargetType.Actor)
339 					return;
340 
341 				var targetActor = order.Target.Actor;
342 				var iao = targetActor.TraitOrDefault<IAcceptResources>();
343 				if (iao == null || !iao.AllowDocking || !IsAcceptableProcType(targetActor))
344 					return;
345 
346 				self.QueueActivity(order.Queued, new FindAndDeliverResources(self, targetActor));
347 				self.ShowTargetLines();
348 			}
349 		}
350 
GetPipAt(int i)351 		PipType GetPipAt(int i)
352 		{
353 			var n = i * Info.Capacity / Info.PipCount;
354 
355 			foreach (var rt in contents)
356 				if (n < rt.Value)
357 					return rt.Key.PipColor;
358 				else
359 					n -= rt.Value;
360 
361 			return PipType.Transparent;
362 		}
363 
IPips.GetPips(Actor self)364 		IEnumerable<PipType> IPips.GetPips(Actor self)
365 		{
366 			var numPips = Info.PipCount;
367 
368 			for (var i = 0; i < numPips; i++)
369 				yield return GetPipAt(i);
370 		}
371 
ISpeedModifier.GetSpeedModifier()372 		int ISpeedModifier.GetSpeedModifier()
373 		{
374 			return 100 - (100 - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity;
375 		}
376 
377 		class HarvestOrderTargeter : IOrderTargeter
378 		{
379 			public string OrderID { get { return "Harvest"; } }
380 			public int OrderPriority { get { return 10; } }
381 			public bool IsQueued { get; protected set; }
TargetOverridesSelection(Actor self, Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers)382 			public bool TargetOverridesSelection(Actor self, Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers) { return true; }
383 
CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)384 			public bool CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)
385 			{
386 				if (target.Type != TargetType.Terrain)
387 					return false;
388 
389 				if (modifiers.HasModifier(TargetModifiers.ForceMove))
390 					return false;
391 
392 				var location = self.World.Map.CellContaining(target.CenterPosition);
393 
394 				// Don't leak info about resources under the shroud
395 				if (!self.Owner.Shroud.IsExplored(location))
396 					return false;
397 
398 				var res = self.World.WorldActor.Trait<ResourceRenderer>().GetRenderedResourceType(location);
399 				var info = self.Info.TraitInfo<HarvesterInfo>();
400 
401 				if (res == null || !info.Resources.Contains(res.Info.Type))
402 					return false;
403 
404 				cursor = "harvest";
405 				IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
406 
407 				return true;
408 			}
409 		}
410 	}
411 }
412