#region Copyright & License Information /* * Copyright 2007-2019 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.Collections.Generic; using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Controls AI unit production.")] public class UnitBuilderBotModuleInfo : ConditionalTraitInfo { // TODO: Investigate whether this might the (or at least one) reason why bots occasionally get into a state of doing nothing. // Reason: If this is less than SquadSize, the bot might get stuck between not producing more units due to this, // but also not creating squads since there aren't enough idle units. [Desc("Only produce units as long as there are less than this amount of units idling inside the base.")] public readonly int IdleBaseUnitsMaximum = 12; [Desc("Production queues AI uses for producing units.")] public readonly HashSet UnitQueues = new HashSet { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; [Desc("What units to the AI should build.", "What relative share of the total army must be this type of unit.")] public readonly Dictionary UnitsToBuild = null; [Desc("What units should the AI have a maximum limit to train.")] public readonly Dictionary UnitLimits = null; public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); } } public class UnitBuilderBotModule : ConditionalTrait, IBotTick, IBotNotifyIdleBaseUnits, IBotRequestUnitProduction { public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. readonly World world; readonly Player player; readonly List queuedBuildRequests = new List(); IBotRequestPauseUnitProduction[] requestPause; List idleUnits = new List(); int ticks; public UnitBuilderBotModule(Actor self, UnitBuilderBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; } protected override void TraitEnabled(Actor self) { requestPause = player.PlayerActor.TraitsImplementing().ToArray(); } void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) { this.idleUnits = idleUnits; } void IBotTick.BotTick(IBot bot) { if (requestPause.Any(rp => rp.PauseUnitProduction)) return; ticks++; if (ticks % FeedbackTime == 0) { var buildRequest = queuedBuildRequests.FirstOrDefault(); if (buildRequest != null) { BuildUnit(bot, buildRequest); queuedBuildRequests.Remove(buildRequest); } foreach (var q in Info.UnitQueues) BuildUnit(bot, q, idleUnits.Count < Info.IdleBaseUnitsMaximum); } } void IBotRequestUnitProduction.RequestUnitProduction(IBot bot, string requestedActor) { queuedBuildRequests.Add(requestedActor); } int IBotRequestUnitProduction.RequestedProductionCount(IBot bot, string requestedActor) { return queuedBuildRequests.Count(r => r == requestedActor); } void BuildUnit(IBot bot, string category, bool buildRandom) { // Pick a free queue var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any()); if (queue == null) return; var unit = buildRandom ? ChooseRandomUnitToBuild(queue) : ChooseUnitToBuild(queue); if (unit == null) return; var name = unit.Name; if (Info.UnitsToBuild != null && !Info.UnitsToBuild.ContainsKey(name)) return; if (Info.UnitLimits != null && Info.UnitLimits.ContainsKey(name) && world.Actors.Count(a => a.Owner == player && a.Info.Name == name) >= Info.UnitLimits[name]) return; bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1)); } // In cases where we want to build a specific unit but don't know the queue name (because there's more than one possibility) void BuildUnit(IBot bot, string name) { var actorInfo = world.Map.Rules.Actors[name]; if (actorInfo == null) return; var buildableInfo = actorInfo.TraitInfoOrDefault(); if (buildableInfo == null) return; ProductionQueue queue = null; foreach (var pq in buildableInfo.Queue) { queue = AIUtils.FindQueues(player, pq).FirstOrDefault(q => !q.AllQueued().Any()); if (queue != null) break; } if (queue != null) { bot.QueueOrder(Order.StartProduction(queue.Actor, name, 1)); AIUtils.BotDebug("AI: {0} decided to build {1} (external request)", queue.Actor.Owner, name); } } ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) { var buildableThings = queue.BuildableItems(); if (!buildableThings.Any()) return null; var unit = buildableThings.Random(world.LocalRandom); return HasAdequateAirUnitReloadBuildings(unit) ? unit : null; } ActorInfo ChooseUnitToBuild(ProductionQueue queue) { var buildableThings = queue.BuildableItems(); if (!buildableThings.Any()) return null; var myUnits = player.World .ActorsHavingTrait() .Where(a => a.Owner == player) .Select(a => a.Info.Name).ToList(); foreach (var unit in Info.UnitsToBuild.Shuffle(world.LocalRandom)) if (buildableThings.Any(b => b.Name == unit.Key)) if (myUnits.Count(a => a == unit.Key) * 100 < unit.Value * myUnits.Count) if (HasAdequateAirUnitReloadBuildings(world.Map.Rules.Actors[unit.Key])) return world.Map.Rules.Actors[unit.Key]; return null; } // For mods like RA (number of RearmActors must match the number of aircraft) bool HasAdequateAirUnitReloadBuildings(ActorInfo actorInfo) { var aircraftInfo = actorInfo.TraitInfoOrDefault(); if (aircraftInfo == null) return true; // If actor isn't Rearmable, it doesn't need a RearmActor to reload var rearmableInfo = actorInfo.TraitInfoOrDefault(); if (rearmableInfo == null) return true; var countOwnAir = AIUtils.CountActorsWithTrait(actorInfo.Name, player); var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait(b, player)); if (countOwnAir >= countBuildings) return false; return true; } } }