#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * 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 { [TraitLocation(SystemActors.Player)] [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("If > 0, only produce units as long as there are less than this amount of units idling inside the base.", "Beware: if it is less than squad size, e.g. the `SquadSize` from `SquadManagerBotModule`, the bot might get stuck as there aren't enough idle units to create squad.")] public readonly int IdleBaseUnitsMaximum = -1; [Desc("Production queues AI uses for producing units.")] public readonly string[] UnitQueues = { "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; [Desc("When should the AI start train specific units.")] public readonly Dictionary UnitDelays = null; [Desc("Only queue construction of a new unit when above this requirement.")] public readonly int ProductionMinCashRequirement = 500; public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); } } public class UnitBuilderBotModule : ConditionalTrait, IBotTick, IBotNotifyIdleBaseUnits, IBotRequestUnitProduction, IGameSaveTraitData, INotifyActorDisposing { public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. readonly World world; readonly Player player; readonly List queuedBuildRequests = new(); readonly ActorIndex.OwnerAndNames unitsToBuild; IBotRequestPauseUnitProduction[] requestPause; int idleUnitCount; int currentQueueIndex = 0; PlayerResources playerResources; int ticks; public UnitBuilderBotModule(Actor self, UnitBuilderBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; unitsToBuild = new ActorIndex.OwnerAndNames(world, info.UnitsToBuild.Keys, player); } protected override void Created(Actor self) { requestPause = self.Owner.PlayerActor.TraitsImplementing().ToArray(); playerResources = self.Owner.PlayerActor.Trait(); } void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) { idleUnitCount = idleUnits.Count; } void IBotTick.BotTick(IBot bot) { // PERF: We shouldn't be queueing new units when we're low on cash if (playerResources.GetCashAndResources() < Info.ProductionMinCashRequirement || requestPause.Any(rp => rp.PauseUnitProduction)) return; ticks++; if (ticks % FeedbackTime == 0) { var buildRequest = queuedBuildRequests.FirstOrDefault(); if (buildRequest != null) { BuildUnit(bot, buildRequest); queuedBuildRequests.Remove(buildRequest); } if (Info.IdleBaseUnitsMaximum <= 0 || Info.IdleBaseUnitsMaximum > idleUnitCount) { for (var i = 0; i < Info.UnitQueues.Length; i++) { if (++currentQueueIndex >= Info.UnitQueues.Length) currentQueueIndex = 0; if (AIUtils.FindQueues(player, Info.UnitQueues[currentQueueIndex]).Any()) { // PERF: We tick only one type of valid queue at a time // if AI gets enough cash, it can fill all of its queues with enough ticks BuildRandomUnit(bot, Info.UnitQueues[currentQueueIndex]); break; } } } } } void IBotRequestUnitProduction.RequestUnitProduction(IBot bot, string requestedActor) { queuedBuildRequests.Add(requestedActor); } int IBotRequestUnitProduction.RequestedProductionCount(IBot bot, string requestedActor) { return queuedBuildRequests.Count(r => r == requestedActor); } void BuildRandomUnit(IBot bot, string category) { if (Info.UnitsToBuild.Count == 0) return; // Pick a free queue var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any()); if (queue == null) return; var unit = ChooseRandomUnitToBuild(queue); if (unit == null) return; bot.QueueOrder(Order.StartProduction(queue.Actor, unit.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("{0} decided to build {1} (external request)", queue.Actor.Owner, name); } } ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) { var buildableThings = queue.BuildableItems().Shuffle(world.LocalRandom).ToArray(); if (buildableThings.Length == 0) return null; var allUnits = unitsToBuild.Actors.Where(a => !a.IsDead).ToArray(); ActorInfo desiredUnit = null; var desiredError = int.MaxValue; foreach (var unit in buildableThings) { if (!Info.UnitsToBuild.TryGetValue(unit.Name, out var share) || (Info.UnitDelays != null && Info.UnitDelays.TryGetValue(unit.Name, out var delay) && delay > world.WorldTick)) continue; var unitCount = allUnits.Count(a => a.Info.Name == unit.Name); if (Info.UnitLimits != null && Info.UnitLimits.TryGetValue(unit.Name, out var count) && unitCount >= count) continue; var error = allUnits.Length > 0 ? unitCount * 100 / allUnits.Length - share : -1; if (error < 0) return HasAdequateAirUnitReloadBuildings(unit) ? unit : null; if (error < desiredError) { desiredError = error; desiredUnit = unit; } } return desiredUnit != null ? (HasAdequateAirUnitReloadBuildings(desiredUnit) ? desiredUnit : null) : 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.CountActorsWithNameAndTrait(actorInfo.Name, player); var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithNameAndTrait(b, player)); if (countOwnAir >= countBuildings) return false; return true; } List IGameSaveTraitData.IssueTraitData(Actor self) { if (IsTraitDisabled) return null; return new List() { new("QueuedBuildRequests", FieldSaver.FormatValue(queuedBuildRequests.ToArray())), new("IdleUnitCount", FieldSaver.FormatValue(idleUnitCount)) }; } void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) { if (self.World.IsReplay) return; var queuedBuildRequestsNode = data.NodeWithKeyOrDefault("QueuedBuildRequests"); if (queuedBuildRequestsNode != null) { queuedBuildRequests.Clear(); queuedBuildRequests.AddRange(FieldLoader.GetValue("QueuedBuildRequests", queuedBuildRequestsNode.Value.Value)); } var idleUnitCountNode = data.NodeWithKeyOrDefault("IdleUnitCount"); if (idleUnitCountNode != null) idleUnitCount = FieldLoader.GetValue("IdleUnitCount", idleUnitCountNode.Value.Value); } void INotifyActorDisposing.Disposing(Actor self) { unitsToBuild.Dispose(); } } }