context: #region Copyright & License Information
  /*
   * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
   * This file is part of OpenRA, which is free software. It is made
   * available to you under the terms of the GNU General Public License
   * as published by the Free Software Foundation, either version 3 of
   * the License, or (at your option) any later version. For more
   * information, see COPYING.
   */
  #endregion
  
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using OpenRA.Primitives;
  using OpenRA.Traits;
  
  namespace OpenRA.Mods.Common.Traits
  {
  	[Desc("Attach this to an actor (usually a building) to let it produce units or construct buildings.",
  		"If one builds another actor of this type, he will get a separate queue to create two actors",
  		"at the same time. Will only work together with the Production: trait.")]
  	public class ProductionQueueInfo : TraitInfo
  	{
  		[FieldLoader.Require]
  		[Desc("What kind of production will be added (e.g. Building, Infantry, Vehicle, ...)")]
  		public readonly string Type = null;
  
  		[Desc("The value used when ordering this for display (e.g. in the Spectator UI).")]
  		public readonly int DisplayOrder = 0;
  
  		[Desc("Group queues from separate buildings together into the same tab.")]
  		public readonly string Group = null;
  
  		[Desc("Only enable this queue for certain factions.")]
  		public readonly HashSet<string> Factions = new HashSet<string>();
  
  		[Desc("Should the prerequisite remain enabled if the owner changes?")]
  		public readonly bool Sticky = true;
  
  		[Desc("Should right clicking on the icon instantly cancel the production instead of putting it on hold?")]
  		public readonly bool DisallowPaused = false;
  
  		[Desc("This percentage value is multiplied with actor cost to translate into build time (lower means faster).")]
  		public readonly int BuildDurationModifier = 100;
  
  		[Desc("Maximum number of a single actor type that can be queued (0 = infinite).")]
  		public readonly int ItemLimit = 999;
  
  		[Desc("Maximum number of items that can be queued across all actor types (0 = infinite).")]
  		public readonly int QueueLimit = 0;
  
  		[Desc("The build time is multiplied with this percentage on low power.")]
  		public readonly int LowPowerModifier = 100;
  
  		[Desc("Production items that have more than this many items in the queue will be produced in a loop.")]
  		public readonly int InfiniteBuildLimit = -1;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when production is complete.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string ReadyAudio = null;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when you can't train another actor",
  			"when the build limit exceeded or the exit is jammed.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string BlockedAudio = null;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when you can't queue another actor",
  			"when the queue length limit is exceeded.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string LimitedAudio = null;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when user clicks on the build palette icon.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string QueuedAudio = null;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when player right-clicks on the build palette icon.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string OnHoldAudio = null;
  
  		[NotificationReference("Speech")]
  		[Desc("Notification played when player right-clicks on a build palette icon that is already on hold.",
  			"The filename of the audio is defined per faction in notifications.yaml.")]
  		public readonly string CancelledAudio = null;
  
  		public override object Create(ActorInitializer init) { return new ProductionQueue(init, init.Self.Owner.PlayerActor, this); }
  
  		public void RulesetLoaded(Ruleset rules, ActorInfo ai)
  		{
  			if (LowPowerModifier <= 0)
  				throw new YamlException("Production queue must have LowPowerModifier of at least 1.");
  		}
  	}
  
  	public class ProductionQueue : IResolveOrder, ITick, ITechTreeElement, INotifyOwnerChanged, INotifyKilled, INotifySold, ISync, INotifyTransform, INotifyCreated
  	{
  		public readonly ProductionQueueInfo Info;
  		readonly Actor self;
  
  		// A list of things we could possibly build
  		protected readonly Dictionary<ActorInfo, ProductionState> Producible = new Dictionary<ActorInfo, ProductionState>();
  		protected readonly List<ProductionItem> Queue = new List<ProductionItem>();
  		readonly IEnumerable<ActorInfo> allProducibles;
  		readonly IEnumerable<ActorInfo> buildableProducibles;
  
  		protected Production[] productionTraits;
  
  		// Will change if the owner changes
  		PowerManager playerPower;
  		protected PlayerResources playerResources;
  		protected DeveloperMode developerMode;
  		protected TechTree techTree;
  
  		public Actor Actor { get { return self; } }
  
  		[Sync]
  		public bool Enabled { get; protected set; }
  
  		public string Faction { get; private set; }
  
  		[Sync]
  		public bool IsValidFaction { get; private set; }
  
  		public ProductionQueue(ActorInitializer init, Actor playerActor, ProductionQueueInfo info)
  		{
  			self = init.Self;
  			Info = info;
  
  			Faction = init.GetValue<FactionInit, string>(self.Owner.Faction.InternalName);
  			IsValidFaction = !info.Factions.Any() || info.Factions.Contains(Faction);
  			Enabled = IsValidFaction;
  
  			allProducibles = Producible.Where(a => a.Value.Buildable || a.Value.Visible).Select(a => a.Key);
  			buildableProducibles = Producible.Where(a => a.Value.Buildable).Select(a => a.Key);
  		}
  
  		void INotifyCreated.Created(Actor self)
  		{
  			playerPower = self.Owner.PlayerActor.TraitOrDefault<PowerManager>();
  			playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
  			developerMode = self.Owner.PlayerActor.Trait<DeveloperMode>();
  			techTree = self.Owner.PlayerActor.Trait<TechTree>();
  
  			productionTraits = self.TraitsImplementing<Production>().Where(p => p.Info.Produces.Contains(Info.Type)).ToArray();
  			CacheProducibles(self.Owner.PlayerActor);
  		}
  
  		protected void ClearQueue()
  		{
  			// Refund the current item
  			foreach (var item in Queue)
  				playerResources.GiveCash(item.TotalCost - item.RemainingCost);
  			Queue.Clear();
  		}
  
  		void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
  		{
  			ClearQueue();
  
  			playerPower = newOwner.PlayerActor.TraitOrDefault<PowerManager>();
  			playerResources = newOwner.PlayerActor.Trait<PlayerResources>();
  			developerMode = newOwner.PlayerActor.Trait<DeveloperMode>();
  			techTree = newOwner.PlayerActor.Trait<TechTree>();
  
  			if (!Info.Sticky)
  			{
  				Faction = self.Owner.Faction.InternalName;
  				IsValidFaction = !Info.Factions.Any() || Info.Factions.Contains(Faction);
  			}
  
  			// Regenerate the producibles and tech tree state
  			oldOwner.PlayerActor.Trait<TechTree>().Remove(this);
  			CacheProducibles(newOwner.PlayerActor);
  			techTree.Update();
  		}
  
  		void INotifyKilled.Killed(Actor killed, AttackInfo e) { if (killed == self) { ClearQueue(); Enabled = false; } }
  		void INotifySold.Selling(Actor self) { ClearQueue(); Enabled = false; }
  		void INotifySold.Sold(Actor self) { }
  
  		void INotifyTransform.BeforeTransform(Actor self) { ClearQueue(); Enabled = false; }
  		void INotifyTransform.OnTransform(Actor self) { }
  		void INotifyTransform.AfterTransform(Actor self) { }
  
  		void CacheProducibles(Actor playerActor)
  		{
  			Producible.Clear();
  			if (!Enabled)
  				return;
  
  			foreach (var a in AllBuildables(Info.Type))
  			{
  				var bi = a.TraitInfo<BuildableInfo>();
  
  				Producible.Add(a, new ProductionState());
  				techTree.Add(a.Name, bi.Prerequisites, bi.BuildLimit, this);
  			}
  		}
  
  		IEnumerable<ActorInfo> AllBuildables(string category)
  		{
  			return self.World.Map.Rules.Actors.Values
  				.Where(x =>
  					x.Name[0] != '^' &&
  					x.HasTraitInfo<BuildableInfo>() &&
  					x.TraitInfo<BuildableInfo>().Queue.Contains(category));
  		}
  
  		public void PrerequisitesAvailable(string key)
  		{
  			Producible[self.World.Map.Rules.Actors[key]].Buildable = true;
  		}
  
  		public void PrerequisitesUnavailable(string key)
  		{
  			Producible[self.World.Map.Rules.Actors[key]].Buildable = false;
  		}
  
  		public void PrerequisitesItemHidden(string key)
  		{
  			Producible[self.World.Map.Rules.Actors[key]].Visible = false;
  		}
  
  		public void PrerequisitesItemVisible(string key)
  		{
  			Producible[self.World.Map.Rules.Actors[key]].Visible = true;
  		}
  
  		public virtual bool IsProducing(ProductionItem item)
  		{
  			return Queue.Count > 0 && Queue[0] == item;
  		}
  
  		public ProductionItem CurrentItem()
  		{
  			return Queue.ElementAtOrDefault(0);
  		}
  
  		public virtual IEnumerable<ProductionItem> AllQueued()
  		{
  			return Queue;
  		}
  
  		public virtual IEnumerable<ActorInfo> AllItems()
  		{
  			if (productionTraits.Any() && productionTraits.All(p => p.IsTraitDisabled))
  				return Enumerable.Empty<ActorInfo>();
  			if (developerMode.AllTech)
  				return Producible.Keys;
  
  			return allProducibles;
  		}
  
  		public virtual IEnumerable<ActorInfo> BuildableItems()
  		{
  			if (productionTraits.Any() && productionTraits.All(p => p.IsTraitDisabled))
  				return Enumerable.Empty<ActorInfo>();
  			if (!Enabled)
  				return Enumerable.Empty<ActorInfo>();
  			if (developerMode.AllTech)
  				return Producible.Keys;
  
  			return buildableProducibles;
  		}
  
  		public bool CanBuild(ActorInfo actor)
  		{
  			if (!Producible.TryGetValue(actor, out var ps))
  				return false;
  
  			return ps.Buildable || developerMode.AllTech;
  		}
  
  		void ITick.Tick(Actor self)
  		{
  			Tick(self);
  		}
  
  		protected virtual void Tick(Actor self)
  		{
  			// PERF: Avoid LINQ when checking whether all production traits are disabled/paused
  			var anyEnabledProduction = false;
  			var anyUnpausedProduction = false;
  			foreach (var p in productionTraits)
  			{
  				anyEnabledProduction |= !p.IsTraitDisabled;
  				anyUnpausedProduction |= !p.IsTraitPaused;
  			}
  
  			if (!anyEnabledProduction)
  				ClearQueue();
  
  			Enabled = IsValidFaction && anyEnabledProduction;
  			TickInner(self, !anyUnpausedProduction);
  		}
  
  		protected virtual void TickInner(Actor self, bool allProductionPaused)
  		{
  			CancelUnbuildableItems();
  
  			if (Queue.Count > 0 && !allProductionPaused)
  				Queue[0].Tick(playerResources);
  		}
  
  		protected void CancelUnbuildableItems()
  		{
  			var buildableNames = BuildableItems().Select(b => b.Name).ToList();
  
  			// EndProduction removes the item from the queue, so we enumerate
  			// by index in reverse to avoid issues with index reassignment
  			for (var i = Queue.Count - 1; i >= 0; i--)
  			{
  				if (buildableNames.Contains(Queue[i].Item))
  					continue;
  
  				// Refund what's been paid so far
  				playerResources.GiveCash(Queue[i].TotalCost - Queue[i].RemainingCost);
  				EndProduction(Queue[i]);
  			}
  		}
  
  		public bool CanQueue(ActorInfo actor, out string notificationAudio)
  		{
  			notificationAudio = Info.BlockedAudio;
  
  			var bi = actor.TraitInfoOrDefault<BuildableInfo>();
  			if (bi == null)
  				return false;
  
  			if (!developerMode.AllTech)
  			{
  				if (Info.QueueLimit > 0 && Queue.Count >= Info.QueueLimit)
  				{
  					notificationAudio = Info.LimitedAudio;
  					return false;
  				}
  
  				var queueCount = Queue.Count(i => i.Item == actor.Name);
  				if (Info.ItemLimit > 0 && queueCount >= Info.ItemLimit)
  				{
  					notificationAudio = Info.LimitedAudio;
  					return false;
  				}
  
  				if (bi.BuildLimit > 0)
  				{
  					var owned = self.Owner.World.ActorsHavingTrait<Buildable>()
  						.Count(a => a.Info.Name == actor.Name && a.Owner == self.Owner);
  					if (queueCount + owned >= bi.BuildLimit)
  						return false;
  				}
  			}
  
  			notificationAudio = Info.QueuedAudio;
  			return true;
  		}
  
  		public void ResolveOrder(Actor self, Order order)
  		{
  			if (!Enabled)
  				return;
  
  			var rules = self.World.Map.Rules;
  			switch (order.OrderString)
  			{
  				case "StartProduction":
  					var unit = rules.Actors[order.TargetString];
  					var bi = unit.TraitInfo<BuildableInfo>();
  
  					// Not built by this queue
  					if (!bi.Queue.Contains(Info.Type))
  						return;
  
  					// You can't build that
  					if (BuildableItems().All(b => b.Name != order.TargetString))
  						return;
  
  					// Check if the player is trying to build more units that they are allowed
  					var fromLimit = int.MaxValue;
  					if (!developerMode.AllTech)
  					{
  						if (Info.QueueLimit > 0)
  							fromLimit = Info.QueueLimit - Queue.Count;
  
  						if (Info.ItemLimit > 0)
  							fromLimit = Math.Min(fromLimit, Info.ItemLimit - Queue.Count(i => i.Item == order.TargetString));
  
  						if (bi.BuildLimit > 0)
  						{
  							var inQueue = Queue.Count(pi => pi.Item == order.TargetString);
  							var owned = self.Owner.World.ActorsHavingTrait<Buildable>().Count(a => a.Info.Name == order.TargetString && a.Owner == self.Owner);
  							fromLimit = Math.Min(fromLimit, bi.BuildLimit - (inQueue + owned));
  						}
  
  						if (fromLimit <= 0)
  							return;
  					}
  
  					var cost = GetProductionCost(unit);
  					var time = GetBuildTime(unit, bi);
  					var amountToBuild = Math.Min(fromLimit, order.ExtraData);
  					for (var n = 0; n < amountToBuild; n++)
  					{
  						var hasPlayedSound = false;
  						BeginProduction(new ProductionItem(this, order.TargetString, cost, playerPower, () => self.World.AddFrameEndTask(_ =>
  						{
  							var isBuilding = unit.HasTraitInfo<BuildingInfo>();
  
  							if (isBuilding && !hasPlayedSound)
  								hasPlayedSound = Game.Sound.PlayNotification(rules, self.Owner, "Speech", Info.ReadyAudio, self.Owner.Faction.InternalName);
  							else if (!isBuilding)
  							{
  								if (BuildUnit(unit))
  									Game.Sound.PlayNotification(rules, self.Owner, "Speech", Info.ReadyAudio, self.Owner.Faction.InternalName);
  
gt: 								else if (!hasPlayedSound && time > 0)