diff --git a/OpenRA.Game/Actor.cs b/OpenRA.Game/Actor.cs index 8407a315f1..c688a4eaf9 100644 --- a/OpenRA.Game/Actor.cs +++ b/OpenRA.Game/Actor.cs @@ -44,6 +44,7 @@ namespace OpenRA public Rectangle VisualBounds { get; private set; } public IEffectiveOwner EffectiveOwner { get; private set; } public IOccupySpace OccupiesSpace { get; private set; } + public ITargetable[] Targetables { get; private set; } public bool IsIdle { get { return currentActivity == null; } } public bool IsDead { get { return Disposed || (health != null && health.IsDead); } } @@ -110,6 +111,7 @@ namespace OpenRA disables = TraitsImplementing().ToArray(); visibilityModifiers = TraitsImplementing().ToArray(); defaultVisibility = Trait(); + Targetables = TraitsImplementing().ToArray(); } Rectangle DetermineBounds() @@ -334,6 +336,33 @@ namespace OpenRA return defaultVisibility.IsVisible(this, player); } + public IEnumerable GetAllTargetTypes() + { + // PERF: Avoid LINQ. + foreach (var targetable in Targetables) + foreach (var targetType in targetable.TargetTypes) + yield return targetType; + } + + public IEnumerable GetEnabledTargetTypes() + { + // PERF: Avoid LINQ. + foreach (var targetable in Targetables) + if (targetable.IsTraitEnabled()) + foreach (var targetType in targetable.TargetTypes) + yield return targetType; + } + + public bool IsTargetableBy(Actor byActor) + { + // PERF: Avoid LINQ. + foreach (var targetable in Targetables) + if (targetable.IsTraitEnabled() && targetable.TargetableBy(this, byActor)) + return true; + + return false; + } + #region Scripting interface Lazy luaInterface; diff --git a/OpenRA.Game/GameRules/WeaponInfo.cs b/OpenRA.Game/GameRules/WeaponInfo.cs index 133d35dc47..16b6ff2548 100644 --- a/OpenRA.Game/GameRules/WeaponInfo.cs +++ b/OpenRA.Game/GameRules/WeaponInfo.cs @@ -127,14 +127,17 @@ namespace OpenRA.GameRules /// Checks if the weapon is valid against (can target) the actor. public bool IsValidAgainst(Actor victim, Actor firedBy) { - var targetable = victim.TraitsImplementing().Where(Exts.IsTraitEnabled); - if (!IsValidTarget(targetable.SelectMany(t => t.TargetTypes))) + var targetTypes = victim.GetEnabledTargetTypes(); + + if (!IsValidTarget(targetTypes)) return false; - if (!Warheads.Any(w => w.IsValidAgainst(victim, firedBy))) - return false; + // PERF: Avoid LINQ. + foreach (var warhead in Warheads) + if (warhead.IsValidAgainst(victim, firedBy)) + return true; - return true; + return false; } /// Checks if the weapon is valid against (can target) the frozen actor. diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index f53e9a0895..1e21983465 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.cs @@ -159,13 +159,27 @@ namespace OpenRA public bool CanTargetActor(Actor a) { - if (HasFogVisibility && fogVisibilities.Any(f => f.IsVisible(a))) - return true; + // PERF: Avoid LINQ. + if (HasFogVisibility) + foreach (var fogVisibility in fogVisibilities) + if (fogVisibility.IsVisible(a)) + return true; return CanViewActor(a); } - public bool HasFogVisibility { get { return fogVisibilities.Any(f => f.HasFogVisibility()); } } + public bool HasFogVisibility + { + get + { + // PERF: Avoid LINQ. + foreach (var fogVisibility in fogVisibilities) + if (fogVisibility.HasFogVisibility()) + return true; + + return false; + } + } #region Scripting interface diff --git a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs index b77df6f1f9..2522a454fe 100644 --- a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs +++ b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs @@ -62,7 +62,7 @@ namespace OpenRA.Traits CenterPosition = self.CenterPosition; Bounds = self.Bounds; - TargetTypes = self.TraitsImplementing().Where(Exts.IsTraitEnabled).SelectMany(t => t.TargetTypes).ToHashSet(); + TargetTypes = self.GetEnabledTargetTypes().ToHashSet(); UpdateVisibility(); } diff --git a/OpenRA.Game/Traits/Target.cs b/OpenRA.Game/Traits/Target.cs index d8c8e3fff3..226d2cdd51 100644 --- a/OpenRA.Game/Traits/Target.cs +++ b/OpenRA.Game/Traits/Target.cs @@ -22,7 +22,6 @@ namespace OpenRA.Traits TargetType type; Actor actor; - IEnumerable targetable; FrozenActor frozen; WPos pos; int generation; @@ -48,7 +47,6 @@ namespace OpenRA.Traits return new Target { actor = a, - targetable = a.TraitsImplementing(), type = TargetType.Actor, generation = a.Generation, }; @@ -83,8 +81,7 @@ namespace OpenRA.Traits if (targeter == null || Type == TargetType.Invalid) return false; - var targeted = this.actor; - if (targeted != null && !targetable.Any(t => t.IsTraitEnabled() && t.TargetableBy(targeted, targeter))) + if (actor != null && !actor.IsTargetableBy(targeter)) return false; return true; @@ -94,7 +91,25 @@ namespace OpenRA.Traits // TODO: either replace based on target type or put in singleton trait public bool RequiresForceFire { - get { return targetable != null && targetable.Any(Exts.IsTraitEnabled) && targetable.Where(Exts.IsTraitEnabled).All(t => t.RequiresForceFire); } + get + { + if (actor == null) + return false; + + // PERF: Avoid LINQ. + var isTargetable = false; + foreach (var targetable in actor.Targetables) + { + if (!targetable.IsTraitEnabled()) + continue; + + isTargetable = true; + if (!targetable.RequiresForceFire) + return false; + } + + return isTargetable; + } } // Representative position - see Positions for the full set of targetable positions. @@ -126,8 +141,7 @@ namespace OpenRA.Traits switch (Type) { case TargetType.Actor: - var targetable = actor.TraitsImplementing().Where(Exts.IsTraitEnabled); - if (!targetable.Any()) + if (!actor.Targetables.Any(Exts.IsTraitEnabled)) return new[] { actor.CenterPosition }; var targetablePositions = actor.TraitOrDefault(); diff --git a/OpenRA.Mods.Common/AI/States/StateBase.cs b/OpenRA.Mods.Common/AI/States/StateBase.cs index b63d0d0cde..36f923a243 100644 --- a/OpenRA.Mods.Common/AI/States/StateBase.cs +++ b/OpenRA.Mods.Common/AI/States/StateBase.cs @@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.AI if (!a.Info.HasTraitInfo()) return false; - var targetTypes = target.TraitsImplementing().Where(Exts.IsTraitEnabled).SelectMany(t => t.TargetTypes); + var targetTypes = target.GetEnabledTargetTypes(); if (!targetTypes.Any()) return false; diff --git a/OpenRA.Mods.Common/AI/SupportPowerDecision.cs b/OpenRA.Mods.Common/AI/SupportPowerDecision.cs index c1fc4921f8..bd59c822e0 100644 --- a/OpenRA.Mods.Common/AI/SupportPowerDecision.cs +++ b/OpenRA.Mods.Common/AI/SupportPowerDecision.cs @@ -125,11 +125,10 @@ namespace OpenRA.Mods.Common.AI if (a == null) return 0; - var targetable = a.TraitsImplementing().Where(Exts.IsTraitEnabled); - if (!targetable.Any(t => t.TargetableBy(a, firedBy.PlayerActor))) + if (!a.IsTargetableBy(firedBy.PlayerActor)) return 0; - if (Types.Overlaps(targetable.SelectMany(t => t.TargetTypes))) + if (Types.Overlaps(a.GetEnabledTargetTypes())) { switch (TargetMetric) { diff --git a/OpenRA.Mods.Common/Activities/Attack.cs b/OpenRA.Mods.Common/Activities/Attack.cs index 2ec721b818..0f3c54a7db 100644 --- a/OpenRA.Mods.Common/Activities/Attack.cs +++ b/OpenRA.Mods.Common/Activities/Attack.cs @@ -68,8 +68,8 @@ namespace OpenRA.Mods.Common.Activities return NextActivity; // Drop the target once none of the weapons are effective against it - var armaments = attack.ChooseArmamentsForTarget(Target, forceAttack); - if (!armaments.Any()) + var armaments = attack.ChooseArmamentsForTarget(Target, forceAttack).ToList(); + if (armaments.Count == 0) return NextActivity; // Update ranges diff --git a/OpenRA.Mods.Common/Activities/Hunt.cs b/OpenRA.Mods.Common/Activities/Hunt.cs index d101a3ede1..e9b61f37b9 100644 --- a/OpenRA.Mods.Common/Activities/Hunt.cs +++ b/OpenRA.Mods.Common/Activities/Hunt.cs @@ -25,12 +25,7 @@ namespace OpenRA.Mods.Common.Activities var attack = self.Trait(); targets = self.World.ActorsHavingTrait().Where( a => self != a && !a.IsDead && a.IsInWorld && a.AppearsHostileTo(self) - && IsTargetable(a, self) && attack.HasAnyValidWeapons(Target.FromActor(a))); - } - - bool IsTargetable(Actor self, Actor viewer) - { - return self.TraitsImplementing().Any(t => t.IsTraitEnabled() && t.TargetableBy(self, viewer)); + && a.IsTargetableBy(self) && attack.HasAnyValidWeapons(Target.FromActor(a))); } public override Activity Tick(Actor self) diff --git a/OpenRA.Mods.Common/Orders/UnitOrderTargeter.cs b/OpenRA.Mods.Common/Orders/UnitOrderTargeter.cs index f6a82fa872..8de25c8889 100644 --- a/OpenRA.Mods.Common/Orders/UnitOrderTargeter.cs +++ b/OpenRA.Mods.Common/Orders/UnitOrderTargeter.cs @@ -67,9 +67,9 @@ namespace OpenRA.Mods.Common.Orders public class TargetTypeOrderTargeter : UnitOrderTargeter { - readonly string[] targetTypes; + readonly HashSet targetTypes; - public TargetTypeOrderTargeter(string[] targetTypes, string order, int priority, string cursor, bool targetEnemyUnits, bool targetAllyUnits) + public TargetTypeOrderTargeter(HashSet targetTypes, string order, int priority, string cursor, bool targetEnemyUnits, bool targetAllyUnits) : base(order, priority, cursor, targetEnemyUnits, targetAllyUnits) { this.targetTypes = targetTypes; @@ -77,7 +77,7 @@ namespace OpenRA.Mods.Common.Orders public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) { - return target.TraitsImplementing().Any(t => t.IsTraitEnabled() && t.TargetTypes.Overlaps(targetTypes)); + return targetTypes.Overlaps(target.GetEnabledTargetTypes()); } public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index 9fe9105413..3580e2cacd 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -189,7 +189,12 @@ namespace OpenRA.Mods.Common.Traits if (Info.AttackRequiresEnteringCell && !positionable.Value.CanEnterCell(t.Actor.Location, null, false)) return false; - return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self)); + // PERF: Avoid LINQ. + foreach (var armament in Armaments) + if (armament.Weapon.IsValidAgainst(t, self.World, self)) + return true; + + return false; } public WDist GetMinimumRange() @@ -197,9 +202,17 @@ namespace OpenRA.Mods.Common.Traits if (IsTraitDisabled) return WDist.Zero; - var min = Armaments.Where(a => !a.IsTraitDisabled) - .Select(a => a.Weapon.MinRange) - .Append(WDist.MaxValue).Min(); + // PERF: Avoid LINQ. + var min = WDist.MaxValue; + foreach (var armament in Armaments) + { + if (armament.IsTraitDisabled) + continue; + var range = armament.Weapon.MinRange; + if (min > range) + min = range; + } + return min != WDist.MaxValue ? min : WDist.Zero; } @@ -208,23 +221,32 @@ namespace OpenRA.Mods.Common.Traits if (IsTraitDisabled) return WDist.Zero; - return Armaments.Where(a => !a.IsTraitDisabled) - .Select(a => a.MaxRange()) - .Append(WDist.Zero).Max(); + // PERF: Avoid LINQ. + var max = WDist.Zero; + foreach (var armament in Armaments) + { + if (armament.IsTraitDisabled) + continue; + var range = armament.MaxRange(); + if (max < range) + max = range; + } + + return max; } // Enumerates all armaments, that this actor possesses, that can be used against Target t - public IEnumerable ChooseArmamentsForTarget(Target t, bool forceAttack, bool onlyEnabled = true) + public IEnumerable ChooseArmamentsForTarget(Target t, bool forceAttack) { // If force-fire is not used, and the target requires force-firing or the target is // terrain or invalid, no armaments can be used - if (!forceAttack && (t.RequiresForceFire || t.Type == TargetType.Terrain || t.Type == TargetType.Invalid)) + if (!forceAttack && (t.Type == TargetType.Terrain || t.Type == TargetType.Invalid || t.RequiresForceFire)) return Enumerable.Empty(); // Get target's owner; in case of terrain or invalid target there will be no problems // with owner == null since forceFire will have to be true in this part of the method // (short-circuiting in the logical expression below) - var owner = null as Player; + Player owner = null; if (t.Type == TargetType.FrozenActor) { owner = t.FrozenActor.Owner; @@ -241,10 +263,11 @@ namespace OpenRA.Mods.Common.Traits owner = t.Actor.Owner; } - return Armaments.Where(a => (!a.IsTraitDisabled || !onlyEnabled) - && a.Weapon.IsValidAgainst(t, self.World, self) + return Armaments.Where(a => + !a.IsTraitDisabled && (owner == null || (forceAttack ? a.Info.ForceTargetStances : a.Info.TargetStances) - .HasStance(self.Owner.Stances[owner]))); + .HasStance(self.Owner.Stances[owner])) + && a.Weapon.IsValidAgainst(t, self.World, self)); } public void AttackTarget(Target target, bool queued, bool allowMove, bool forceAttack = false) diff --git a/OpenRA.Mods.Common/Traits/AutoTarget.cs b/OpenRA.Mods.Common/Traits/AutoTarget.cs index 9120e2abcc..38882c3822 100644 --- a/OpenRA.Mods.Common/Traits/AutoTarget.cs +++ b/OpenRA.Mods.Common/Traits/AutoTarget.cs @@ -135,24 +135,21 @@ namespace OpenRA.Mods.Common.Traits --nextScanTime; } - public Actor ScanForTarget(Actor self, Actor currentTarget, bool allowMove) + public Actor ScanForTarget(Actor self, bool allowMove) { if (nextScanTime <= 0) { + nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval); var range = info.ScanRadius > 0 ? WDist.FromCells(info.ScanRadius) : attack.GetMaximumRange(); - if (self.IsIdle || currentTarget == null || !Target.FromActor(currentTarget).IsInRange(self.CenterPosition, range)) - { - nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval); - return ChooseTarget(self, range, allowMove); - } + return ChooseTarget(self, range, allowMove); } - return currentTarget; + return null; } public void ScanAndAttack(Actor self, bool allowMove) { - var targetActor = ScanForTarget(self, null, allowMove); + var targetActor = ScanForTarget(self, allowMove); if (targetActor != null) Attack(self, targetActor, allowMove); } @@ -167,43 +164,57 @@ namespace OpenRA.Mods.Common.Traits Actor ChooseTarget(Actor self, WDist range, bool allowMove) { - var inRange = self.World.FindActorsInCircle(self.CenterPosition, range) - .Where(a => - !a.TraitsImplementing().Any(t => t.PreventsAutoTarget(a, self))); + var actorsByArmament = new Dictionary>(); + var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, range); + foreach (var actor in actorsInRange) + { + if (PreventsAutoTarget(self, actor) || !self.Owner.CanTargetActor(actor)) + continue; + + // Select only the first compatible armament for each actor: if this actor is selected + // it will be thanks to the first armament anyways, since that is the first selection + // criterion + var target = Target.FromActor(actor); + var armaments = attack.ChooseArmamentsForTarget(target, false); + if (!allowMove) + armaments = armaments.Where(arm => + target.IsInRange(self.CenterPosition, arm.MaxRange()) && + !target.IsInRange(self.CenterPosition, arm.Weapon.MinRange)); + + var armament = armaments.FirstOrDefault(); + if (armament == null) + continue; + + List actors; + if (actorsByArmament.TryGetValue(armament, out actors)) + actors.Add(actor); + else + actorsByArmament.Add(armament, new List { actor }); + } // Armaments are enumerated in attack.Armaments in construct order // When autotargeting, first choose targets according to the used armament construct order // And then according to distance from actor // This enables preferential treatment of certain armaments // (e.g. tesla trooper's tesla zap should have precedence over tesla charge) - var actorByArmament = inRange - - // Select only the first compatible armament for each actor: if this actor is selected - // it will be thanks to the first armament anyways, since that is the first selection - // criterion - .Select(a => - { - var target = Target.FromActor(a); - return new KeyValuePair( - attack.ChooseArmamentsForTarget(target, false) - .FirstOrDefault(arm => allowMove - || (target.IsInRange(self.CenterPosition, arm.MaxRange()) - && !target.IsInRange(self.CenterPosition, arm.Weapon.MinRange))), a); - }) - - .Where(kv => kv.Key != null && self.Owner.CanTargetActor(kv.Value)) - .GroupBy(kv => kv.Key, kv => kv.Value) - .ToDictionary(kv => kv.Key, kv => kv.ClosestTo(self)); - foreach (var arm in attack.Armaments) { - Actor actor; - if (actorByArmament.TryGetValue(arm, out actor)) - return actor; + List actors; + if (actorsByArmament.TryGetValue(arm, out actors)) + return actors.ClosestTo(self); } return null; } + + bool PreventsAutoTarget(Actor attacker, Actor target) + { + foreach (var pat in target.TraitsImplementing()) + if (pat.PreventsAutoTarget(target, attacker)) + return true; + + return false; + } } [Desc("Will not get automatically targeted by enemy (like walls)")] diff --git a/OpenRA.Mods.Common/Traits/Crates/DuplicateUnitCrateAction.cs b/OpenRA.Mods.Common/Traits/Crates/DuplicateUnitCrateAction.cs index 66c5343746..a33201dfaf 100644 --- a/OpenRA.Mods.Common/Traits/Crates/DuplicateUnitCrateAction.cs +++ b/OpenRA.Mods.Common/Traits/Crates/DuplicateUnitCrateAction.cs @@ -58,8 +58,7 @@ namespace OpenRA.Mods.Common.Traits if (info.ValidFactions.Any() && !info.ValidFactions.Contains(collector.Owner.Faction.InternalName)) return false; - var targetable = collector.TraitsImplementing().Where(Exts.IsTraitEnabled); - if (!info.ValidTargets.Overlaps(targetable.SelectMany(t => t.TargetTypes))) + if (!info.ValidTargets.Overlaps(collector.GetEnabledTargetTypes())) return false; var positionable = collector.TraitOrDefault(); diff --git a/OpenRA.Mods.Common/Warheads/Warhead.cs b/OpenRA.Mods.Common/Warheads/Warhead.cs index 3856771a41..2dd0eed660 100644 --- a/OpenRA.Mods.Common/Warheads/Warhead.cs +++ b/OpenRA.Mods.Common/Warheads/Warhead.cs @@ -56,8 +56,7 @@ namespace OpenRA.Mods.Common.Warheads return false; // A target type is valid if it is in the valid targets list, and not in the invalid targets list. - var targetable = victim.TraitsImplementing().Where(Exts.IsTraitEnabled); - if (!IsValidTarget(targetable.SelectMany(t => t.TargetTypes))) + if (!IsValidTarget(victim.GetEnabledTargetTypes())) return false; return true; diff --git a/OpenRA.Mods.RA/Traits/DemoTruck.cs b/OpenRA.Mods.RA/Traits/DemoTruck.cs index 9b50d545c7..93eb4e0672 100644 --- a/OpenRA.Mods.RA/Traits/DemoTruck.cs +++ b/OpenRA.Mods.RA/Traits/DemoTruck.cs @@ -44,7 +44,7 @@ namespace OpenRA.Mods.RA.Traits { get { - yield return new TargetTypeOrderTargeter(new[] { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; + yield return new TargetTypeOrderTargeter(new HashSet { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; yield return new DeployOrderTargeter("Detonate", 5); } } diff --git a/OpenRA.Mods.RA/Traits/Disguise.cs b/OpenRA.Mods.RA/Traits/Disguise.cs index 49a86156b5..05a4dab353 100644 --- a/OpenRA.Mods.RA/Traits/Disguise.cs +++ b/OpenRA.Mods.RA/Traits/Disguise.cs @@ -94,7 +94,7 @@ namespace OpenRA.Mods.RA.Traits { get { - yield return new TargetTypeOrderTargeter(new[] { "Disguise" }, "Disguise", 7, "ability", true, true) { ForceAttack = false }; + yield return new TargetTypeOrderTargeter(new HashSet { "Disguise" }, "Disguise", 7, "ability", true, true) { ForceAttack = false }; } } diff --git a/OpenRA.Mods.RA/Traits/Infiltration/Infiltrates.cs b/OpenRA.Mods.RA/Traits/Infiltration/Infiltrates.cs index 7e31df4c61..dc2fa3a2e1 100644 --- a/OpenRA.Mods.RA/Traits/Infiltration/Infiltrates.cs +++ b/OpenRA.Mods.RA/Traits/Infiltration/Infiltrates.cs @@ -77,8 +77,7 @@ namespace OpenRA.Mods.RA.Traits targetTypes = frozen.TargetTypes; } else - targetTypes = order.TargetActor.TraitsImplementing().Where(Exts.IsTraitEnabled) - .SelectMany(t => t.TargetTypes); + targetTypes = order.TargetActor.GetEnabledTargetTypes(); return Info.Types.Overlaps(targetTypes); } @@ -96,7 +95,7 @@ namespace OpenRA.Mods.RA.Traits var target = self.ResolveFrozenActorOrder(order, Color.Red); if (target.Type != TargetType.Actor - || !Info.Types.Overlaps(target.Actor.TraitsImplementing().SelectMany(t => t.TargetTypes))) + || !Info.Types.Overlaps(target.Actor.GetAllTargetTypes())) return; if (!order.Queued) @@ -124,7 +123,7 @@ namespace OpenRA.Mods.RA.Traits if (!info.ValidStances.HasStance(stance)) return false; - return target.TraitsImplementing().Any(t => t.TargetTypes.Overlaps(info.Types)); + return info.Types.Overlaps(target.GetAllTargetTypes()); } public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) @@ -134,7 +133,7 @@ namespace OpenRA.Mods.RA.Traits if (!info.ValidStances.HasStance(stance)) return false; - return target.Info.TraitInfos().Any(t => info.Types.Overlaps(t.GetTargetTypes())); + return info.Types.Overlaps(target.Info.TraitInfos().SelectMany(ti => ti.GetTargetTypes())); } } } diff --git a/OpenRA.Mods.RA/Traits/MadTank.cs b/OpenRA.Mods.RA/Traits/MadTank.cs index 8c3345a692..d473d170db 100644 --- a/OpenRA.Mods.RA/Traits/MadTank.cs +++ b/OpenRA.Mods.RA/Traits/MadTank.cs @@ -98,7 +98,7 @@ namespace OpenRA.Mods.RA.Traits { get { - yield return new TargetTypeOrderTargeter(new[] { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; + yield return new TargetTypeOrderTargeter(new HashSet { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; yield return new DeployOrderTargeter("Detonate", 5); } }