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