diff --git a/OpenRA.Mods.Common/Activities/CaptureActor.cs b/OpenRA.Mods.Common/Activities/CaptureActor.cs index 128a48734e..7c03ee9eb6 100644 --- a/OpenRA.Mods.Common/Activities/CaptureActor.cs +++ b/OpenRA.Mods.Common/Activities/CaptureActor.cs @@ -37,7 +37,7 @@ namespace OpenRA.Mods.Common.Activities } if (!targetIsDeadOrHiddenActor && target.Type != TargetType.FrozenActor && - (enterCaptureManager == null || !enterCaptureManager.CanBeTargetedBy(enterActor, self, manager))) + (enterCaptureManager == null || !manager.CanTarget(enterCaptureManager))) Cancel(self, true); } @@ -51,7 +51,7 @@ namespace OpenRA.Mods.Common.Activities // Make sure we can still capture the target before entering // (but not before, because this may stop the actor in the middle of nowhere) - if (enterCaptureManager == null || !enterCaptureManager.CanBeTargetedBy(enterActor, self, manager)) + if (enterCaptureManager == null || !manager.CanTarget(enterCaptureManager)) { Cancel(self, true); return false; @@ -59,7 +59,7 @@ namespace OpenRA.Mods.Common.Activities // StartCapture returns false when a capture delay is enabled // We wait until it returns true before allowing entering the target - if (!manager.StartCapture(self, enterActor, enterCaptureManager, out var captures)) + if (!manager.StartCapture(enterCaptureManager, out var captures)) return false; if (!captures.Info.ConsumedByCapture) @@ -80,11 +80,11 @@ namespace OpenRA.Mods.Common.Activities if (enterActor != targetActor) return; - if (enterCaptureManager.BeingCaptured || !enterCaptureManager.CanBeTargetedBy(enterActor, self, manager)) + if (enterCaptureManager.BeingCaptured || !manager.CanTarget(enterCaptureManager)) return; // Prioritize capturing over sabotaging - var captures = manager.ValidCapturesWithLowestSabotageThreshold(self, enterActor, enterCaptureManager); + var captures = manager.ValidCapturesWithLowestSabotageThreshold(enterCaptureManager); if (captures == null) return; @@ -134,25 +134,25 @@ namespace OpenRA.Mods.Common.Activities protected override void OnLastRun(Actor self) { - CancelCapture(self); + CancelCapture(); base.OnLastRun(self); } protected override void OnActorDispose(Actor self) { - CancelCapture(self); + CancelCapture(); base.OnActorDispose(self); } public override void Cancel(Actor self, bool keepQueue = false) { - CancelCapture(self); + CancelCapture(); base.Cancel(self, keepQueue); } - void CancelCapture(Actor self) + void CancelCapture() { - manager.CancelCapture(self, enterActor, enterCaptureManager); + manager.CancelCapture(enterCaptureManager); } } } diff --git a/OpenRA.Mods.Common/Scripting/Properties/CaptureProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/CaptureProperties.cs index f8b3f40f60..97c7661177 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/CaptureProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/CaptureProperties.cs @@ -41,7 +41,7 @@ namespace OpenRA.Mods.Common.Scripting public bool CanCapture(Actor target) { var targetManager = target.TraitOrDefault(); - return targetManager != null && targetManager.CanBeTargetedBy(target, Self, captureManager); + return targetManager != null && captureManager.CanTarget(targetManager); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs index a45dc1dd1d..a7819659d7 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs @@ -130,7 +130,7 @@ namespace OpenRA.Mods.Common.Traits if (captureManager == null) return false; - return capturers.Any(tp => captureManager.CanBeTargetedBy(target, tp.Actor, tp.Trait)); + return capturers.Any(tp => tp.Trait.CanTarget(captureManager)); }) .OrderByDescending(target => target.GetSellValue()) .Take(maximumCaptureTargetOptions); diff --git a/OpenRA.Mods.Common/Traits/Capturable.cs b/OpenRA.Mods.Common/Traits/Capturable.cs index 575c25d3f3..38d38e008a 100644 --- a/OpenRA.Mods.Common/Traits/Capturable.cs +++ b/OpenRA.Mods.Common/Traits/Capturable.cs @@ -22,9 +22,6 @@ namespace OpenRA.Mods.Common.Traits [Desc("CaptureTypes (from the Captures trait) that are able to capture this.")] public readonly BitSet Types = default; - [Desc("What player relationships the target's owner needs to be captured by this actor.")] - public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; - [Desc("Cancel the actor's current activity when getting captured.")] public readonly bool CancelActivity = false; diff --git a/OpenRA.Mods.Common/Traits/CaptureManager.cs b/OpenRA.Mods.Common/Traits/CaptureManager.cs index 9f3ac771c7..c66f1bffe3 100644 --- a/OpenRA.Mods.Common/Traits/CaptureManager.cs +++ b/OpenRA.Mods.Common/Traits/CaptureManager.cs @@ -38,32 +38,21 @@ namespace OpenRA.Mods.Common.Traits [Desc("Should units friendly to the capturing actor auto-target this actor while it is being captured?")] public readonly bool PreventsAutoTarget = true; - public override object Create(ActorInitializer init) { return new CaptureManager(this); } - - public bool CanBeTargetedBy(FrozenActor frozenActor, Actor captor, Captures captures) - { - if (captures.IsTraitDisabled) - return false; - - // TODO: FrozenActors don't yet have a way of caching conditions, so we can't filter disabled traits - // This therefore assumes that all Capturable traits are enabled, which is probably wrong. - // Actors with FrozenUnderFog should therefore not disable the Capturable trait. - var stance = captor.Owner.RelationshipWith(frozenActor.Owner); - return frozenActor.Info.TraitInfos() - .Any(c => c.ValidRelationships.HasRelationship(stance) && captures.Info.CaptureTypes.Overlaps(c.Types)); - } + public override object Create(ActorInitializer init) { return new CaptureManager(init.Self, this); } } public class CaptureManager : INotifyCreated, INotifyCapture, ITick, IDisableEnemyAutoTarget { + readonly Actor self; readonly CaptureManagerInfo info; + IMove move; ICaptureProgressWatcher[] progressWatchers; - BitSet allyCapturableTypes; - BitSet neutralCapturableTypes; - BitSet enemyCapturableTypes; - BitSet capturesTypes; + BitSet allyCapturesTypes; + BitSet neutralCapturesTypes; + BitSet enemyCapturesTypes; + BitSet capturableTypes; IEnumerable enabledCapturable; IEnumerable enabledCaptures; @@ -81,8 +70,9 @@ namespace OpenRA.Mods.Common.Traits public bool BeingCaptured { get; private set; } - public CaptureManager(CaptureManagerInfo info) + public CaptureManager(Actor self, CaptureManagerInfo info) { + this.self = self; this.info = info; } @@ -103,69 +93,74 @@ namespace OpenRA.Mods.Common.Traits RefreshCapturable(); } - public void RefreshCapturable() + public void RefreshCaptures() { - allyCapturableTypes = neutralCapturableTypes = enemyCapturableTypes = default; - foreach (var c in enabledCapturable) + allyCapturesTypes = neutralCapturesTypes = enemyCapturesTypes = default; + foreach (var c in enabledCaptures) { if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Ally)) - allyCapturableTypes = allyCapturableTypes.Union(c.Info.Types); + allyCapturesTypes = allyCapturesTypes.Union(c.Info.CaptureTypes); if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Neutral)) - neutralCapturableTypes = neutralCapturableTypes.Union(c.Info.Types); + neutralCapturesTypes = neutralCapturesTypes.Union(c.Info.CaptureTypes); if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Enemy)) - enemyCapturableTypes = enemyCapturableTypes.Union(c.Info.Types); + enemyCapturesTypes = enemyCapturesTypes.Union(c.Info.CaptureTypes); } } - public void RefreshCaptures() + public void RefreshCapturable() { - capturesTypes = enabledCaptures.Aggregate( + capturableTypes = enabledCapturable.Aggregate( default(BitSet), - (a, b) => a.Union(b.Info.CaptureTypes)); + (a, b) => a.Union(b.Info.Types)); } - public bool CanBeTargetedBy(Actor self, Actor captor, CaptureManager captorManager) + /// Should only be called from the captor's CaptureManager. + public bool CanTarget(CaptureManager target) { - var relationship = captor.Owner.RelationshipWith(self.Owner); - if (relationship.HasRelationship(PlayerRelationship.Enemy)) - return captorManager.capturesTypes.Overlaps(enemyCapturableTypes); - - if (relationship.HasRelationship(PlayerRelationship.Neutral)) - return captorManager.capturesTypes.Overlaps(neutralCapturableTypes); - - if (relationship.HasRelationship(PlayerRelationship.Ally)) - return captorManager.capturesTypes.Overlaps(allyCapturableTypes); - - return false; + return CanTarget(target.self.Owner, target.capturableTypes); } - public bool CanBeTargetedBy(Actor self, Actor captor, Captures captures) + /// Should only be called from the captor CaptureManager. + public bool CanTarget(FrozenActor target) { - if (captures.IsTraitDisabled) + if (!target.Info.HasTraitInfo()) return false; - var relationship = captor.Owner.RelationshipWith(self.Owner); + // TODO: FrozenActors don't yet have a way of caching conditions, so we can't filter disabled traits + // This therefore assumes that all Capturable traits are enabled, which is probably wrong. + // Actors with FrozenUnderFog should therefore not disable the Capturable trait. + var targetTypes = target.Info.TraitInfos().Aggregate( + default(BitSet), + (a, b) => a.Union(b.Types)); + + return CanTarget(target.Owner, targetTypes); + } + + bool CanTarget(Player target, BitSet captureTypes) + { + var relationship = self.Owner.RelationshipWith(target); if (relationship.HasRelationship(PlayerRelationship.Enemy)) - return captures.Info.CaptureTypes.Overlaps(enemyCapturableTypes); + return captureTypes.Overlaps(enemyCapturesTypes); if (relationship.HasRelationship(PlayerRelationship.Neutral)) - return captures.Info.CaptureTypes.Overlaps(neutralCapturableTypes); + return captureTypes.Overlaps(neutralCapturesTypes); if (relationship.HasRelationship(PlayerRelationship.Ally)) - return captures.Info.CaptureTypes.Overlaps(allyCapturableTypes); + return captureTypes.Overlaps(allyCapturesTypes); return false; } - public Captures ValidCapturesWithLowestSabotageThreshold(Actor self, Actor captee, CaptureManager capteeManager) + public Captures ValidCapturesWithLowestSabotageThreshold(CaptureManager target) { - if (captee.IsDead) + if (target.self.IsDead) return null; + var relationship = self.Owner.RelationshipWith(target.self.Owner); foreach (var c in enabledCaptures.OrderBy(c => c.Info.SabotageThreshold).ThenBy(c => c.Info.CaptureDelay)) - if (capteeManager.CanBeTargetedBy(captee, self, c)) + if (c.Info.ValidRelationships.HasRelationship(relationship) && target.capturableTypes.Overlaps(c.Info.CaptureTypes)) return c; return null; @@ -182,7 +177,7 @@ namespace OpenRA.Mods.Common.Traits /// This method grants the capturing conditions on the captor and target and returns /// true if the captor is able to start entering or false if it needs to wait. /// - public bool StartCapture(Actor self, Actor target, CaptureManager targetManager, out Captures captures) + public bool StartCapture(CaptureManager targetManager, out Captures captures) { captures = null; @@ -190,10 +185,11 @@ namespace OpenRA.Mods.Common.Traits if (self.WillDispose) return false; + var target = targetManager.self; if (target != currentTarget) { - if (currentTarget != null) - CancelCapture(self, currentTarget, currentTargetManager); + if (currentTargetManager != null) + CancelCapture(currentTargetManager); targetManager.currentCaptors.Add(self); currentTarget = target; @@ -209,7 +205,7 @@ namespace OpenRA.Mods.Common.Traits if (targetManager.beingCapturedToken == Actor.InvalidConditionToken) targetManager.beingCapturedToken = target.GrantCondition(targetManager.info.BeingCapturedCondition); - captures = ValidCapturesWithLowestSabotageThreshold(self, target, targetManager); + captures = ValidCapturesWithLowestSabotageThreshold(targetManager); if (captures == null) return false; @@ -243,11 +239,12 @@ namespace OpenRA.Mods.Common.Traits /// This method revokes the capturing conditions on the captor and target /// and resets any capturing progress. /// - public void CancelCapture(Actor self, Actor target, CaptureManager targetManager) + public void CancelCapture(CaptureManager targetManager) { if (currentTarget == null) return; + var target = targetManager.self; foreach (var w in progressWatchers) w.Update(self, self, target, 0, 0); diff --git a/OpenRA.Mods.Common/Traits/Captures.cs b/OpenRA.Mods.Common/Traits/Captures.cs index 935ac88fbf..2b5f3513e2 100644 --- a/OpenRA.Mods.Common/Traits/Captures.cs +++ b/OpenRA.Mods.Common/Traits/Captures.cs @@ -43,6 +43,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Experience granted to the capturing player.")] public readonly int PlayerExperience = 0; + [Desc("What player relationships the target's owner needs to be captured by this actor.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + [Desc("Relationships that the structure's previous owner needs to have for the capturing player to receive Experience.")] public readonly PlayerRelationship PlayerExperienceRelationships = PlayerRelationship.Enemy; @@ -69,12 +72,12 @@ namespace OpenRA.Mods.Common.Traits public class Captures : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice { - readonly CaptureManager captureManager; + public readonly CaptureManager CaptureManager; public Captures(Actor self, CapturesInfo info) : base(info) { - captureManager = self.Trait(); + CaptureManager = self.Trait(); } public IEnumerable Orders @@ -110,8 +113,8 @@ namespace OpenRA.Mods.Common.Traits self.ShowTargetLines(); } - protected override void TraitEnabled(Actor self) { captureManager.RefreshCaptures(); } - protected override void TraitDisabled(Actor self) { captureManager.RefreshCaptures(); } + protected override void TraitEnabled(Actor self) { CaptureManager.RefreshCaptures(); } + protected override void TraitDisabled(Actor self) { CaptureManager.RefreshCaptures(); } sealed class CaptureOrderTargeter : UnitOrderTargeter { @@ -125,8 +128,8 @@ namespace OpenRA.Mods.Common.Traits public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) { - var captureManager = target.TraitOrDefault(); - if (captureManager == null || !captureManager.CanBeTargetedBy(target, self, captures)) + var targetManager = target.TraitOrDefault(); + if (targetManager == null || !captures.CaptureManager.CanTarget(targetManager)) { cursor = captures.Info.EnterBlockedCursor; return false; @@ -147,8 +150,7 @@ namespace OpenRA.Mods.Common.Traits public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) { - var captureManagerInfo = target.Info.TraitInfoOrDefault(); - if (captureManagerInfo == null || !captureManagerInfo.CanBeTargetedBy(target, self, captures)) + if (!captures.CaptureManager.CanTarget(target)) { cursor = captures.Info.EnterBlockedCursor; return false; diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs new file mode 100644 index 0000000000..6434cd3a7c --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs @@ -0,0 +1,46 @@ +#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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RemoveValidRelationsFromCapturable : UpdateRule + { + public override string Name => "Remove ValidRelations property from Capturable."; + + public override string Description => "ValidRelations has been moved from Capturable to Captures to match weapon definitions."; + + readonly List locations = new(); + + public override IEnumerable AfterUpdate(ModData modData) + { + if (locations.Any()) + yield return Description + "\n" + + "ValidRelations have been removed from:\n" + + UpdateUtils.FormatMessageList(locations); + + locations.Clear(); + } + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + foreach (var capturable in actorNode.ChildrenMatching("Capturable")) + { + if (capturable.RemoveNodes("ValidRelations") > 0) + locations.Add($"{actorNode.Key}: {capturable.Key} ({actorNode.Location.Filename})"); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 129ec04ba0..17c7c01a24 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -108,6 +108,7 @@ namespace OpenRA.Mods.Common.UpdateRules new UpdatePath("playtest-20230801", new UpdateRule[] { // bleed only changes here. + new RemoveValidRelationsFromCapturable(), // Execute these rules last to avoid premature yaml merge crashes. new AbstractDocking(), diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 88c808381f..68cfd06c67 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -1076,7 +1076,6 @@ CaptureManager: Capturable: Types: husk - ValidRelationships: Enemy, Neutral, Ally TransformOnCapture: ForceHealthPercentage: 25 Tooltip: diff --git a/mods/cnc/rules/infantry.yaml b/mods/cnc/rules/infantry.yaml index e861c28671..81ec266c54 100644 --- a/mods/cnc/rules/infantry.yaml +++ b/mods/cnc/rules/infantry.yaml @@ -213,8 +213,12 @@ E6: SabotageThreshold: 55 PlayerExperience: 10 Captures@CAPTURES: - CaptureTypes: building, husk + CaptureTypes: building PlayerExperience: 10 + Captures@CATURESHUSK: + CaptureTypes: husk + PlayerExperience: 10 + ValidRelationships: Enemy, Neutral, Ally -AttackFrontal: RMBO: diff --git a/mods/ra/maps/intervention/rules.yaml b/mods/ra/maps/intervention/rules.yaml index bb8b9ed3ff..3547b3cefd 100644 --- a/mods/ra/maps/intervention/rules.yaml +++ b/mods/ra/maps/intervention/rules.yaml @@ -25,7 +25,6 @@ MISS: Name: Soviet Air Force HQ Capturable: Types: building - ValidRelationships: Enemy CaptureManager: TENT: diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index 6bc53b19ce..c99af96dd3 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -1062,7 +1062,6 @@ CaptureManager: Capturable: Types: husk - ValidRelationships: Enemy, Neutral TransformOnCapture: ForceHealthPercentage: 15 InfiltrateForTransform: