diff --git a/OpenRA.Mods.Common/AI/AIHarvesterManager.cs b/OpenRA.Mods.Common/AI/AIHarvesterManager.cs deleted file mode 100644 index 33f636af29..0000000000 --- a/OpenRA.Mods.Common/AI/AIHarvesterManager.cs +++ /dev/null @@ -1,94 +0,0 @@ -#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; -using OpenRA.Mods.Common.Activities; -using OpenRA.Mods.Common.Pathfinder; -using OpenRA.Mods.Common.Traits; -using OpenRA.Support; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.AI -{ - class AIHarvesterManager - { - readonly HackyAI ai; - readonly World world; - readonly IPathFinder pathfinder; - readonly DomainIndex domainIndex; - readonly ResourceLayer resLayer; - readonly ResourceClaimLayer claimLayer; - - public AIHarvesterManager(HackyAI ai, Player p) - { - this.ai = ai; - world = p.World; - pathfinder = world.WorldActor.Trait(); - domainIndex = world.WorldActor.Trait(); - resLayer = world.WorldActor.TraitOrDefault(); - claimLayer = world.WorldActor.TraitOrDefault(); - } - - CPos FindNextResource(Actor actor, Harvester harv) - { - var locomotorInfo = actor.Info.TraitInfo().LocomotorInfo; - - Func isValidResource = cell => - domainIndex.IsPassable(actor.Location, cell, locomotorInfo) && - harv.CanHarvestCell(actor, cell) && - claimLayer.CanClaimCell(actor, cell); - - var path = pathfinder.FindPath( - PathSearch.Search(world, locomotorInfo, actor, true, isValidResource) - .WithCustomCost(loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), ai.Info.HarvesterEnemyAvoidanceRadius) - .Where(u => !u.IsDead && actor.Owner.Stances[u.Owner] == Stance.Enemy) - .Sum(u => Math.Max(WDist.Zero.Length, ai.Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))) - .FromPoint(actor.Location)); - - if (path.Count == 0) - return CPos.Zero; - - return path[0]; - } - - public void Tick(List harvesters) - { - if (resLayer == null || resLayer.IsResourceLayerEmpty) - return; - - // Find idle harvesters and give them orders: - foreach (var harvester in harvesters) - { - var harv = harvester.Trait(); - if (!harv.IsEmpty) - continue; - - if (!harvester.IsIdle) - { - var act = harvester.CurrentActivity; - if (!harv.LastSearchFailed || act.NextActivity == null || act.NextActivity.GetType() != typeof(FindResources)) - continue; - } - - var para = harvester.TraitOrDefault(); - if (para != null && para.IsInAir) - continue; - - // Tell the idle harvester to quit slacking: - var newSafeResourcePatch = FindNextResource(harvester, harv); - HackyAI.BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(harvester, newSafeResourcePatch)); - ai.QueueOrder(new Order("Harvest", harvester, Target.FromCell(world, newSafeResourcePatch), false)); - } - } - } -} diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index 2f953c99d9..2599efcf51 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -281,7 +281,6 @@ namespace OpenRA.Mods.Common.AI BitArray resourceTypeIndices; - AIHarvesterManager harvManager; AISupportPowerManager supportPowerManager; List builders = new List(); @@ -291,10 +290,6 @@ namespace OpenRA.Mods.Common.AI // Units that the ai already knows about. Any unit not on this list needs to be given a role. List activeUnits = new List(); - // Harvesters are usually listed under ExcludeFromSquads, so they're not included in the activeUnits list, but still needed in AIHarvesterManager. - // TODO: Consider adding an explicit UnitsCommonNames.Harvester category. - List harvesters = new List(); - public const int FeedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. public readonly World World; @@ -335,7 +330,6 @@ namespace OpenRA.Mods.Common.AI playerResource = p.PlayerActor.Trait(); botOrderManager = p.PlayerActor.Trait(); - harvManager = new AIHarvesterManager(this, p); supportPowerManager = new AISupportPowerManager(this, p); foreach (var building in Info.BuildingQueues) @@ -600,7 +594,6 @@ namespace OpenRA.Mods.Common.AI activeUnits.RemoveAll(unitCannotBeOrdered); unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); - harvesters.RemoveAll(unitCannotBeOrdered); if (--rushTicks <= 0) { @@ -619,7 +612,6 @@ namespace OpenRA.Mods.Common.AI { assignRolesTicks = Info.AssignRolesInterval; FindNewUnits(self); - harvManager.Tick(harvesters); InitializeBase(self, true); } @@ -723,13 +715,10 @@ namespace OpenRA.Mods.Common.AI void FindNewUnits(Actor self) { var newUnits = self.World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !activeUnits.Contains(a) && !harvesters.Contains(a)); + .Where(a => a.Owner == Player && !activeUnits.Contains(a)); foreach (var a in newUnits) { - if (a.Info.HasTraitInfo()) - harvesters.Add(a); - if (Info.UnitsCommonNames.Mcv.Contains(a.Info.Name) || Info.UnitsCommonNames.ExcludeFromSquads.Contains(a.Info.Name)) continue; diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 330ddc0ff4..e2aa444cdb 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -125,7 +125,7 @@ - + @@ -937,6 +937,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs new file mode 100644 index 0000000000..9326fb14c3 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs @@ -0,0 +1,133 @@ +#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; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.AI; +using OpenRA.Mods.Common.Pathfinder; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Put this on the Player actor. Manages bot harvesters to ensure they always continue harvesting as long as there are resources on the map.")] + public class HarvesterBotModuleInfo : ConditionalTraitInfo, Requires + { + [Desc("Interval (in ticks) between giving out orders to idle harvesters.")] + public readonly int ScanForIdleHarvestersInterval = 20; + + [Desc("Avoid enemy actors nearby when searching for a new resource patch. Should be somewhere near the max weapon range.")] + public readonly WDist HarvesterEnemyAvoidanceRadius = WDist.FromCells(8); + + public override object Create(ActorInitializer init) { return new HarvesterBotModule(init.Self, this); } + } + + public class HarvesterBotModule : ConditionalTrait, ITick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + IPathFinder pathfinder; + DomainIndex domainIndex; + ResourceLayer resLayer; + ResourceClaimLayer claimLayer; + BotOrderManager botOrderManager; + List harvesters = new List(); + int scanForIdleHarvestersTicks; + + public HarvesterBotModule(Actor self, HarvesterBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + unitCannotBeOrdered = a => a.Owner != self.Owner || a.IsDead || !a.IsInWorld; + } + + protected override void TraitEnabled(Actor self) + { + pathfinder = world.WorldActor.Trait(); + domainIndex = world.WorldActor.Trait(); + resLayer = world.WorldActor.TraitOrDefault(); + claimLayer = world.WorldActor.TraitOrDefault(); + botOrderManager = self.Owner.PlayerActor.Trait(); + scanForIdleHarvestersTicks = Info.ScanForIdleHarvestersInterval; + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled) + return; + + if (resLayer == null || resLayer.IsResourceLayerEmpty) + return; + + if (--scanForIdleHarvestersTicks > 0) + return; + + harvesters.RemoveAll(unitCannotBeOrdered); + scanForIdleHarvestersTicks = Info.ScanForIdleHarvestersInterval; + + // Find new harvesters + // TODO: Look for a more performance-friendly way to update this list + var newHarvesters = world.ActorsHavingTrait().Where(a => a.Owner == player && !harvesters.Contains(a)); + foreach (var a in newHarvesters) + harvesters.Add(a); + + // Find idle harvesters and give them orders: + foreach (var harvester in harvesters) + { + var harv = harvester.Trait(); + if (!harv.IsEmpty) + continue; + + if (!harvester.IsIdle) + { + var act = harvester.CurrentActivity; + if (!harv.LastSearchFailed || act.NextActivity == null || act.NextActivity.GetType() != typeof(FindResources)) + continue; + } + + var para = harvester.TraitOrDefault(); + if (para != null && para.IsInAir) + continue; + + // Tell the idle harvester to quit slacking: + var newSafeResourcePatch = FindNextResource(harvester, harv); + AIUtils.BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(harvester, newSafeResourcePatch)); + botOrderManager.QueueOrder(new Order("Harvest", harvester, Target.FromCell(world, newSafeResourcePatch), false)); + } + } + + CPos FindNextResource(Actor actor, Harvester harv) + { + var locomotorInfo = actor.Info.TraitInfo().LocomotorInfo; + + Func isValidResource = cell => + domainIndex.IsPassable(actor.Location, cell, locomotorInfo) && + harv.CanHarvestCell(actor, cell) && + claimLayer.CanClaimCell(actor, cell); + + var path = pathfinder.FindPath( + PathSearch.Search(world, locomotorInfo, actor, true, isValidResource) + .WithCustomCost(loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius) + .Where(u => !u.IsDead && actor.Owner.Stances[u.Owner] == Stance.Enemy) + .Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))) + .FromPoint(actor.Location)); + + if (path.Count == 0) + return CPos.Zero; + + return path[0]; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddHarvesterBotModule.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddHarvesterBotModule.cs new file mode 100644 index 0000000000..d8d8b6c94a --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/AddHarvesterBotModule.cs @@ -0,0 +1,122 @@ +#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 AddHarvesterBotModule : UpdateRule + { + public override string Name { get { return "Split HackyAI harvester handling to HarvesterBotModule"; } } + public override string Description + { + get + { + return "Some properties and all harvester handling have been moved from HackyAI\n" + + "to the new HarvesterBotModule."; + } + } + + bool messageShown; + + readonly string[] harvesterFields = + { + "HarvesterEnemyAvoidanceRadius", "AssignRolesInterval" + }; + + public override IEnumerable AfterUpdate(ModData modData) + { + var message = "You may want to check your AI yamls for possible redundant module entries.\n" + + "Additionally, make sure the Player actor has the ConditionManager trait and add it manually if it doesn't."; + + if (!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; + + var addNodes = new List(); + + // We add a 'default' HarvesterBotModule in any case, + // and only add more for AIs that define custom values for one of its fields. + var defaultHarvNode = new MiniYamlNode("HarvesterBotModule", ""); + + foreach (var hackyAINode in hackyAIs) + { + // HackyAIInfo.Name might contain spaces, so Type is better suited to be used as condition name + var aiType = hackyAINode.LastChildMatching("Type").NodeValue(); + var conditionString = "enable-" + aiType + "-ai"; + var requiresCondition = new MiniYamlNode("RequiresCondition", conditionString); + var conditionNode = hackyAINode.LastChildMatching("Condition"); + if (conditionNode == null) + { + var enableModule = new MiniYamlNode("Condition", conditionString); + hackyAINode.AddNode(enableModule); + } + + if (harvesterFields.Any(f => hackyAINode.ChildrenMatching(f).Any())) + { + var harvNode = new MiniYamlNode("HarvesterBotModule@" + aiType, ""); + harvNode.AddNode(requiresCondition); + + foreach (var hf in harvesterFields) + { + var fieldNode = hackyAINode.LastChildMatching(hf); + if (fieldNode != null) + { + if (hf == "AssignRolesInterval") + fieldNode.MoveAndRenameNode(hackyAINode, harvNode, "ScanForIdleHarvestersInterval"); + else + fieldNode.MoveNode(hackyAINode, harvNode); + } + } + + addNodes.Add(harvNode); + } + else + { + // We want the default module to be enabled for every AI that didn't customise one of its fields, + // so we need to update RequiresCondition to be enabled on any of the conditions granted by these AIs, + // but only if the condition hasn't been added yet. + var requiresConditionNode = defaultHarvNode.LastChildMatching("RequiresCondition"); + if (requiresConditionNode == null) + defaultHarvNode.AddNode(requiresCondition); + else + { + var oldValue = requiresConditionNode.NodeValue(); + if (oldValue.Contains(conditionString)) + continue; + + requiresConditionNode.ReplaceValue(oldValue + " || " + conditionString); + } + } + } + + addNodes.Add(defaultHarvNode); + + foreach (var node in addNodes) + actorNode.AddNode(node); + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 88376168c9..6e95ac3d53 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -106,6 +106,7 @@ namespace OpenRA.Mods.Common.UpdateRules new RequireProductionType(), new CloakRequiresConditionToPause(), new AddBotOrderManager(), + new AddHarvesterBotModule(), }) }; diff --git a/mods/cnc/rules/ai.yaml b/mods/cnc/rules/ai.yaml index 127043ceb6..ac8503cf0c 100644 --- a/mods/cnc/rules/ai.yaml +++ b/mods/cnc/rules/ai.yaml @@ -131,6 +131,7 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-cabal-ai HackyAI@Watson: Name: Watson Type: watson @@ -262,6 +263,7 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-watson-ai HackyAI@HAL9001: Name: HAL 9001 Type: hal9001 @@ -395,3 +397,6 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-hal9001-ai + HarvesterBotModule: + RequiresCondition: enable-cabal-ai || enable-watson-ai || enable-hal9001-ai diff --git a/mods/cnc/rules/player.yaml b/mods/cnc/rules/player.yaml index 58caf5843e..ed2e430061 100644 --- a/mods/cnc/rules/player.yaml +++ b/mods/cnc/rules/player.yaml @@ -58,3 +58,4 @@ Player: GrantConditionOnPrerequisiteManager: ResourceStorageWarning: PlayerExperience: + ConditionManager: diff --git a/mods/d2k/rules/ai.yaml b/mods/d2k/rules/ai.yaml index 52ec8eb865..f1f6a0e171 100644 --- a/mods/d2k/rules/ai.yaml +++ b/mods/d2k/rules/ai.yaml @@ -125,6 +125,7 @@ Player: OrderName: ProduceActorPower.Fremen Consideration@1: Against: Ally + Condition: enable-omnius-ai HackyAI@Vidius: Name: Vidious Type: vidious @@ -250,6 +251,7 @@ Player: OrderName: ProduceActorPower.Fremen Consideration@1: Against: Ally + Condition: enable-vidious-ai HackyAI@Gladius: Name: Gladius Type: gladius @@ -374,3 +376,6 @@ Player: OrderName: ProduceActorPower.Fremen Consideration@1: Against: Ally + Condition: enable-gladius-ai + HarvesterBotModule: + RequiresCondition: enable-omnius-ai || enable-vidious-ai || enable-gladius-ai diff --git a/mods/d2k/rules/player.yaml b/mods/d2k/rules/player.yaml index 3250bf91c8..4e34fba55d 100644 --- a/mods/d2k/rules/player.yaml +++ b/mods/d2k/rules/player.yaml @@ -144,3 +144,4 @@ Player: ResourceStorageWarning: AdviceInterval: 26 PlayerExperience: + ConditionManager: diff --git a/mods/ra/maps/fort-lonestar/rules.yaml b/mods/ra/maps/fort-lonestar/rules.yaml index 7be7e6031e..93b7158d26 100644 --- a/mods/ra/maps/fort-lonestar/rules.yaml +++ b/mods/ra/maps/fort-lonestar/rules.yaml @@ -105,6 +105,8 @@ Player: DefaultCashDropdownLocked: True DefaultCashDropdownVisible: False DefaultCash: 50 + -ConditionManager: + -HarvesterBotModule: -HackyAI@RushAI: -HackyAI@NormalAI: -HackyAI@NavalAI: diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index a4b132dd6e..dc8f695b38 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -121,6 +121,7 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-rush-ai HackyAI@NormalAI: Name: Normal AI Type: normal @@ -261,6 +262,7 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-normal-ai HackyAI@TurtleAI: Name: Turtle AI Type: turtle @@ -402,6 +404,7 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-turtle-ai HackyAI@NavalAI: Name: Naval AI Type: naval @@ -518,3 +521,6 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 + Condition: enable-naval-ai + HarvesterBotModule: + RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai || enable-naval-ai diff --git a/mods/ra/rules/player.yaml b/mods/ra/rules/player.yaml index c0673dcee0..7bb33aaa6c 100644 --- a/mods/ra/rules/player.yaml +++ b/mods/ra/rules/player.yaml @@ -143,3 +143,4 @@ Player: Sequence: veteran ResourceStorageWarning: PlayerExperience: + ConditionManager: diff --git a/mods/ts/rules/ai.yaml b/mods/ts/rules/ai.yaml index 0142942d3d..f7cf9ae0ae 100644 --- a/mods/ts/rules/ai.yaml +++ b/mods/ts/rules/ai.yaml @@ -78,3 +78,6 @@ Player: medic: 3 repair: 3 SquadSize: 20 + Condition: enable-test-ai + HarvesterBotModule: + RequiresCondition: enable-test-ai diff --git a/mods/ts/rules/player.yaml b/mods/ts/rules/player.yaml index 3d614dd34e..7851972559 100644 --- a/mods/ts/rules/player.yaml +++ b/mods/ts/rules/player.yaml @@ -114,3 +114,4 @@ Player: Id: unrestricted ResourceStorageWarning: PlayerExperience: + ConditionManager: