From 04c69efc307f90b37c837fb7a5df6cebe68db3dd Mon Sep 17 00:00:00 2001 From: reaperrr Date: Mon, 29 Oct 2018 03:42:31 +0100 Subject: [PATCH] Prepare HackyAI for module support - Split order handling to BotOrderManager - Make HackyAI provide a condition - Move BotDebug to AIUtils --- .../AI/AISupportPowerManager.cs | 12 +-- OpenRA.Mods.Common/AI/AIUtils.cs | 6 ++ OpenRA.Mods.Common/AI/BaseBuilder.cs | 28 +++---- OpenRA.Mods.Common/AI/BotOrderManager.cs | 54 +++++++++++++ OpenRA.Mods.Common/AI/HackyAI.cs | 39 +++++----- OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 2 + .../Rules/20180923/AddBotOrderManager.cs | 77 +++++++++++++++++++ OpenRA.Mods.Common/UpdateRules/UpdatePath.cs | 1 + mods/cnc/rules/ai.yaml | 1 + mods/d2k/rules/ai.yaml | 1 + mods/ra/rules/ai.yaml | 1 + mods/ts/rules/ai.yaml | 1 + 12 files changed, 183 insertions(+), 40 deletions(-) create mode 100644 OpenRA.Mods.Common/AI/BotOrderManager.cs create mode 100644 OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddBotOrderManager.cs diff --git a/OpenRA.Mods.Common/AI/AISupportPowerManager.cs b/OpenRA.Mods.Common/AI/AISupportPowerManager.cs index 9104cfc307..ad0ad249f2 100644 --- a/OpenRA.Mods.Common/AI/AISupportPowerManager.cs +++ b/OpenRA.Mods.Common/AI/AISupportPowerManager.cs @@ -65,14 +65,14 @@ namespace OpenRA.Mods.Common.AI var powerDecision = powerDecisions[sp.Info.OrderName]; if (powerDecision == null) { - HackyAI.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", sp.Info.OrderName); + AIUtils.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", sp.Info.OrderName); continue; } var attackLocation = FindCoarseAttackLocationToSupportPower(sp); if (attackLocation == null) { - HackyAI.BotDebug("AI: {1} can't find suitable coarse attack location for support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); + AIUtils.BotDebug("AI: {1} can't find suitable coarse attack location for support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); waitingPowers[sp] += powerDecision.GetNextScanTime(ai); continue; @@ -82,14 +82,14 @@ namespace OpenRA.Mods.Common.AI attackLocation = FindFineAttackLocationToSupportPower(sp, (CPos)attackLocation); if (attackLocation == null) { - HackyAI.BotDebug("AI: {1} can't find suitable final attack location for support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); + AIUtils.BotDebug("AI: {1} can't find suitable final attack location for support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); waitingPowers[sp] += powerDecision.GetNextScanTime(ai); continue; } // Valid target found, delay by a few ticks to avoid rescanning before power fires via order - HackyAI.BotDebug("AI: {2} found new target location {0} for support power {1}.", attackLocation, sp.Info.OrderName, player.PlayerName); + AIUtils.BotDebug("AI: {2} found new target location {0} for support power {1}.", attackLocation, sp.Info.OrderName, player.PlayerName); waitingPowers[sp] += 10; ai.QueueOrder(new Order(sp.Key, supportPowerManager.Self, Target.FromCell(world, attackLocation.Value), false) { SuppressVisualFeedback = true }); } @@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.AI var powerDecision = powerDecisions[readyPower.Info.OrderName]; if (powerDecision == null) { - HackyAI.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", readyPower.Info.OrderName); + AIUtils.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", readyPower.Info.OrderName); return null; } @@ -144,7 +144,7 @@ namespace OpenRA.Mods.Common.AI var powerDecision = powerDecisions[readyPower.Info.OrderName]; if (powerDecision == null) { - HackyAI.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", readyPower.Info.OrderName); + AIUtils.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", readyPower.Info.OrderName); return null; } diff --git a/OpenRA.Mods.Common/AI/AIUtils.cs b/OpenRA.Mods.Common/AI/AIUtils.cs index 477f985bcb..f48c287160 100644 --- a/OpenRA.Mods.Common/AI/AIUtils.cs +++ b/OpenRA.Mods.Common/AI/AIUtils.cs @@ -47,5 +47,11 @@ namespace OpenRA.Mods.Common.AI .All(ac => terrainTypes.Contains(map.GetTerrainInfo(ac).Type)))) .Any(availableCells => availableCells > 0); } + + public static void BotDebug(string s, params object[] args) + { + if (Game.Settings.Debug.BotDebug) + Game.Debug(s, args); + } } } diff --git a/OpenRA.Mods.Common/AI/BaseBuilder.cs b/OpenRA.Mods.Common/AI/BaseBuilder.cs index 8e5b568147..96a009849c 100644 --- a/OpenRA.Mods.Common/AI/BaseBuilder.cs +++ b/OpenRA.Mods.Common/AI/BaseBuilder.cs @@ -146,7 +146,7 @@ namespace OpenRA.Mods.Common.AI var location = ai.ChooseBuildLocation(currentBuilding.Item, true, type); if (location == null) { - HackyAI.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item)); + AIUtils.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item)); ai.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1)); failCount += failCount; @@ -216,7 +216,7 @@ namespace OpenRA.Mods.Common.AI { if (power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0) { - HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name); return power; } } @@ -227,13 +227,13 @@ namespace OpenRA.Mods.Common.AI var refinery = GetProducibleBuilding(ai.Info.BuildingCommonNames.Refinery, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) { - HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); + AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); return refinery; } if (power != null && refinery != null && !HasSufficientPowerForActor(refinery)) { - HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return power; } } @@ -244,13 +244,13 @@ namespace OpenRA.Mods.Common.AI var production = GetProducibleBuilding(ai.Info.BuildingCommonNames.Production, buildableThings); if (production != null && HasSufficientPowerForActor(production)) { - HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); + AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); return production; } if (power != null && production != null && !HasSufficientPowerForActor(production)) { - HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return power; } } @@ -263,13 +263,13 @@ namespace OpenRA.Mods.Common.AI var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings); if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) { - HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); + AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); return navalproduction; } if (power != null && navalproduction != null && !HasSufficientPowerForActor(navalproduction)) { - HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return power; } } @@ -280,13 +280,13 @@ namespace OpenRA.Mods.Common.AI var silo = GetProducibleBuilding(ai.Info.BuildingCommonNames.Silo, buildableThings); if (silo != null && HasSufficientPowerForActor(silo)) { - HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); + AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); return silo; } if (power != null && silo != null && !HasSufficientPowerForActor(silo)) { - HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return power; } } @@ -324,22 +324,22 @@ namespace OpenRA.Mods.Common.AI if (power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0) { if (playerPower.PowerOutageRemainingTicks > 0) - HackyAI.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); else - HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return power; } } // Lets build this - HackyAI.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}", + AIUtils.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}", queue.Actor.Owner, name, frac.Value, frac.Value * playerBuildings.Length, playerBuildings.Length, count); return actor; } // Too spammy to keep enabled all the time, but very useful when debugging specific issues. - // HackyAI.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); + // AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); return null; } } diff --git a/OpenRA.Mods.Common/AI/BotOrderManager.cs b/OpenRA.Mods.Common/AI/BotOrderManager.cs new file mode 100644 index 0000000000..97987d61ad --- /dev/null +++ b/OpenRA.Mods.Common/AI/BotOrderManager.cs @@ -0,0 +1,54 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.AI +{ + public sealed class BotOrderManagerInfo : ITraitInfo + { + [Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). Excess orders remain queued for subsequent ticks.")] + public readonly int MinOrderQuotientPerTick = 5; + + public object Create(ActorInitializer init) { return new BotOrderManager(this); } + } + + public sealed class BotOrderManager : ITick + { + readonly BotOrderManagerInfo info; + readonly Queue orders = new Queue(); + + public BotOrderManager(BotOrderManagerInfo info) + { + this.info = info; + } + + public void QueueOrder(Order order) + { + orders.Enqueue(order); + } + + void IssueOrders(World world) + { + var ordersToIssueThisTick = Math.Min((orders.Count + info.MinOrderQuotientPerTick - 1) / info.MinOrderQuotientPerTick, orders.Count); + for (var i = 0; i < ordersToIssueThisTick; i++) + world.IssueOrder(orders.Dequeue()); + } + + void ITick.Tick(Actor self) + { + // Make sure we tick after all of the bot modules so that we don't introduce an additional tick delay + self.World.AddFrameEndTask(IssueOrders); + } + } +} diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index de5f514ceb..2f953c99d9 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -21,7 +21,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.AI { - public sealed class HackyAIInfo : IBotInfo, ITraitInfo + public sealed class HackyAIInfo : IBotInfo, ITraitInfo, Requires { public class UnitCategories { @@ -49,6 +49,11 @@ namespace OpenRA.Mods.Common.AI [Desc("Human-readable name this bot uses.")] public readonly string Name = "Unnamed Bot"; + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("Condition to grant. Mostly used to activate modules.")] + public readonly string Condition = null; + [Desc("Minimum number of units AI must have before attacking.")] public readonly int SquadSize = 8; @@ -73,9 +78,6 @@ namespace OpenRA.Mods.Common.AI [Desc("Minimum delay (in ticks) between creating squads.")] public readonly int MinimumAttackForceDelay = 0; - [Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). Excess orders remain queued for subsequent ticks.")] - public readonly int MinOrderQuotientPerTick = 5; - [Desc("Minimum excess power the AI should try to maintain.")] public readonly int MinimumExcessPower = 0; @@ -269,6 +271,9 @@ namespace OpenRA.Mods.Common.AI readonly Func isEnemyUnit; readonly Predicate unitCannotBeOrdered; + BotOrderManager botOrderManager; + int conditionToken = ConditionManager.InvalidConditionToken; + CPos initialBaseCenter; PowerManager playerPower; PlayerResources playerResource; @@ -303,8 +308,6 @@ namespace OpenRA.Mods.Common.AI int minCaptureDelayTicks; readonly int maximumCaptureTargetOptions; - readonly Queue orders = new Queue(); - public HackyAI(HackyAIInfo info, ActorInitializer init) { Info = info; @@ -323,12 +326,6 @@ namespace OpenRA.Mods.Common.AI maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); } - public static void BotDebug(string s, params object[] args) - { - if (Game.Settings.Debug.BotDebug) - Game.Debug(s, args); - } - // Called by the host's player creation code public void Activate(Player p) { @@ -336,6 +333,7 @@ namespace OpenRA.Mods.Common.AI IsEnabled = true; playerPower = p.PlayerActor.TraitOrDefault(); playerResource = p.PlayerActor.Trait(); + botOrderManager = p.PlayerActor.Trait(); harvManager = new AIHarvesterManager(this, p); supportPowerManager = new AISupportPowerManager(this, p); @@ -361,11 +359,16 @@ namespace OpenRA.Mods.Common.AI resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough foreach (var t in Map.Rules.Actors["world"].TraitInfos()) resourceTypeIndices.Set(tileset.GetTerrainIndex(t.TerrainType), true); + + var conditionManager = p.PlayerActor.TraitOrDefault(); + if (conditionManager != null && conditionToken == ConditionManager.InvalidConditionToken) + conditionToken = conditionManager.GrantCondition(p.PlayerActor, Info.Condition); } + // DEPRECATED: Bot modules should queue orders directly. public void QueueOrder(Order order) { - orders.Enqueue(order); + botOrderManager.QueueOrder(order); } ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) @@ -553,10 +556,6 @@ namespace OpenRA.Mods.Common.AI foreach (var b in builders) b.Tick(); - - var ordersToIssueThisTick = Math.Min((orders.Count + Info.MinOrderQuotientPerTick - 1) / Info.MinOrderQuotientPerTick, orders.Count); - for (var i = 0; i < ordersToIssueThisTick; i++) - World.IssueOrder(orders.Dequeue()); } internal Actor FindClosestEnemy(WPos pos) @@ -711,7 +710,7 @@ namespace OpenRA.Mods.Common.AI return; QueueOrder(new Order(target.OrderString, capturer, Target.FromActor(target.Actor), true)); - BotDebug("AI ({0}): Ordered {1} to capture {2}", Player.ClientIndex, capturer, target.Actor); + AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", Player.ClientIndex, capturer, target.Actor); activeUnits.Remove(capturer); } @@ -853,7 +852,7 @@ namespace OpenRA.Mods.Common.AI if (!possibleRallyPoints.Any()) { - BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location); + AIUtils.BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location); return producer.Location; } @@ -976,7 +975,7 @@ namespace OpenRA.Mods.Common.AI { if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light && !rb.RepairActive) { - BotDebug("Bot noticed damage {0} {1}->{2}, repairing.", + AIUtils.BotDebug("Bot noticed damage {0} {1}->{2}, repairing.", self, e.PreviousDamageState, e.DamageState); QueueOrder(new Order("RepairBuilding", self.Owner.PlayerActor, Target.FromActor(self), false)); } diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 5dbbfa4cfb..330ddc0ff4 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -123,6 +123,7 @@ + @@ -935,6 +936,7 @@ + diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddBotOrderManager.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddBotOrderManager.cs new file mode 100644 index 0000000000..f7a73e3aa5 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddBotOrderManager.cs @@ -0,0 +1,77 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class AddBotOrderManager : UpdateRule + { + public override string Name { get { return "Split bot order management from HackyAI to BotOrderManager"; } } + public override string Description + { + get + { + return "The MinOrderQuotientPerTick property and all bot order handling have been moved from HackyAI\n" + + "to the new BotOrderManager."; + } + } + + bool showMessage; + bool messageShown; + + public override IEnumerable AfterUpdate(ModData modData) + { + var message = "You may want to manually change MinOrderQuotientPerTick on BotOrderManager,\n" + + "if you were using a custom value on any AI."; + + if (showMessage && !messageShown) + yield return message; + + messageShown = true; + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNode actorNode) + { + if (actorNode.Key != "Player") + yield break; + + var hackyAIs = actorNode.ChildrenMatching("HackyAI"); + if (!hackyAIs.Any()) + yield break; + + foreach (var hackyAINode in hackyAIs) + { + // We no longer support individual values for each AI, + // and in practice the default of 5 has proven to be a solid middle-ground, + // so just removing any custom value and notifying the modder about it should suffice. + var minQuotient = hackyAINode.LastChildMatching("MinOrderQuotientPerTick"); + if (minQuotient != null) + { + hackyAINode.RemoveNode(minQuotient); + if (!showMessage) + showMessage = true; + } + } + + var botOrderManager = actorNode.LastChildMatching("BotOrderManager"); + if (botOrderManager == null) + { + var addBotOrderManager = new MiniYamlNode("BotOrderManager", ""); + actorNode.AddNode(addBotOrderManager); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index e7ddc05131..88376168c9 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -105,6 +105,7 @@ namespace OpenRA.Mods.Common.UpdateRules new RemovedDemolishLocking(), new RequireProductionType(), new CloakRequiresConditionToPause(), + new AddBotOrderManager(), }) }; diff --git a/mods/cnc/rules/ai.yaml b/mods/cnc/rules/ai.yaml index 35958973d5..127043ceb6 100644 --- a/mods/cnc/rules/ai.yaml +++ b/mods/cnc/rules/ai.yaml @@ -1,4 +1,5 @@ Player: + BotOrderManager: HackyAI@Cabal: Name: Cabal Type: cabal diff --git a/mods/d2k/rules/ai.yaml b/mods/d2k/rules/ai.yaml index f7ca63718e..52ec8eb865 100644 --- a/mods/d2k/rules/ai.yaml +++ b/mods/d2k/rules/ai.yaml @@ -1,4 +1,5 @@ Player: + BotOrderManager: HackyAI@Omnius: Name: Omnius Type: omnius diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index c4035a207c..a4b132dd6e 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -1,4 +1,5 @@ Player: + BotOrderManager: HackyAI@RushAI: Name: Rush AI Type: rush diff --git a/mods/ts/rules/ai.yaml b/mods/ts/rules/ai.yaml index ef6f9f24c2..0142942d3d 100644 --- a/mods/ts/rules/ai.yaml +++ b/mods/ts/rules/ai.yaml @@ -1,4 +1,5 @@ Player: + BotOrderManager: HackyAI@TestAI: Name: Test AI Type: test