diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index fa137dc4dc..c72e3e343a 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -132,6 +132,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("When should the AI start building specific buildings.")] public readonly Dictionary BuildingDelays = null; + [Desc("Only queue construction of a new structure when above this requirement.")] + public readonly int ProductionMinCashRequirement = 500; + public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); } } @@ -149,6 +152,9 @@ namespace OpenRA.Mods.Common.Traits public CPos DefenseCenter { get; private set; } + // Actor, ActorCount. + public Dictionary BuildingsBeingProduced = new(); + readonly World world; readonly Player player; PowerManager playerPower; @@ -156,13 +162,16 @@ namespace OpenRA.Mods.Common.Traits IResourceLayer resourceLayer; IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; - readonly List builders = new(); + + readonly BaseBuilderQueueManager[] builders; + int currentBuilderIndex = 0; public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; + builders = new BaseBuilderQueueManager[info.BuildingQueues.Count + info.DefenseQueues.Count]; } protected override void Created(Actor self) @@ -171,14 +180,14 @@ namespace OpenRA.Mods.Common.Traits playerResources = self.Owner.PlayerActor.Trait(); resourceLayer = self.World.WorldActor.TraitOrDefault(); positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing().ToArray(); - } - protected override void TraitEnabled(Actor self) - { + var i = 0; + foreach (var building in Info.BuildingQueues) - builders.Add(new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer)); + builders[i++] = new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer); + foreach (var defense in Info.DefenseQueues) - builders.Add(new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer)); + builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer); } void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) @@ -195,10 +204,49 @@ namespace OpenRA.Mods.Common.Traits void IBotTick.BotTick(IBot bot) { + // TODO: this causes pathfinding lag when AI's gets blocked in SetRallyPointsForNewProductionBuildings(bot); - foreach (var b in builders) - b.Tick(bot); + BuildingsBeingProduced.Clear(); + + // 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 + var findQueue = false; + for (int i = 0, builderIndex = currentBuilderIndex; i < builders.Length; i++) + { + if (++builderIndex >= builders.Length) + builderIndex = 0; + + --builders[builderIndex].WaitTicks; + + var queues = AIUtils.FindQueues(player, builders[builderIndex].Category).ToArray(); + if (queues.Length != 0) + { + if (!findQueue) + { + currentBuilderIndex = builderIndex; + findQueue = true; + } + + // Refresh "BuildingsBeingProduced" only when AI can produce + if (playerResources.GetCashAndResources() >= Info.ProductionMinCashRequirement) + { + foreach (var queue in queues) + { + var producing = queue.AllQueued().FirstOrDefault(); + if (producing == null) + continue; + + if (BuildingsBeingProduced.TryGetValue(producing.Item, out var number)) + BuildingsBeingProduced[producing.Item] = number + 1; + else + BuildingsBeingProduced.Add(producing.Item, 1); + } + } + } + } + + builders[currentBuilderIndex].Tick(bot); } void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 4a7354b71c..4285432359 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -18,7 +18,8 @@ namespace OpenRA.Mods.Common.Traits { sealed class BaseBuilderQueueManager { - readonly string category; + public readonly string Category; + public int WaitTicks; readonly BaseBuilderBotModule baseBuilder; readonly World world; @@ -27,7 +28,6 @@ namespace OpenRA.Mods.Common.Traits readonly PlayerResources playerResources; readonly IResourceLayer resourceLayer; - int waitTicks; Actor[] playerBuildings; int failCount; int failRetryTicks; @@ -36,6 +36,8 @@ namespace OpenRA.Mods.Common.Traits int cachedBuildings; int minimumExcessPower; + bool itemQueuedThisTick = false; + WaterCheck waterState = WaterCheck.NotChecked; public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, @@ -47,7 +49,7 @@ namespace OpenRA.Mods.Common.Traits playerPower = pm; playerResources = pr; resourceLayer = rl; - this.category = category; + Category = category; failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; minimumExcessPower = baseBuilder.Info.MinimumExcessPower; if (baseBuilder.Info.NavalProductionTypes.Count == 0) @@ -94,23 +96,27 @@ namespace OpenRA.Mods.Common.Traits } // Only update once per second or so - if (--waitTicks > 0) + if (WaitTicks > 0) return; playerBuildings = world.ActorsHavingTrait().Where(a => a.Owner == player).ToArray(); var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Length / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower); + // PERF: Queue only one actor at a time per category + itemQueuedThisTick = false; var active = false; - foreach (var queue in AIUtils.FindQueues(player, category)) + foreach (var queue in AIUtils.FindQueues(player, Category)) + { if (TickQueue(bot, queue)) active = true; + } // Add a random factor so not every AI produces at the same tick early in the game. // Minimum should not be negative as delays in HackyAI could be zero. var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay); - waitTicks = active ? baseBuilder.Info.StructureProductionActiveDelay + randomFactor + WaitTicks = active ? baseBuilder.Info.StructureProductionActiveDelay + randomFactor : baseBuilder.Info.StructureProductionInactiveDelay + randomFactor; } @@ -121,11 +127,16 @@ namespace OpenRA.Mods.Common.Traits // Waiting to build something if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts) { + // PERF: We shouldn't be queueing new units when we're low on cash + if (playerResources.GetCashAndResources() < baseBuilder.Info.ProductionMinCashRequirement || itemQueuedThisTick) + return false; + var item = ChooseBuildingToBuild(queue); if (item == null) return false; bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1)); + itemQueuedThisTick = true; } else if (currentBuilding != null && currentBuilding.Done) { @@ -335,7 +346,7 @@ namespace OpenRA.Mods.Common.Traits var buildingVariantInfo = actorInfo.TraitInfoOrDefault(); var variants = buildingVariantInfo?.Actors ?? Array.Empty(); - var count = playerBuildings.Count(a => a.Info.Name == name || variants.Contains(a.Info.Name)); + var count = playerBuildings.Count(a => a.Info.Name == name || variants.Contains(a.Info.Name)) + (baseBuilder.BuildingsBeingProduced.TryGetValue(name, out var num) ? num : 0); // Do we want to build this structure? if (count * 100 > frac.Value * playerBuildings.Length) diff --git a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs index b388387d63..9e34d0bc61 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs @@ -26,7 +26,7 @@ namespace OpenRA.Mods.Common.Traits public readonly int IdleBaseUnitsMaximum = 12; [Desc("Production queues AI uses for producing units.")] - public readonly HashSet UnitQueues = new() { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; + 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; @@ -37,6 +37,9 @@ namespace OpenRA.Mods.Common.Traits [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); } } @@ -51,6 +54,8 @@ namespace OpenRA.Mods.Common.Traits IBotRequestPauseUnitProduction[] requestPause; int idleUnitCount; + int currentQueueIndex = 0; + PlayerResources playerResources; int ticks; @@ -64,6 +69,7 @@ namespace OpenRA.Mods.Common.Traits protected override void Created(Actor self) { requestPause = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + playerResources = self.Owner.PlayerActor.Trait(); } void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) @@ -73,7 +79,8 @@ namespace OpenRA.Mods.Common.Traits void IBotTick.BotTick(IBot bot) { - if (requestPause.Any(rp => rp.PauseUnitProduction)) + // 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++; @@ -87,8 +94,19 @@ namespace OpenRA.Mods.Common.Traits queuedBuildRequests.Remove(buildRequest); } - foreach (var q in Info.UnitQueues) - BuildUnit(bot, q, idleUnitCount < Info.IdleBaseUnitsMaximum); + 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 + BuildUnit(bot, Info.UnitQueues[currentQueueIndex], idleUnitCount < Info.IdleBaseUnitsMaximum); + break; + } + } } }